From 53c1fcc3077d63a8f56ba70d11731e2142a456b0 Mon Sep 17 00:00:00 2001 From: kevinwang5658 <20214115+kevinwang5658@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:25:53 +0000 Subject: [PATCH 1/8] feat: Add android-cli resource (auto-generated from issue #68) --- .../(resources)/editors-ides/android-cli.mdx | 90 ++++++++++ src/index.ts | 4 + .../android/android-cli/android-cli.ts | 165 ++++++++++++++++++ .../android/android-cli/android-emulator.ts | 116 ++++++++++++ .../android-sdk-packages-parameter.ts | 27 +++ .../completions/android-cli.packages.ts | 22 +++ .../completions/android-emulator.profile.ts | 54 ++++++ src/resources/android/android-cli/examples.ts | 64 +++++++ test/android/android-cli.test.ts | 71 ++++++++ 9 files changed, 613 insertions(+) create mode 100644 docs/resources/(resources)/editors-ides/android-cli.mdx create mode 100644 src/resources/android/android-cli/android-cli.ts create mode 100644 src/resources/android/android-cli/android-emulator.ts create mode 100644 src/resources/android/android-cli/android-sdk-packages-parameter.ts create mode 100644 src/resources/android/android-cli/completions/android-cli.packages.ts create mode 100644 src/resources/android/android-cli/completions/android-emulator.profile.ts create mode 100644 src/resources/android/android-cli/examples.ts create mode 100644 test/android/android-cli.test.ts diff --git a/docs/resources/(resources)/editors-ides/android-cli.mdx b/docs/resources/(resources)/editors-ides/android-cli.mdx new file mode 100644 index 00000000..13b768af --- /dev/null +++ b/docs/resources/(resources)/editors-ides/android-cli.mdx @@ -0,0 +1,90 @@ +--- +title: android-cli +description: Reference pages for the Android CLI resources +--- + +The Android CLI resources install and configure [Android CLI](https://developer.android.com/tools/agents/android-cli), Google's command-line tool for managing the Android development environment. Two resources are provided: one for installing the CLI and managing SDK packages, and one for provisioning Android Virtual Devices (AVDs). + +--- + +## android-cli + +Installs the `android` command-line tool and manages Android SDK packages declaratively. On macOS, Android CLI is installed via Homebrew (`android/tap`). On Linux (AMD64 only), it is installed via the official curl script to `~/.local/bin`. + +### Parameters + +- **sdkPath**: *(string)* Path to the Android SDK directory. Written to `~/.androidrc` as `--sdk=`. If not specified, the android CLI uses its default SDK location. +- **packages**: *(string[])* Android SDK packages to install. Package paths use forward-slash notation matching the `android sdk install` command (e.g. `platforms/android-35`, `build-tools/35.0.0`, `cmdline-tools/latest`, `platform-tools`, `system-images/android-35/google_apis_playstore/x86_64`). + +### Example usage + +```json title="codify.jsonc" +[ + { + "type": "android-cli", + "packages": [ + "cmdline-tools/latest", + "platform-tools", + "platforms/android-35", + "build-tools/35.0.0" + ] + } +] +``` + +### Notes + +- Linux ARM64 is **not** supported by Android CLI. Only AMD64/x86_64 is supported on Linux. +- Run `android sdk list --all` to see all available package identifiers. +- Run `android info` to display the default SDK path in use. + +--- + +## android-emulator + +Creates and manages an Android Virtual Device (AVD) using `android emulator create`. Each emulator declaration is an independent resource, identified by its `profile` and optional `name`. + +Depends on `android-cli` being installed. + +> **Note:** There is no `android emulator delete` command. Destroy uses `avdmanager delete avd` (from the `cmdline-tools` package) if available, or falls back to removing AVD files directly from `~/.android/avd/`. + +### Parameters + +- **profile** *(required)*: *(string)* Android hardware profile for the emulator (e.g. `medium_phone`, `pixel_9`). Run `android emulator create --list-profiles` to see available profiles. +- **name**: *(string)* Custom name for the Android Virtual Device. Defaults to the profile name. + +### Example usage + +```json title="codify.jsonc" +[ + { + "type": "android-cli", + "packages": [ + "cmdline-tools/latest", + "platform-tools", + "platforms/android-35", + "system-images/android-35/google_apis_playstore/x86_64" + ] + }, + { + "type": "android-emulator", + "profile": "pixel_9" + } +] +``` + +### Common profiles + +| Profile | Description | +|---------|-------------| +| `medium_phone` | Generic medium phone (default) | +| `small_phone` | Generic small phone | +| `foldable` | Foldable form factor | +| `medium_tablet` | Generic medium tablet | +| `pixel_9` | Google Pixel 9 | +| `pixel_9_pro` | Google Pixel 9 Pro | +| `pixel_9_pro_fold` | Google Pixel 9 Pro Fold | +| `pixel_8` | Google Pixel 8 | +| `wear_os_large_round` | Wear OS round watch | +| `tv_1080p` | Android TV 1080p | +| `automotive_1024p_landscape` | Android Automotive | diff --git a/src/index.ts b/src/index.ts index 0cb3831b..57787256 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import { Plugin, runPlugin } from '@codifycli/plugin-core'; +import { AndroidCliResource } from './resources/android/android-cli/android-cli.js'; +import { AndroidEmulatorResource } from './resources/android/android-cli/android-emulator.js'; import { AndroidStudioResource } from './resources/android/android-studio.js'; import { AptResource } from './resources/apt/apt.js'; import { AsdfResource } from './resources/asdf/asdf.js'; @@ -113,6 +115,8 @@ runPlugin(Plugin.create( new GitRepositoryResource(), new GitRepositoriesResource(), new AndroidStudioResource(), + new AndroidCliResource(), + new AndroidEmulatorResource(), new AsdfResource(), new AsdfPluginResource(), new AsdfInstallResource(), diff --git a/src/resources/android/android-cli/android-cli.ts b/src/resources/android/android-cli/android-cli.ts new file mode 100644 index 00000000..4fc38406 --- /dev/null +++ b/src/resources/android/android-cli/android-cli.ts @@ -0,0 +1,165 @@ +import { + CreatePlan, + DestroyPlan, + ModifyPlan, + PackageManager, + ParameterChange, + Resource, + ResourceSettings, + SpawnStatus, + Utils, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import * as fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { AndroidSdkPackagesParameter } from './android-sdk-packages-parameter.js'; +import { exampleAndroidCliBasic, exampleAndroidCliFullSetup } from './examples.js'; + +export const schema = z + .object({ + sdkPath: z + .string() + .describe( + 'Path to the Android SDK directory. Written to ~/.androidrc as --sdk=. Defaults to the android CLI default location.' + ) + .optional(), + packages: z + .array(z.string()) + .describe( + 'Android SDK packages to install. Examples: "platforms/android-35", "build-tools/35.0.0", "platform-tools", "cmdline-tools/latest", "system-images/android-35/google_apis_playstore/x86_64".' + ) + .optional(), + }) + .describe('Android CLI — installs the android command-line tool and manages the Android SDK environment'); + +export type AndroidCliConfig = z.infer; + +const ANDROIDRC_PATH = path.join(os.homedir(), '.androidrc'); + +const defaultConfig: Partial = { + packages: [], +}; + +export class AndroidCliResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'android-cli', + defaultConfig, + exampleConfigs: { + example1: exampleAndroidCliBasic, + example2: exampleAndroidCliFullSetup, + }, + operatingSystems: [OS.Darwin, OS.Linux], + schema, + parameterSettings: { + sdkPath: { type: 'directory', canModify: true }, + packages: { type: 'stateful', definition: new AndroidSdkPackagesParameter() }, + }, + }; + } + + async refresh(params: Partial): Promise | null> { + const $ = getPty(); + + const { status } = await $.spawnSafe('which android'); + if (status === SpawnStatus.ERROR) return null; + + const result: Partial = {}; + + if (params.sdkPath) { + try { + const rcContent = await fs.readFile(ANDROIDRC_PATH, 'utf8'); + const sdkLine = rcContent.split('\n').find((l) => l.startsWith('--sdk=')); + if (!sdkLine) return null; + result.sdkPath = sdkLine.replace('--sdk=', '').trim(); + } catch { + return null; + } + } + + return result; + } + + async create(plan: CreatePlan): Promise { + const $ = getPty(); + + if (Utils.isMacOS()) { + await $.spawnSafe('brew tap android/tap', { + env: { HOMEBREW_NO_AUTO_UPDATE: '1', HOMEBREW_NO_ASK: '1', NONINTERACTIVE: '1' }, + }); + await Utils.installViaPkgMgr('android-cli', undefined, PackageManager.BREW); + } else { + if (await Utils.isArmArch()) { + throw new Error( + 'Android CLI does not support Linux ARM64. Only AMD64/x86_64 is supported on Linux.' + ); + } + await $.spawn( + 'curl -fsSL https://dl.google.com/android/cli/latest/linux_x86_64/install.sh | bash', + { interactive: true } + ); + } + + if (plan.desiredConfig.sdkPath) { + await this.setSdkPath(plan.desiredConfig.sdkPath); + } + } + + async modify(pc: ParameterChange, _plan: ModifyPlan): Promise { + if (pc.name === 'sdkPath') { + if (pc.newValue) { + await this.setSdkPath(pc.newValue as string); + } else { + await this.removeSdkPath(); + } + } + } + + async destroy(plan: DestroyPlan): Promise { + if (Utils.isMacOS()) { + await Utils.uninstallViaPkgMgr('android-cli', undefined, PackageManager.BREW); + } else { + const androidBinPath = path.join(os.homedir(), '.local', 'bin', 'android'); + await fs.rm(androidBinPath, { force: true }); + } + + if (plan.currentConfig.sdkPath) { + await this.removeSdkPath(); + } + } + + private async setSdkPath(sdkPath: string): Promise { + let rcContent = ''; + try { + rcContent = await fs.readFile(ANDROIDRC_PATH, 'utf8'); + } catch { /* file doesn't exist yet */ } + + const lines = rcContent.split('\n').filter(Boolean); + const sdkIndex = lines.findIndex((l) => l.startsWith('--sdk=')); + + if (sdkIndex >= 0) { + lines[sdkIndex] = `--sdk=${sdkPath}`; + } else { + lines.push(`--sdk=${sdkPath}`); + } + + await fs.writeFile(ANDROIDRC_PATH, lines.join('\n') + '\n', 'utf8'); + } + + private async removeSdkPath(): Promise { + try { + const rcContent = await fs.readFile(ANDROIDRC_PATH, 'utf8'); + const remaining = rcContent.split('\n').filter((l) => !l.startsWith('--sdk=')); + + if (remaining.filter(Boolean).length === 0) { + await fs.rm(ANDROIDRC_PATH, { force: true }); + } else { + await fs.writeFile(ANDROIDRC_PATH, remaining.join('\n') + '\n', 'utf8'); + } + } catch { /* file doesn't exist, nothing to do */ } + } +} diff --git a/src/resources/android/android-cli/android-emulator.ts b/src/resources/android/android-cli/android-emulator.ts new file mode 100644 index 00000000..15e642c1 --- /dev/null +++ b/src/resources/android/android-cli/android-emulator.ts @@ -0,0 +1,116 @@ +import { + CreatePlan, + DestroyPlan, + Resource, + ResourceSettings, + SpawnStatus, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import * as fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { exampleAndroidEmulatorBasic, exampleAndroidEmulatorPixel } from './examples.js'; + +export const schema = z + .object({ + profile: z + .string() + .describe( + 'Android hardware profile for the emulator (e.g. "medium_phone", "pixel_9"). Run `android emulator create --list-profiles` to see all available profiles.' + ), + name: z + .string() + .describe( + 'Custom name for the Android Virtual Device. Defaults to the profile name if not specified.' + ) + .optional(), + }) + .describe('Create and manage an Android Virtual Device (AVD) using the android CLI'); + +export type AndroidEmulatorConfig = z.infer; + +const AVD_DIR = path.join(os.homedir(), '.android', 'avd'); + +const defaultConfig: Partial = { + profile: '', +}; + +export class AndroidEmulatorResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'android-emulator', + defaultConfig, + exampleConfigs: { + example1: exampleAndroidEmulatorBasic, + example2: exampleAndroidEmulatorPixel, + }, + operatingSystems: [OS.Darwin, OS.Linux], + schema, + dependencies: ['android-cli'], + parameterSettings: { + profile: {}, + name: { canModify: false }, + }, + allowMultiple: { + identifyingParameters: ['profile', 'name'], + }, + }; + } + + async refresh(params: Partial): Promise | null> { + const $ = getPty(); + + const { status, data } = await $.spawnSafe('android emulator list', { interactive: true }); + if (status === SpawnStatus.ERROR) return null; + + const avdName = this.resolveAvdName(params); + if (!avdName) return null; + + const lines = data.split('\n').map((l) => l.trim()).filter(Boolean); + const found = lines.some( + (l) => l.toLowerCase() === avdName.toLowerCase() || l.toLowerCase().startsWith(avdName.toLowerCase() + ' ') + ); + + if (!found) return null; + + return { + profile: params.profile, + ...(params.name ? { name: params.name } : {}), + }; + } + + async create(plan: CreatePlan): Promise { + const $ = getPty(); + const { profile, name } = plan.desiredConfig; + + let cmd = `android emulator create --profile="${profile}"`; + if (name) { + // The android CLI may support --name in future releases; include it if provided. + cmd += ` --name="${name}"`; + } + + await $.spawn(cmd, { interactive: true }); + } + + async destroy(plan: DestroyPlan): Promise { + const $ = getPty(); + const avdName = this.resolveAvdName(plan.currentConfig); + if (!avdName) return; + + // Try avdmanager first (available when cmdline-tools is installed) + const { status } = await $.spawnSafe(`avdmanager delete avd -n "${avdName}"`, { interactive: true }); + + if (status === SpawnStatus.ERROR) { + // Fallback: remove AVD files directly + await fs.rm(path.join(AVD_DIR, `${avdName}.avd`), { recursive: true, force: true }); + await fs.rm(path.join(AVD_DIR, `${avdName}.ini`), { force: true }); + } + } + + private resolveAvdName(params: Partial): string | undefined { + return params.name ?? params.profile; + } +} diff --git a/src/resources/android/android-cli/android-sdk-packages-parameter.ts b/src/resources/android/android-cli/android-sdk-packages-parameter.ts new file mode 100644 index 00000000..15574018 --- /dev/null +++ b/src/resources/android/android-cli/android-sdk-packages-parameter.ts @@ -0,0 +1,27 @@ +import { ArrayStatefulParameter, getPty, SpawnStatus } from '@codifycli/plugin-core'; + +import { AndroidCliConfig } from './android-cli.js'; + +export class AndroidSdkPackagesParameter extends ArrayStatefulParameter { + async refresh(_desired: string[] | null): Promise { + const $ = getPty(); + + const { status, data } = await $.spawnSafe('android sdk list'); + if (status === SpawnStatus.ERROR) return null; + + return data + .split('\n') + .map((l) => l.trim()) + .filter((l) => l && !l.startsWith('-') && !l.startsWith('Path') && !l.startsWith('Installed') && !l.includes('|')); + } + + async addItem(item: string): Promise { + const $ = getPty(); + await $.spawn(`android sdk install "${item}"`, { interactive: true }); + } + + async removeItem(item: string): Promise { + const $ = getPty(); + await $.spawn(`android sdk remove "${item}"`, { interactive: true }); + } +} diff --git a/src/resources/android/android-cli/completions/android-cli.packages.ts b/src/resources/android/android-cli/completions/android-cli.packages.ts new file mode 100644 index 00000000..d997e902 --- /dev/null +++ b/src/resources/android/android-cli/completions/android-cli.packages.ts @@ -0,0 +1,22 @@ +export default async function loadAndroidSdkPackages(): Promise { + const response = await fetch('https://dl.google.com/android/repository/repository2-3.xml', { + headers: { 'User-Agent': 'codify-completions-cron' }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch Android SDK repository: ${response.status} ${response.statusText}`); + } + + const xml = await response.text(); + + // Extract path attributes from package elements and convert ; separators to / + const paths = new Set(); + const regex = /\bpath="([^"]+)"/g; + let match: RegExpExecArray | null; + while ((match = regex.exec(xml)) !== null) { + // Convert legacy semicolon path separators to the android CLI forward-slash format + paths.add(match[1].replace(/;/g, '/')); + } + + return [...paths].sort(); +} diff --git a/src/resources/android/android-cli/completions/android-emulator.profile.ts b/src/resources/android/android-cli/completions/android-emulator.profile.ts new file mode 100644 index 00000000..c36ad06a --- /dev/null +++ b/src/resources/android/android-cli/completions/android-emulator.profile.ts @@ -0,0 +1,54 @@ +// Known Android hardware profiles from the AVD Manager device definitions. +// These correspond to profiles accepted by `android emulator create --profile=`. +export default async function loadAndroidEmulatorProfiles(): Promise { + return [ + // Generic form factors + 'medium_phone', + 'small_phone', + 'foldable', + 'medium_tablet', + 'resizable', + 'desktop_medium', + + // Pixel phones + 'pixel_9', + 'pixel_9_pro', + 'pixel_9_pro_xl', + 'pixel_9_pro_fold', + 'pixel_8', + 'pixel_8_pro', + 'pixel_7', + 'pixel_7_pro', + 'pixel_7a', + 'pixel_6', + 'pixel_6_pro', + 'pixel_6a', + 'pixel_5', + 'pixel_4', + 'pixel_4_xl', + 'pixel_4a', + 'pixel_3', + 'pixel_3_xl', + 'pixel_3a', + 'pixel_3a_xl', + + // Pixel tablets / foldables + 'pixel_tablet', + 'pixel_fold', + + // Wear OS + 'wear_os_large_round', + 'wear_os_small_round', + 'wear_os_square', + 'wear_os_rect', + + // Android TV + 'tv_1080p', + 'tv_720p', + 'tv_4k', + + // Automotive + 'automotive_1024p_landscape', + 'automotive_portrait', + ]; +} diff --git a/src/resources/android/android-cli/examples.ts b/src/resources/android/android-cli/examples.ts new file mode 100644 index 00000000..fbc74874 --- /dev/null +++ b/src/resources/android/android-cli/examples.ts @@ -0,0 +1,64 @@ +import { ExampleConfig } from '@codifycli/plugin-core'; + +export const exampleAndroidCliBasic: ExampleConfig = { + title: 'Android development environment', + description: 'Install the Android CLI with essential SDK packages for building Android apps — platform, build tools, and ADB.', + configs: [ + { + type: 'android-cli', + packages: ['cmdline-tools/latest', 'platform-tools', 'platforms/android-35', 'build-tools/35.0.0'], + }, + ], +}; + +export const exampleAndroidCliFullSetup: ExampleConfig = { + title: 'Android environment with emulator', + description: 'Install Android CLI with SDK packages and provision a Pixel 9 emulator for local development and testing.', + configs: [ + { + type: 'android-cli', + packages: [ + 'cmdline-tools/latest', + 'platform-tools', + 'platforms/android-35', + 'build-tools/35.0.0', + 'system-images/android-35/google_apis_playstore/x86_64', + ], + }, + { + type: 'android-emulator', + profile: 'pixel_9', + }, + ], +}; + +export const exampleAndroidEmulatorBasic: ExampleConfig = { + title: 'Medium phone emulator', + description: 'Create a standard medium phone AVD — the default Android emulator profile, good for general app testing.', + configs: [ + { + type: 'android-cli', + packages: [ + 'cmdline-tools/latest', + 'platform-tools', + 'platforms/android-35', + 'system-images/android-35/google_apis_playstore/x86_64', + ], + }, + { + type: 'android-emulator', + profile: 'medium_phone', + }, + ], +}; + +export const exampleAndroidEmulatorPixel: ExampleConfig = { + title: 'Pixel 9 emulator', + description: 'Provision a Pixel 9 emulator matching current flagship hardware for testing on the latest Android profile.', + configs: [ + { + type: 'android-emulator', + profile: 'pixel_9', + }, + ], +}; diff --git a/test/android/android-cli.test.ts b/test/android/android-cli.test.ts new file mode 100644 index 00000000..c3bdef98 --- /dev/null +++ b/test/android/android-cli.test.ts @@ -0,0 +1,71 @@ +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import { SpawnStatus } from '@codifycli/schemas'; +import * as path from 'node:path'; +import { beforeAll, describe, expect, it } from 'vitest'; + +describe('Android CLI integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + beforeAll(async () => { + const result = await testSpawn('which android'); + if (result.status === SpawnStatus.SUCCESS) { + await PluginTester.uninstall(pluginPath, [{ type: 'android-cli' }]); + } + }, 120_000); + + it('Can install and uninstall Android CLI', { timeout: 300_000 }, async () => { + await PluginTester.fullTest( + pluginPath, + [{ type: 'android-cli' }], + { + validateApply: async () => { + const result = await testSpawn('which android'); + expect(result.status).toBe(SpawnStatus.SUCCESS); + }, + validateDestroy: async () => { + const result = await testSpawn('which android'); + expect(result.status).toBe(SpawnStatus.ERROR); + }, + } + ); + }); + + it('Can install Android CLI with SDK packages', { timeout: 600_000 }, async () => { + await PluginTester.fullTest( + pluginPath, + [ + { + type: 'android-cli', + packages: ['cmdline-tools/latest', 'platform-tools'], + }, + ], + { + validateApply: async () => { + const which = await testSpawn('which android'); + expect(which.status).toBe(SpawnStatus.SUCCESS); + + const list = await testSpawn('android sdk list'); + expect(list.status).toBe(SpawnStatus.SUCCESS); + expect(list.data).toContain('platform-tools'); + }, + testModify: { + modifiedConfigs: [ + { + type: 'android-cli', + packages: ['platform-tools'], + }, + ], + validateModify: async () => { + const list = await testSpawn('android sdk list'); + expect(list.status).toBe(SpawnStatus.SUCCESS); + expect(list.data).toContain('platform-tools'); + }, + }, + validateDestroy: async () => { + const result = await testSpawn('which android'); + expect(result.status).toBe(SpawnStatus.ERROR); + }, + } + ); + }); +}); From e85211d67dc77d2fb0d6dce5633dcbea50a02ae1 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 30 Jun 2026 11:20:05 -0400 Subject: [PATCH 2/8] fix: macOS install and tests. Moved android studios to it's own folder --- src/index.ts | 2 +- .../android/android-cli/android-cli.ts | 28 ++++----- .../android/android-cli/android-emulator.ts | 36 ++--------- .../android-sdk-packages-parameter.ts | 5 +- src/resources/android/android-cli/examples.ts | 6 +- .../android/{ => android-studios}/README.md | 0 .../{ => android-studios}/android-studio.ts | 2 +- .../android/{ => android-studios}/types.ts | 0 test/android/android-cli.test.ts | 59 ++++++++++++++----- 9 files changed, 72 insertions(+), 66 deletions(-) rename src/resources/android/{ => android-studios}/README.md (100%) rename src/resources/android/{ => android-studios}/android-studio.ts (99%) rename src/resources/android/{ => android-studios}/types.ts (100%) diff --git a/src/index.ts b/src/index.ts index 57787256..1d5c6b9b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ import { Plugin, runPlugin } from '@codifycli/plugin-core'; import { AndroidCliResource } from './resources/android/android-cli/android-cli.js'; import { AndroidEmulatorResource } from './resources/android/android-cli/android-emulator.js'; -import { AndroidStudioResource } from './resources/android/android-studio.js'; import { AptResource } from './resources/apt/apt.js'; import { AsdfResource } from './resources/asdf/asdf.js'; import { AsdfInstallResource } from './resources/asdf/asdf-install.js'; @@ -76,6 +75,7 @@ import { PhpStormResource } from './resources/jetbrains/phpstorm/phpstorm.js'; import { GoLandResource } from './resources/jetbrains/goland/goland.js'; import { RiderResource } from './resources/jetbrains/rider/rider.js'; import { RubyMineResource } from './resources/jetbrains/rubymine/rubymine.js'; +import {AndroidStudioResource} from "./resources/android/android-studios/android-studio.js"; export const MIN_SUPPORTED_CLI_VERSION: string | undefined = '1.1.0'; diff --git a/src/resources/android/android-cli/android-cli.ts b/src/resources/android/android-cli/android-cli.ts index 4fc38406..fef992b7 100644 --- a/src/resources/android/android-cli/android-cli.ts +++ b/src/resources/android/android-cli/android-cli.ts @@ -2,7 +2,6 @@ import { CreatePlan, DestroyPlan, ModifyPlan, - PackageManager, ParameterChange, Resource, ResourceSettings, @@ -27,7 +26,7 @@ export const schema = z 'Path to the Android SDK directory. Written to ~/.androidrc as --sdk=. Defaults to the android CLI default location.' ) .optional(), - packages: z + sdkPackages: z .array(z.string()) .describe( 'Android SDK packages to install. Examples: "platforms/android-35", "build-tools/35.0.0", "platform-tools", "cmdline-tools/latest", "system-images/android-35/google_apis_playstore/x86_64".' @@ -41,7 +40,7 @@ export type AndroidCliConfig = z.infer; const ANDROIDRC_PATH = path.join(os.homedir(), '.androidrc'); const defaultConfig: Partial = { - packages: [], + sdkPackages: [], }; export class AndroidCliResource extends Resource { @@ -57,7 +56,7 @@ export class AndroidCliResource extends Resource { schema, parameterSettings: { sdkPath: { type: 'directory', canModify: true }, - packages: { type: 'stateful', definition: new AndroidSdkPackagesParameter() }, + sdkPackages: { type: 'stateful', definition: new AndroidSdkPackagesParameter() }, }, }; } @@ -87,13 +86,16 @@ export class AndroidCliResource extends Resource { async create(plan: CreatePlan): Promise { const $ = getPty(); + const isArm = await Utils.isArmArch(); + if (Utils.isMacOS()) { - await $.spawnSafe('brew tap android/tap', { - env: { HOMEBREW_NO_AUTO_UPDATE: '1', HOMEBREW_NO_ASK: '1', NONINTERACTIVE: '1' }, - }); - await Utils.installViaPkgMgr('android-cli', undefined, PackageManager.BREW); + const arch = isArm ? 'darwin_arm64' : 'darwin_x86_64'; + await $.spawn( + `curl -fsSL https://dl.google.com/android/cli/latest/${arch}/install.sh | bash`, + { interactive: true } + ); } else { - if (await Utils.isArmArch()) { + if (isArm) { throw new Error( 'Android CLI does not support Linux ARM64. Only AMD64/x86_64 is supported on Linux.' ); @@ -120,12 +122,8 @@ export class AndroidCliResource extends Resource { } async destroy(plan: DestroyPlan): Promise { - if (Utils.isMacOS()) { - await Utils.uninstallViaPkgMgr('android-cli', undefined, PackageManager.BREW); - } else { - const androidBinPath = path.join(os.homedir(), '.local', 'bin', 'android'); - await fs.rm(androidBinPath, { force: true }); - } + const androidBinPath = path.join(os.homedir(), '.local', 'bin', 'android'); + await fs.rm(androidBinPath, { force: true }); if (plan.currentConfig.sdkPath) { await this.removeSdkPath(); diff --git a/src/resources/android/android-cli/android-emulator.ts b/src/resources/android/android-cli/android-emulator.ts index 15e642c1..85e6d893 100644 --- a/src/resources/android/android-cli/android-emulator.ts +++ b/src/resources/android/android-cli/android-emulator.ts @@ -19,14 +19,8 @@ export const schema = z profile: z .string() .describe( - 'Android hardware profile for the emulator (e.g. "medium_phone", "pixel_9"). Run `android emulator create --list-profiles` to see all available profiles.' + 'Android hardware profile for the emulator (e.g. "medium_phone", "pixel_9"). Run `android emulator create --list-profiles` to see all available profiles. The profile name is also used as the AVD name.' ), - name: z - .string() - .describe( - 'Custom name for the Android Virtual Device. Defaults to the profile name if not specified.' - ) - .optional(), }) .describe('Create and manage an Android Virtual Device (AVD) using the android CLI'); @@ -52,10 +46,9 @@ export class AndroidEmulatorResource extends Resource { dependencies: ['android-cli'], parameterSettings: { profile: {}, - name: { canModify: false }, }, allowMultiple: { - identifyingParameters: ['profile', 'name'], + identifyingParameters: ['profile'], }, }; } @@ -66,7 +59,7 @@ export class AndroidEmulatorResource extends Resource { const { status, data } = await $.spawnSafe('android emulator list', { interactive: true }); if (status === SpawnStatus.ERROR) return null; - const avdName = this.resolveAvdName(params); + const avdName = params.profile; if (!avdName) return null; const lines = data.split('\n').map((l) => l.trim()).filter(Boolean); @@ -76,41 +69,24 @@ export class AndroidEmulatorResource extends Resource { if (!found) return null; - return { - profile: params.profile, - ...(params.name ? { name: params.name } : {}), - }; + return { profile: params.profile }; } async create(plan: CreatePlan): Promise { const $ = getPty(); - const { profile, name } = plan.desiredConfig; - - let cmd = `android emulator create --profile="${profile}"`; - if (name) { - // The android CLI may support --name in future releases; include it if provided. - cmd += ` --name="${name}"`; - } - - await $.spawn(cmd, { interactive: true }); + await $.spawn(`android emulator create "${plan.desiredConfig.profile}"`, { interactive: true }); } async destroy(plan: DestroyPlan): Promise { const $ = getPty(); - const avdName = this.resolveAvdName(plan.currentConfig); + const avdName = plan.currentConfig.profile; if (!avdName) return; - // Try avdmanager first (available when cmdline-tools is installed) const { status } = await $.spawnSafe(`avdmanager delete avd -n "${avdName}"`, { interactive: true }); if (status === SpawnStatus.ERROR) { - // Fallback: remove AVD files directly await fs.rm(path.join(AVD_DIR, `${avdName}.avd`), { recursive: true, force: true }); await fs.rm(path.join(AVD_DIR, `${avdName}.ini`), { force: true }); } } - - private resolveAvdName(params: Partial): string | undefined { - return params.name ?? params.profile; - } } diff --git a/src/resources/android/android-cli/android-sdk-packages-parameter.ts b/src/resources/android/android-cli/android-sdk-packages-parameter.ts index 15574018..5c96f28c 100644 --- a/src/resources/android/android-cli/android-sdk-packages-parameter.ts +++ b/src/resources/android/android-cli/android-sdk-packages-parameter.ts @@ -11,8 +11,9 @@ export class AndroidSdkPackagesParameter extends ArrayStatefulParameter l.trim()) - .filter((l) => l && !l.startsWith('-') && !l.startsWith('Path') && !l.startsWith('Installed') && !l.includes('|')); + .filter((l) => l.match(/^\s{2}\S/)) + .map((l) => l.trim().split(/\s+/)[0]) + .filter(Boolean); } async addItem(item: string): Promise { diff --git a/src/resources/android/android-cli/examples.ts b/src/resources/android/android-cli/examples.ts index fbc74874..8b0a83c9 100644 --- a/src/resources/android/android-cli/examples.ts +++ b/src/resources/android/android-cli/examples.ts @@ -6,7 +6,7 @@ export const exampleAndroidCliBasic: ExampleConfig = { configs: [ { type: 'android-cli', - packages: ['cmdline-tools/latest', 'platform-tools', 'platforms/android-35', 'build-tools/35.0.0'], + sdkPackages:['cmdline-tools/latest', 'platform-tools', 'platforms/android-35', 'build-tools/35.0.0'], }, ], }; @@ -17,7 +17,7 @@ export const exampleAndroidCliFullSetup: ExampleConfig = { configs: [ { type: 'android-cli', - packages: [ + sdkPackages:[ 'cmdline-tools/latest', 'platform-tools', 'platforms/android-35', @@ -38,7 +38,7 @@ export const exampleAndroidEmulatorBasic: ExampleConfig = { configs: [ { type: 'android-cli', - packages: [ + sdkPackages:[ 'cmdline-tools/latest', 'platform-tools', 'platforms/android-35', diff --git a/src/resources/android/README.md b/src/resources/android/android-studios/README.md similarity index 100% rename from src/resources/android/README.md rename to src/resources/android/android-studios/README.md diff --git a/src/resources/android/android-studio.ts b/src/resources/android/android-studios/android-studio.ts similarity index 99% rename from src/resources/android/android-studio.ts rename to src/resources/android/android-studios/android-studio.ts index 4e488622..f9cbd8c2 100644 --- a/src/resources/android/android-studio.ts +++ b/src/resources/android/android-studios/android-studio.ts @@ -5,7 +5,7 @@ import os from 'node:os'; import path from 'node:path'; import plist from 'plist'; -import { Utils as LocalUtils } from '../../utils/index.js'; +import { Utils as LocalUtils } from '../../../utils/index.js'; import { AndroidStudioPlist, AndroidStudioVersionData } from './types.js'; export const schema = z.object({ diff --git a/src/resources/android/types.ts b/src/resources/android/android-studios/types.ts similarity index 100% rename from src/resources/android/types.ts rename to src/resources/android/android-studios/types.ts diff --git a/test/android/android-cli.test.ts b/test/android/android-cli.test.ts index c3bdef98..71b989fe 100644 --- a/test/android/android-cli.test.ts +++ b/test/android/android-cli.test.ts @@ -36,7 +36,7 @@ describe('Android CLI integration tests', async () => { [ { type: 'android-cli', - packages: ['cmdline-tools/latest', 'platform-tools'], + sdkPackages:['cmdline-tools/latest', 'platform-tools'], }, ], { @@ -48,19 +48,6 @@ describe('Android CLI integration tests', async () => { expect(list.status).toBe(SpawnStatus.SUCCESS); expect(list.data).toContain('platform-tools'); }, - testModify: { - modifiedConfigs: [ - { - type: 'android-cli', - packages: ['platform-tools'], - }, - ], - validateModify: async () => { - const list = await testSpawn('android sdk list'); - expect(list.status).toBe(SpawnStatus.SUCCESS); - expect(list.data).toContain('platform-tools'); - }, - }, validateDestroy: async () => { const result = await testSpawn('which android'); expect(result.status).toBe(SpawnStatus.ERROR); @@ -69,3 +56,47 @@ describe('Android CLI integration tests', async () => { ); }); }); + +describe('Android Emulator integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + beforeAll(async () => { + const result = await testSpawn('which android'); + if (result.status === SpawnStatus.SUCCESS) { + await PluginTester.uninstall(pluginPath, [{ type: 'android-cli' }]); + } + }, 120_000); + + it('Can create and destroy an Android emulator', { timeout: 900_000 }, async () => { + await PluginTester.fullTest( + pluginPath, + [ + { + type: 'android-cli', + sdkPackages:[ + 'cmdline-tools/latest', + 'platform-tools', + 'platforms/android-35', + 'system-images/android-35/google_apis_playstore/x86_64', + ], + }, + { + type: 'android-emulator', + profile: 'medium_phone', + }, + ], + { + validateApply: async () => { + const list = await testSpawn('android emulator list'); + expect(list.status).toBe(SpawnStatus.SUCCESS); + expect(list.data).toContain('medium_phone'); + }, + validateDestroy: async () => { + const list = await testSpawn('android emulator list'); + expect(list.data).not.toContain('medium_phone'); + }, + } + ); + }); + +}); From c8d2a48661f07a534689f8048b37678f3153de35 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 30 Jun 2026 11:23:47 -0400 Subject: [PATCH 3/8] feat: add android studio version completions --- .../src/__generated__/completions-index.ts | 6 ++++++ .../android/android-studios/android-studio.ts | 19 ++++++++++++------- .../completions/android-studio.version.ts | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 src/resources/android/android-studios/completions/android-studio.version.ts diff --git a/completions-cron/src/__generated__/completions-index.ts b/completions-cron/src/__generated__/completions-index.ts index bcf804c5..db060603 100644 --- a/completions-cron/src/__generated__/completions-index.ts +++ b/completions-cron/src/__generated__/completions-index.ts @@ -27,6 +27,9 @@ import mod22 from '../../../src/resources/cursor/completions/cursor.extensions.j import mod23 from '../../../src/resources/asdf/completions/asdf.plugins.js'; import mod24 from '../../../src/resources/asdf/completions/asdf-plugin.plugin.js'; import mod25 from '../../../src/resources/apt/completions/apt.install.js'; +import mod26 from '../../../src/resources/android/android-studios/completions/android-studio.version.js'; +import mod27 from '../../../src/resources/android/android-cli/completions/android-emulator.profile.js'; +import mod28 from '../../../src/resources/android/android-cli/completions/android-cli.packages.js'; export interface CompletionModule { resourceType: string @@ -61,4 +64,7 @@ export const completionModules: CompletionModule[] = [ { resourceType: 'asdf', parameterPath: '/plugins', fetch: mod23 }, { resourceType: 'asdf-plugin', parameterPath: '/plugin', fetch: mod24 }, { resourceType: 'apt', parameterPath: '/install', fetch: mod25 }, + { resourceType: 'android-studio', parameterPath: '/version', fetch: mod26 }, + { resourceType: 'android-emulator', parameterPath: '/profile', fetch: mod27 }, + { resourceType: 'android-cli', parameterPath: '/packages', fetch: mod28 }, ] diff --git a/src/resources/android/android-studios/android-studio.ts b/src/resources/android/android-studios/android-studio.ts index f9cbd8c2..dd7d016c 100644 --- a/src/resources/android/android-studios/android-studio.ts +++ b/src/resources/android/android-studios/android-studio.ts @@ -123,7 +123,7 @@ export class AndroidStudioResource extends Resource { return { directory, - version: installedVersion, + ...(installedVersion ? { version: installedVersion } : {}), }; } @@ -250,12 +250,17 @@ export class AndroidStudioResource extends Resource { || parameters.version === plist.CFBundleShortVersionString ) - return matched.length > 0 - ? { - directory: path.dirname(matched[0].location), - version: matched[0].webInfo?.version ?? matched[0].plist.CFBundleShortVersionString - } - : null; + if (matched.length === 0) return null; + + const best = matched[0]; + const version = best.webInfo?.version + ?? this.allAndroidStudioVersions?.find((v) => v.build === best.plist.CFBundleVersion)?.version + ?? best.plist.CFBundleShortVersionString; + + return { + directory: path.dirname(best.location), + version, + }; } private getVersionData( diff --git a/src/resources/android/android-studios/completions/android-studio.version.ts b/src/resources/android/android-studios/completions/android-studio.version.ts new file mode 100644 index 00000000..a1a5c78d --- /dev/null +++ b/src/resources/android/android-studios/completions/android-studio.version.ts @@ -0,0 +1,15 @@ +import { AndroidStudioVersionData } from '../types.js'; + +export default async function loadAndroidStudioVersions(): Promise { + const response = await fetch('https://jb.gg/android-studio-releases-list.json'); + + if (!response.ok) { + throw new Error(`Failed to fetch Android Studio releases: ${response.status} ${await response.text()}`); + } + + const data = await response.json() as { content: { item: AndroidStudioVersionData[] } }; + + return data.content.item + .filter((item) => item.channel === 'Release') + .map((item) => item.version); +} From 8628340051f75fed1697b04fbc3d51008a2a714c Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 30 Jun 2026 11:41:28 -0400 Subject: [PATCH 4/8] fix: skip tests for linux arm (unsupported) --- test/android/android-cli.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/android/android-cli.test.ts b/test/android/android-cli.test.ts index 71b989fe..9ed92296 100644 --- a/test/android/android-cli.test.ts +++ b/test/android/android-cli.test.ts @@ -1,9 +1,12 @@ import { PluginTester, testSpawn } from '@codifycli/plugin-test'; import { SpawnStatus } from '@codifycli/schemas'; import * as path from 'node:path'; +import * as os from 'node:os'; import { beforeAll, describe, expect, it } from 'vitest'; -describe('Android CLI integration tests', async () => { +const isLinuxArm = os.platform() === 'linux' && os.arch() === 'arm64'; + +describe.skipIf(isLinuxArm)('Android CLI integration tests', async () => { const pluginPath = path.resolve('./src/index.ts'); beforeAll(async () => { @@ -57,7 +60,7 @@ describe('Android CLI integration tests', async () => { }); }); -describe('Android Emulator integration tests', async () => { +describe.skipIf(isLinuxArm)('Android Emulator integration tests', async () => { const pluginPath = path.resolve('./src/index.ts'); beforeAll(async () => { From 63f8790f518d9e3b8ae0b8de4492561e9f0f4f49 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 30 Jun 2026 12:26:30 -0400 Subject: [PATCH 5/8] feat: made android cli emulator a stateful parameter instead. Improved docs for android --- .../src/__generated__/completions-index.ts | 8 +- .../android-development/android-cli.mdx | 72 +++++++++++++++ .../android-studio.mdx | 0 .../(resources)/android-development/meta.json | 4 + .../(resources)/editors-ides/android-cli.mdx | 90 ------------------ .../(resources)/editors-ides/meta.json | 5 +- docs/resources/(resources)/meta.json | 1 + docs/resources/index.mdx | 1 + src/index.ts | 2 - .../android/android-cli/android-cli.ts | 12 ++- .../android/android-cli/android-emulator.ts | 92 ------------------- .../android-emulators-parameter.ts | 27 ++++++ ...or.profile.ts => android-cli.emulators.ts} | 0 ...packages.ts => android-cli.sdkPackages.ts} | 0 src/resources/android/android-cli/examples.ts | 40 +------- test/android/android-cli.test.ts | 7 +- 16 files changed, 129 insertions(+), 232 deletions(-) create mode 100644 docs/resources/(resources)/android-development/android-cli.mdx rename docs/resources/(resources)/{editors-ides => android-development}/android-studio.mdx (100%) create mode 100644 docs/resources/(resources)/android-development/meta.json delete mode 100644 docs/resources/(resources)/editors-ides/android-cli.mdx delete mode 100644 src/resources/android/android-cli/android-emulator.ts create mode 100644 src/resources/android/android-cli/android-emulators-parameter.ts rename src/resources/android/android-cli/completions/{android-emulator.profile.ts => android-cli.emulators.ts} (100%) rename src/resources/android/android-cli/completions/{android-cli.packages.ts => android-cli.sdkPackages.ts} (100%) diff --git a/completions-cron/src/__generated__/completions-index.ts b/completions-cron/src/__generated__/completions-index.ts index db060603..a3142781 100644 --- a/completions-cron/src/__generated__/completions-index.ts +++ b/completions-cron/src/__generated__/completions-index.ts @@ -28,8 +28,8 @@ import mod23 from '../../../src/resources/asdf/completions/asdf.plugins.js'; import mod24 from '../../../src/resources/asdf/completions/asdf-plugin.plugin.js'; import mod25 from '../../../src/resources/apt/completions/apt.install.js'; import mod26 from '../../../src/resources/android/android-studios/completions/android-studio.version.js'; -import mod27 from '../../../src/resources/android/android-cli/completions/android-emulator.profile.js'; -import mod28 from '../../../src/resources/android/android-cli/completions/android-cli.packages.js'; +import mod27 from '../../../src/resources/android/android-cli/completions/android-cli.sdkPackages.js'; +import mod28 from '../../../src/resources/android/android-cli/completions/android-cli.emulators.js'; export interface CompletionModule { resourceType: string @@ -65,6 +65,6 @@ export const completionModules: CompletionModule[] = [ { resourceType: 'asdf-plugin', parameterPath: '/plugin', fetch: mod24 }, { resourceType: 'apt', parameterPath: '/install', fetch: mod25 }, { resourceType: 'android-studio', parameterPath: '/version', fetch: mod26 }, - { resourceType: 'android-emulator', parameterPath: '/profile', fetch: mod27 }, - { resourceType: 'android-cli', parameterPath: '/packages', fetch: mod28 }, + { resourceType: 'android-cli', parameterPath: '/sdkPackages', fetch: mod27 }, + { resourceType: 'android-cli', parameterPath: '/emulators', fetch: mod28 }, ] diff --git a/docs/resources/(resources)/android-development/android-cli.mdx b/docs/resources/(resources)/android-development/android-cli.mdx new file mode 100644 index 00000000..aa5a43b0 --- /dev/null +++ b/docs/resources/(resources)/android-development/android-cli.mdx @@ -0,0 +1,72 @@ +--- +title: android-cli +description: Reference pages for the Android CLI resources +--- + +The `android-cli` resource installs and configures [Android CLI](https://developer.android.com/tools/agents/android-cli), Google's command-line tool for managing the Android development environment. It manages the CLI itself, SDK packages, and Android Virtual Devices (AVDs) in a single resource. + +On macOS, Android CLI is installed via the official curl script (ARM64 and x86_64 supported). On Linux, only AMD64/x86_64 is supported. + +## Parameters + +- **sdkPath**: *(string)* Path to the Android SDK directory. Written to `~/.androidrc` as `--sdk=`. Defaults to the android CLI's built-in default location if omitted. +- **sdkPackages**: *(string[])* Android SDK packages to install declaratively. Package paths use forward-slash notation (e.g. `platforms/android-35`, `build-tools/35.0.0`, `cmdline-tools/latest`, `platform-tools`, `system-images/android-35/google_apis_playstore/x86_64`). Run `android sdk list --all` to see all available identifiers. +- **emulators**: *(string[])* Android emulator profiles to create as AVDs. Each string is a hardware profile name (e.g. `medium_phone`, `pixel_9`). Emulators are always created after `sdkPackages` are installed. Run `android emulator create --list-profiles` to see available profiles. + +## Example usage + +Install the CLI with essential SDK packages: + +```json title="codify.jsonc" +[ + { + "type": "android-cli", + "sdkPackages": [ + "cmdline-tools/latest", + "platform-tools", + "platforms/android-35", + "build-tools/35.0.0" + ] + } +] +``` + +Full Android development environment with an emulator: + +```json title="codify.jsonc" +[ + { + "type": "android-cli", + "sdkPackages": [ + "cmdline-tools/latest", + "platform-tools", + "platforms/android-35", + "build-tools/35.0.0", + "system-images/android-35/google_apis_playstore/x86_64" + ], + "emulators": ["pixel_9"] + } +] +``` + +## Common emulator profiles + +| Profile | Description | +|---------|-------------| +| `medium_phone` | Generic medium phone (default) | +| `small_phone` | Generic small phone | +| `foldable` | Foldable form factor | +| `medium_tablet` | Generic medium tablet | +| `pixel_9` | Google Pixel 9 | +| `pixel_9_pro` | Google Pixel 9 Pro | +| `pixel_9_pro_fold` | Google Pixel 9 Pro Fold | +| `pixel_8` | Google Pixel 8 | +| `wear_os_large_round` | Wear OS round watch | +| `tv_1080p` | Android TV 1080p | +| `automotive_1024p_landscape` | Android Automotive | + +## Notes + +- Linux ARM64 is **not** supported. Only AMD64/x86_64 is supported on Linux. +- AVDs are removed using `android emulator remove`. +- Run `android info` to display the default SDK path in use. diff --git a/docs/resources/(resources)/editors-ides/android-studio.mdx b/docs/resources/(resources)/android-development/android-studio.mdx similarity index 100% rename from docs/resources/(resources)/editors-ides/android-studio.mdx rename to docs/resources/(resources)/android-development/android-studio.mdx diff --git a/docs/resources/(resources)/android-development/meta.json b/docs/resources/(resources)/android-development/meta.json new file mode 100644 index 00000000..7b0ed98d --- /dev/null +++ b/docs/resources/(resources)/android-development/meta.json @@ -0,0 +1,4 @@ +{ + "title": "android", + "pages": ["android-cli", "android-studio"] +} diff --git a/docs/resources/(resources)/editors-ides/android-cli.mdx b/docs/resources/(resources)/editors-ides/android-cli.mdx deleted file mode 100644 index 13b768af..00000000 --- a/docs/resources/(resources)/editors-ides/android-cli.mdx +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: android-cli -description: Reference pages for the Android CLI resources ---- - -The Android CLI resources install and configure [Android CLI](https://developer.android.com/tools/agents/android-cli), Google's command-line tool for managing the Android development environment. Two resources are provided: one for installing the CLI and managing SDK packages, and one for provisioning Android Virtual Devices (AVDs). - ---- - -## android-cli - -Installs the `android` command-line tool and manages Android SDK packages declaratively. On macOS, Android CLI is installed via Homebrew (`android/tap`). On Linux (AMD64 only), it is installed via the official curl script to `~/.local/bin`. - -### Parameters - -- **sdkPath**: *(string)* Path to the Android SDK directory. Written to `~/.androidrc` as `--sdk=`. If not specified, the android CLI uses its default SDK location. -- **packages**: *(string[])* Android SDK packages to install. Package paths use forward-slash notation matching the `android sdk install` command (e.g. `platforms/android-35`, `build-tools/35.0.0`, `cmdline-tools/latest`, `platform-tools`, `system-images/android-35/google_apis_playstore/x86_64`). - -### Example usage - -```json title="codify.jsonc" -[ - { - "type": "android-cli", - "packages": [ - "cmdline-tools/latest", - "platform-tools", - "platforms/android-35", - "build-tools/35.0.0" - ] - } -] -``` - -### Notes - -- Linux ARM64 is **not** supported by Android CLI. Only AMD64/x86_64 is supported on Linux. -- Run `android sdk list --all` to see all available package identifiers. -- Run `android info` to display the default SDK path in use. - ---- - -## android-emulator - -Creates and manages an Android Virtual Device (AVD) using `android emulator create`. Each emulator declaration is an independent resource, identified by its `profile` and optional `name`. - -Depends on `android-cli` being installed. - -> **Note:** There is no `android emulator delete` command. Destroy uses `avdmanager delete avd` (from the `cmdline-tools` package) if available, or falls back to removing AVD files directly from `~/.android/avd/`. - -### Parameters - -- **profile** *(required)*: *(string)* Android hardware profile for the emulator (e.g. `medium_phone`, `pixel_9`). Run `android emulator create --list-profiles` to see available profiles. -- **name**: *(string)* Custom name for the Android Virtual Device. Defaults to the profile name. - -### Example usage - -```json title="codify.jsonc" -[ - { - "type": "android-cli", - "packages": [ - "cmdline-tools/latest", - "platform-tools", - "platforms/android-35", - "system-images/android-35/google_apis_playstore/x86_64" - ] - }, - { - "type": "android-emulator", - "profile": "pixel_9" - } -] -``` - -### Common profiles - -| Profile | Description | -|---------|-------------| -| `medium_phone` | Generic medium phone (default) | -| `small_phone` | Generic small phone | -| `foldable` | Foldable form factor | -| `medium_tablet` | Generic medium tablet | -| `pixel_9` | Google Pixel 9 | -| `pixel_9_pro` | Google Pixel 9 Pro | -| `pixel_9_pro_fold` | Google Pixel 9 Pro Fold | -| `pixel_8` | Google Pixel 8 | -| `wear_os_large_round` | Wear OS round watch | -| `tv_1080p` | Android TV 1080p | -| `automotive_1024p_landscape` | Android Automotive | diff --git a/docs/resources/(resources)/editors-ides/meta.json b/docs/resources/(resources)/editors-ides/meta.json index 5ce119b6..2f542118 100644 --- a/docs/resources/(resources)/editors-ides/meta.json +++ b/docs/resources/(resources)/editors-ides/meta.json @@ -1,4 +1,7 @@ { "title": "editors & ides", - "pages": ["vscode", "cursor", "intellij-idea", "clion", "goland", "phpstorm", "pycharm", "rider", "rubymine", "rustrover", "webstorm", "android-studio"] + "pages": [ + "[android-studio](../android/android-studio)", + "..." + ] } diff --git a/docs/resources/(resources)/meta.json b/docs/resources/(resources)/meta.json index a6e0de73..da392709 100644 --- a/docs/resources/(resources)/meta.json +++ b/docs/resources/(resources)/meta.json @@ -3,6 +3,7 @@ "ai-agents", "package-managers", "editors-ides", + "android-development", "asdf", "git", "javascript", diff --git a/docs/resources/index.mdx b/docs/resources/index.mdx index a5db2c41..992160cc 100644 --- a/docs/resources/index.mdx +++ b/docs/resources/index.mdx @@ -94,6 +94,7 @@ Run AI models locally: Configure popular development environments: - **[vscode](/docs/resources/vscode)** - Visual Studio Code extensions and settings +- **[android-cli](/docs/resources/android-cli)** - Android CLI, SDK packages, and emulators - **[android-studio](/docs/resources/android-studio)** - Android Studio IDE - **[xcode-tools](/docs/resources/xcode-tools)** - Xcode Command Line Tools - **[pgcli](/docs/resources/pgcli)** - Postgres CLI with auto-completion diff --git a/src/index.ts b/src/index.ts index 1d5c6b9b..2e0ab5dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ import { Plugin, runPlugin } from '@codifycli/plugin-core'; import { AndroidCliResource } from './resources/android/android-cli/android-cli.js'; -import { AndroidEmulatorResource } from './resources/android/android-cli/android-emulator.js'; import { AptResource } from './resources/apt/apt.js'; import { AsdfResource } from './resources/asdf/asdf.js'; import { AsdfInstallResource } from './resources/asdf/asdf-install.js'; @@ -116,7 +115,6 @@ runPlugin(Plugin.create( new GitRepositoriesResource(), new AndroidStudioResource(), new AndroidCliResource(), - new AndroidEmulatorResource(), new AsdfResource(), new AsdfPluginResource(), new AsdfInstallResource(), diff --git a/src/resources/android/android-cli/android-cli.ts b/src/resources/android/android-cli/android-cli.ts index fef992b7..32df82f5 100644 --- a/src/resources/android/android-cli/android-cli.ts +++ b/src/resources/android/android-cli/android-cli.ts @@ -15,6 +15,7 @@ import * as fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; +import { AndroidEmulatorsParameter } from './android-emulators-parameter.js'; import { AndroidSdkPackagesParameter } from './android-sdk-packages-parameter.js'; import { exampleAndroidCliBasic, exampleAndroidCliFullSetup } from './examples.js'; @@ -32,6 +33,12 @@ export const schema = z 'Android SDK packages to install. Examples: "platforms/android-35", "build-tools/35.0.0", "platform-tools", "cmdline-tools/latest", "system-images/android-35/google_apis_playstore/x86_64".' ) .optional(), + emulators: z + .array(z.string()) + .describe( + 'Android emulator profiles to create as AVDs (e.g. "medium_phone", "pixel_9"). Run `android emulator create --list-profiles` to see available options.' + ) + .optional(), }) .describe('Android CLI — installs the android command-line tool and manages the Android SDK environment'); @@ -41,6 +48,7 @@ const ANDROIDRC_PATH = path.join(os.homedir(), '.androidrc'); const defaultConfig: Partial = { sdkPackages: [], + emulators: [], }; export class AndroidCliResource extends Resource { @@ -54,9 +62,11 @@ export class AndroidCliResource extends Resource { }, operatingSystems: [OS.Darwin, OS.Linux], schema, + removeStatefulParametersBeforeDestroy: true, parameterSettings: { sdkPath: { type: 'directory', canModify: true }, - sdkPackages: { type: 'stateful', definition: new AndroidSdkPackagesParameter() }, + sdkPackages: { type: 'stateful', definition: new AndroidSdkPackagesParameter(), order: 1 }, + emulators: { type: 'stateful', definition: new AndroidEmulatorsParameter(), order: 2 }, }, }; } diff --git a/src/resources/android/android-cli/android-emulator.ts b/src/resources/android/android-cli/android-emulator.ts deleted file mode 100644 index 85e6d893..00000000 --- a/src/resources/android/android-cli/android-emulator.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - CreatePlan, - DestroyPlan, - Resource, - ResourceSettings, - SpawnStatus, - getPty, - z, -} from '@codifycli/plugin-core'; -import { OS } from '@codifycli/schemas'; -import * as fs from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; - -import { exampleAndroidEmulatorBasic, exampleAndroidEmulatorPixel } from './examples.js'; - -export const schema = z - .object({ - profile: z - .string() - .describe( - 'Android hardware profile for the emulator (e.g. "medium_phone", "pixel_9"). Run `android emulator create --list-profiles` to see all available profiles. The profile name is also used as the AVD name.' - ), - }) - .describe('Create and manage an Android Virtual Device (AVD) using the android CLI'); - -export type AndroidEmulatorConfig = z.infer; - -const AVD_DIR = path.join(os.homedir(), '.android', 'avd'); - -const defaultConfig: Partial = { - profile: '', -}; - -export class AndroidEmulatorResource extends Resource { - getSettings(): ResourceSettings { - return { - id: 'android-emulator', - defaultConfig, - exampleConfigs: { - example1: exampleAndroidEmulatorBasic, - example2: exampleAndroidEmulatorPixel, - }, - operatingSystems: [OS.Darwin, OS.Linux], - schema, - dependencies: ['android-cli'], - parameterSettings: { - profile: {}, - }, - allowMultiple: { - identifyingParameters: ['profile'], - }, - }; - } - - async refresh(params: Partial): Promise | null> { - const $ = getPty(); - - const { status, data } = await $.spawnSafe('android emulator list', { interactive: true }); - if (status === SpawnStatus.ERROR) return null; - - const avdName = params.profile; - if (!avdName) return null; - - const lines = data.split('\n').map((l) => l.trim()).filter(Boolean); - const found = lines.some( - (l) => l.toLowerCase() === avdName.toLowerCase() || l.toLowerCase().startsWith(avdName.toLowerCase() + ' ') - ); - - if (!found) return null; - - return { profile: params.profile }; - } - - async create(plan: CreatePlan): Promise { - const $ = getPty(); - await $.spawn(`android emulator create "${plan.desiredConfig.profile}"`, { interactive: true }); - } - - async destroy(plan: DestroyPlan): Promise { - const $ = getPty(); - const avdName = plan.currentConfig.profile; - if (!avdName) return; - - const { status } = await $.spawnSafe(`avdmanager delete avd -n "${avdName}"`, { interactive: true }); - - if (status === SpawnStatus.ERROR) { - await fs.rm(path.join(AVD_DIR, `${avdName}.avd`), { recursive: true, force: true }); - await fs.rm(path.join(AVD_DIR, `${avdName}.ini`), { force: true }); - } - } -} diff --git a/src/resources/android/android-cli/android-emulators-parameter.ts b/src/resources/android/android-cli/android-emulators-parameter.ts new file mode 100644 index 00000000..621a0711 --- /dev/null +++ b/src/resources/android/android-cli/android-emulators-parameter.ts @@ -0,0 +1,27 @@ +import { ArrayStatefulParameter, getPty, SpawnStatus } from '@codifycli/plugin-core'; + +import { AndroidCliConfig } from './android-cli.js'; + +export class AndroidEmulatorsParameter extends ArrayStatefulParameter { + async refresh(_desired: string[] | null): Promise { + const $ = getPty(); + + const { status, data } = await $.spawnSafe('android emulator list', { interactive: true }); + if (status === SpawnStatus.ERROR) return null; + + return data + .split('\n') + .map((l) => l.trim().split(/\s+/)[0]) + .filter(Boolean); + } + + async addItem(item: string): Promise { + const $ = getPty(); + await $.spawn(`android emulator create "${item}"`, { interactive: true }); + } + + async removeItem(item: string): Promise { + const $ = getPty(); + await $.spawn(`android emulator remove "${item}"`, { interactive: true }); + } +} diff --git a/src/resources/android/android-cli/completions/android-emulator.profile.ts b/src/resources/android/android-cli/completions/android-cli.emulators.ts similarity index 100% rename from src/resources/android/android-cli/completions/android-emulator.profile.ts rename to src/resources/android/android-cli/completions/android-cli.emulators.ts diff --git a/src/resources/android/android-cli/completions/android-cli.packages.ts b/src/resources/android/android-cli/completions/android-cli.sdkPackages.ts similarity index 100% rename from src/resources/android/android-cli/completions/android-cli.packages.ts rename to src/resources/android/android-cli/completions/android-cli.sdkPackages.ts diff --git a/src/resources/android/android-cli/examples.ts b/src/resources/android/android-cli/examples.ts index 8b0a83c9..6e5744f3 100644 --- a/src/resources/android/android-cli/examples.ts +++ b/src/resources/android/android-cli/examples.ts @@ -6,7 +6,7 @@ export const exampleAndroidCliBasic: ExampleConfig = { configs: [ { type: 'android-cli', - sdkPackages:['cmdline-tools/latest', 'platform-tools', 'platforms/android-35', 'build-tools/35.0.0'], + sdkPackages: ['cmdline-tools/latest', 'platform-tools', 'platforms/android-35', 'build-tools/35.0.0'], }, ], }; @@ -17,48 +17,14 @@ export const exampleAndroidCliFullSetup: ExampleConfig = { configs: [ { type: 'android-cli', - sdkPackages:[ + sdkPackages: [ 'cmdline-tools/latest', 'platform-tools', 'platforms/android-35', 'build-tools/35.0.0', 'system-images/android-35/google_apis_playstore/x86_64', ], - }, - { - type: 'android-emulator', - profile: 'pixel_9', - }, - ], -}; - -export const exampleAndroidEmulatorBasic: ExampleConfig = { - title: 'Medium phone emulator', - description: 'Create a standard medium phone AVD — the default Android emulator profile, good for general app testing.', - configs: [ - { - type: 'android-cli', - sdkPackages:[ - 'cmdline-tools/latest', - 'platform-tools', - 'platforms/android-35', - 'system-images/android-35/google_apis_playstore/x86_64', - ], - }, - { - type: 'android-emulator', - profile: 'medium_phone', - }, - ], -}; - -export const exampleAndroidEmulatorPixel: ExampleConfig = { - title: 'Pixel 9 emulator', - description: 'Provision a Pixel 9 emulator matching current flagship hardware for testing on the latest Android profile.', - configs: [ - { - type: 'android-emulator', - profile: 'pixel_9', + emulators: ['pixel_9'], }, ], }; diff --git a/test/android/android-cli.test.ts b/test/android/android-cli.test.ts index 9ed92296..01335583 100644 --- a/test/android/android-cli.test.ts +++ b/test/android/android-cli.test.ts @@ -76,16 +76,13 @@ describe.skipIf(isLinuxArm)('Android Emulator integration tests', async () => { [ { type: 'android-cli', - sdkPackages:[ + sdkPackages: [ 'cmdline-tools/latest', 'platform-tools', 'platforms/android-35', 'system-images/android-35/google_apis_playstore/x86_64', ], - }, - { - type: 'android-emulator', - profile: 'medium_phone', + emulators: ['medium_phone'], }, ], { From 492cd5287acadaa517ebc91e421cd9f45f05d44c Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 30 Jun 2026 12:35:47 -0400 Subject: [PATCH 6/8] feat: improved documentation (capitalized folder names) --- docs/resources/(resources)/ai-agents/meta.json | 9 +++++++-- .../(resources)/android-development/meta.json | 4 ---- .../{android-development => android}/android-cli.mdx | 0 .../android-studio.mdx | 0 docs/resources/(resources)/android/meta.json | 7 +++++++ docs/resources/(resources)/asdf/meta.json | 8 ++++++-- docs/resources/(resources)/editors-ides/meta.json | 4 ++-- docs/resources/(resources)/git/meta.json | 9 +++++++-- docs/resources/(resources)/go/meta.json | 6 ++++-- docs/resources/(resources)/javascript/meta.json | 10 ++++++++-- .../resources/(resources)/package-managers/meta.json | 11 +++++++++-- docs/resources/(resources)/python/meta.json | 12 ++++++++++-- docs/resources/(resources)/ruby/meta.json | 8 +++++--- docs/resources/(resources)/scripting/meta.json | 2 +- docs/resources/(resources)/shell/meta.json | 2 +- docs/resources/(resources)/ssh/meta.json | 8 ++++++-- docs/resources/(resources)/syncthing/meta.json | 10 +++++++--- docs/resources/(resources)/tart/meta.json | 9 ++++++--- 18 files changed, 86 insertions(+), 33 deletions(-) delete mode 100644 docs/resources/(resources)/android-development/meta.json rename docs/resources/(resources)/{android-development => android}/android-cli.mdx (100%) rename docs/resources/(resources)/{android-development => android}/android-studio.mdx (100%) create mode 100644 docs/resources/(resources)/android/meta.json diff --git a/docs/resources/(resources)/ai-agents/meta.json b/docs/resources/(resources)/ai-agents/meta.json index 8ed55c6b..82293d90 100644 --- a/docs/resources/(resources)/ai-agents/meta.json +++ b/docs/resources/(resources)/ai-agents/meta.json @@ -1,4 +1,9 @@ { - "title": "ai & agents", - "pages": ["claude-code", "claude-code-project", "ollama", "openclaw"] + "title": "AI & Agents", + "pages": [ + "claude-code", + "claude-code-project", + "ollama", + "openclaw" + ] } diff --git a/docs/resources/(resources)/android-development/meta.json b/docs/resources/(resources)/android-development/meta.json deleted file mode 100644 index 7b0ed98d..00000000 --- a/docs/resources/(resources)/android-development/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "android", - "pages": ["android-cli", "android-studio"] -} diff --git a/docs/resources/(resources)/android-development/android-cli.mdx b/docs/resources/(resources)/android/android-cli.mdx similarity index 100% rename from docs/resources/(resources)/android-development/android-cli.mdx rename to docs/resources/(resources)/android/android-cli.mdx diff --git a/docs/resources/(resources)/android-development/android-studio.mdx b/docs/resources/(resources)/android/android-studio.mdx similarity index 100% rename from docs/resources/(resources)/android-development/android-studio.mdx rename to docs/resources/(resources)/android/android-studio.mdx diff --git a/docs/resources/(resources)/android/meta.json b/docs/resources/(resources)/android/meta.json new file mode 100644 index 00000000..5f93f272 --- /dev/null +++ b/docs/resources/(resources)/android/meta.json @@ -0,0 +1,7 @@ +{ + "title": "Android", + "pages": [ + "android-cli", + "android-studio" + ] +} diff --git a/docs/resources/(resources)/asdf/meta.json b/docs/resources/(resources)/asdf/meta.json index b5e9284b..363cc6ba 100644 --- a/docs/resources/(resources)/asdf/meta.json +++ b/docs/resources/(resources)/asdf/meta.json @@ -1,4 +1,8 @@ { - "title": "asdf", - "pages": ["asdf", "asdf-install", "asdf-plugin"] + "title": "Asdf", + "pages": [ + "asdf", + "asdf-install", + "asdf-plugin" + ] } diff --git a/docs/resources/(resources)/editors-ides/meta.json b/docs/resources/(resources)/editors-ides/meta.json index 2f542118..74fa45d2 100644 --- a/docs/resources/(resources)/editors-ides/meta.json +++ b/docs/resources/(resources)/editors-ides/meta.json @@ -1,7 +1,7 @@ { - "title": "editors & ides", + "title": "Editors & IDEs", "pages": [ - "[android-studio](../android/android-studio)", + "[android-studio](/docs/resources/android/android-studio)", "..." ] } diff --git a/docs/resources/(resources)/git/meta.json b/docs/resources/(resources)/git/meta.json index 88354b5d..c3207ec9 100644 --- a/docs/resources/(resources)/git/meta.json +++ b/docs/resources/(resources)/git/meta.json @@ -1,4 +1,9 @@ { - "title": "git", - "pages": ["git", "git-lfs", "git-repository", "wait-github-ssh-key"] + "title": "Git", + "pages": [ + "git", + "git-lfs", + "git-repository", + "wait-github-ssh-key" + ] } diff --git a/docs/resources/(resources)/go/meta.json b/docs/resources/(resources)/go/meta.json index a4a2b7c7..f63cc27e 100644 --- a/docs/resources/(resources)/go/meta.json +++ b/docs/resources/(resources)/go/meta.json @@ -1,4 +1,6 @@ { - "title": "go", - "pages": ["goenv"] + "title": "Go", + "pages": [ + "goenv" + ] } diff --git a/docs/resources/(resources)/javascript/meta.json b/docs/resources/(resources)/javascript/meta.json index 8e63b54f..aa7c41c3 100644 --- a/docs/resources/(resources)/javascript/meta.json +++ b/docs/resources/(resources)/javascript/meta.json @@ -1,4 +1,10 @@ { - "title": "javascript", - "pages": ["fast-node-manager", "npm", "npm-login", "nvm", "pnpm"] + "title": "JavaScript", + "pages": [ + "fast-node-manager", + "npm", + "npm-login", + "nvm", + "pnpm" + ] } diff --git a/docs/resources/(resources)/package-managers/meta.json b/docs/resources/(resources)/package-managers/meta.json index 24e86742..397caba5 100644 --- a/docs/resources/(resources)/package-managers/meta.json +++ b/docs/resources/(resources)/package-managers/meta.json @@ -1,4 +1,11 @@ { - "title": "package-managers", - "pages": ["homebrew", "apt", "dnf", "macports", "snap", "yum"] + "title": "Package Managers", + "pages": [ + "homebrew", + "apt", + "dnf", + "macports", + "snap", + "yum" + ] } diff --git a/docs/resources/(resources)/python/meta.json b/docs/resources/(resources)/python/meta.json index cbf1677e..3516e52a 100644 --- a/docs/resources/(resources)/python/meta.json +++ b/docs/resources/(resources)/python/meta.json @@ -1,4 +1,12 @@ { - "title": "python", - "pages": ["pip", "pip-sync", "pyenv", "uv", "venv-project", "virtualenv", "virtualenv-project"] + "title": "Python", + "pages": [ + "pip", + "pip-sync", + "pyenv", + "uv", + "venv-project", + "virtualenv", + "virtualenv-project" + ] } diff --git a/docs/resources/(resources)/ruby/meta.json b/docs/resources/(resources)/ruby/meta.json index b549e174..7553f068 100644 --- a/docs/resources/(resources)/ruby/meta.json +++ b/docs/resources/(resources)/ruby/meta.json @@ -1,4 +1,6 @@ { - "title": "ruby", - "pages": ["rbenv"] -} \ No newline at end of file + "title": "Ruby", + "pages": [ + "rbenv" + ] +} diff --git a/docs/resources/(resources)/scripting/meta.json b/docs/resources/(resources)/scripting/meta.json index 98ab21a9..d6c98592 100644 --- a/docs/resources/(resources)/scripting/meta.json +++ b/docs/resources/(resources)/scripting/meta.json @@ -1,3 +1,3 @@ { - "title": "scripting" + "title": "Scripting" } diff --git a/docs/resources/(resources)/shell/meta.json b/docs/resources/(resources)/shell/meta.json index af2d01b7..909d759d 100644 --- a/docs/resources/(resources)/shell/meta.json +++ b/docs/resources/(resources)/shell/meta.json @@ -1,3 +1,3 @@ { - "title": "shell" + "title": "Shell" } diff --git a/docs/resources/(resources)/ssh/meta.json b/docs/resources/(resources)/ssh/meta.json index 1bd6dc3a..a13178f8 100644 --- a/docs/resources/(resources)/ssh/meta.json +++ b/docs/resources/(resources)/ssh/meta.json @@ -1,4 +1,8 @@ { - "title": "ssh", - "pages": ["ssh-add", "ssh-config", "ssh-key"] + "title": "SSH", + "pages": [ + "ssh-add", + "ssh-config", + "ssh-key" + ] } diff --git a/docs/resources/(resources)/syncthing/meta.json b/docs/resources/(resources)/syncthing/meta.json index e1b2c1dc..9c798262 100644 --- a/docs/resources/(resources)/syncthing/meta.json +++ b/docs/resources/(resources)/syncthing/meta.json @@ -1,4 +1,8 @@ { - "title": "syncthing", - "pages": ["syncthing", "syncthing-device", "syncthing-folder"] -} \ No newline at end of file + "title": "Syncthing", + "pages": [ + "syncthing", + "syncthing-device", + "syncthing-folder" + ] +} diff --git a/docs/resources/(resources)/tart/meta.json b/docs/resources/(resources)/tart/meta.json index 06c8902a..22a4b377 100644 --- a/docs/resources/(resources)/tart/meta.json +++ b/docs/resources/(resources)/tart/meta.json @@ -1,4 +1,7 @@ { - "title": "tart", - "pages": ["tart", "tart-vm"] -} \ No newline at end of file + "title": "Tart", + "pages": [ + "tart", + "tart-vm" + ] +} From 68e59b5fe08f46cb63f6c2c117218308861b3bd2 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Wed, 1 Jul 2026 11:03:20 -0400 Subject: [PATCH 7/8] fix: added warning message for tart clone if disk is not accessible. Added apply note for android cli. Fixed android cli emulator list --- package-lock.json | 4 +- package.json | 2 +- .../android/android-cli/android-cli.ts | 4 ++ .../completions/android-cli.emulators.ts | 50 ++----------------- src/resources/tart/clone-parameter.ts | 26 +++++++++- 5 files changed, 36 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4311153..5d0e5c33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "default", - "version": "1.11.0", + "version": "1.12.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "default", - "version": "1.11.0", + "version": "1.12.0-beta.1", "license": "ISC", "dependencies": { "@codifycli/plugin-core": "^1.2.5", diff --git a/package.json b/package.json index 67aded2d..40dd01e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "default", - "version": "1.11.0", + "version": "1.12.0-beta.4", "description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux", "main": "dist/index.js", "scripts": { diff --git a/src/resources/android/android-cli/android-cli.ts b/src/resources/android/android-cli/android-cli.ts index 32df82f5..a595b5ba 100644 --- a/src/resources/android/android-cli/android-cli.ts +++ b/src/resources/android/android-cli/android-cli.ts @@ -9,6 +9,8 @@ import { Utils, getPty, z, + CodifyCliSender, + ApplyNotes, } from '@codifycli/plugin-core'; import { OS } from '@codifycli/schemas'; import * as fs from 'node:fs/promises'; @@ -119,6 +121,8 @@ export class AndroidCliResource extends Resource { if (plan.desiredConfig.sdkPath) { await this.setSdkPath(plan.desiredConfig.sdkPath); } + + CodifyCliSender.sendApplyNote(ApplyNotes.NEW_SHELL_REQUIRED, 'android-cli') } async modify(pc: ParameterChange, _plan: ModifyPlan): Promise { diff --git a/src/resources/android/android-cli/completions/android-cli.emulators.ts b/src/resources/android/android-cli/completions/android-cli.emulators.ts index c36ad06a..b59b01e2 100644 --- a/src/resources/android/android-cli/completions/android-cli.emulators.ts +++ b/src/resources/android/android-cli/completions/android-cli.emulators.ts @@ -2,53 +2,11 @@ // These correspond to profiles accepted by `android emulator create --profile=`. export default async function loadAndroidEmulatorProfiles(): Promise { return [ - // Generic form factors + 'large_desktop', + 'medium_desktop', 'medium_phone', - 'small_phone', - 'foldable', 'medium_tablet', - 'resizable', - 'desktop_medium', - - // Pixel phones - 'pixel_9', - 'pixel_9_pro', - 'pixel_9_pro_xl', - 'pixel_9_pro_fold', - 'pixel_8', - 'pixel_8_pro', - 'pixel_7', - 'pixel_7_pro', - 'pixel_7a', - 'pixel_6', - 'pixel_6_pro', - 'pixel_6a', - 'pixel_5', - 'pixel_4', - 'pixel_4_xl', - 'pixel_4a', - 'pixel_3', - 'pixel_3_xl', - 'pixel_3a', - 'pixel_3a_xl', - - // Pixel tablets / foldables - 'pixel_tablet', - 'pixel_fold', - - // Wear OS - 'wear_os_large_round', - 'wear_os_small_round', - 'wear_os_square', - 'wear_os_rect', - - // Android TV - 'tv_1080p', - 'tv_720p', - 'tv_4k', - - // Automotive - 'automotive_1024p_landscape', - 'automotive_portrait', + 'small_desktop', + 'small_phone', ]; } diff --git a/src/resources/tart/clone-parameter.ts b/src/resources/tart/clone-parameter.ts index f607983d..b2a842d3 100644 --- a/src/resources/tart/clone-parameter.ts +++ b/src/resources/tart/clone-parameter.ts @@ -15,11 +15,35 @@ export class TartCloneParameter extends ArrayStatefulParameter | null> { + async refresh(desired: Array | null): Promise | null> { const $ = getPty(); // List all available VMs in JSON format const { status, data } = await $.spawnSafe('tart list --format json', { interactive: true }); + + // A non-zero exit can mean two very different things, and exit code alone can't + // distinguish them — so we parse the output: + // 1. Tart isn't installed / has nothing to report -> the resource doesn't exist (null). + // 2. Tart failed to *access* its storage -> a real error we must not swallow. + // The most common #2 is macOS blocking access to the TART_HOME directory (e.g. an + // external/removable volume without Full Disk Access), which surfaces as + // "Operation not permitted" / "you don't have permission to view it" even though the + // Unix permissions are fine. Silently returning null there makes Codify believe + // declared VMs are missing and offer to re-clone them. + const permissionDenied = /operation not permitted|don.?t have permission|permission to view/i.test(data ?? ''); + if (permissionDenied) { + const tartHome = process.env.TART_HOME; + throw new Error( + `Failed to list Tart VMs — macOS denied access to Tart's storage` + + (tartHome ? ` (TART_HOME="${tartHome}")` : '') + + `.\n\n${data}\n\n` + + `If TART_HOME points at an external or removable volume, grant the app running ` + + `Codify (your terminal and/or the Codify desktop app) access under System Settings ` + + `→ Privacy & Security → Files and Folders (Removable Volumes) or Full Disk Access, ` + + `then restart the app.` + ); + } + if (status !== SpawnStatus.SUCCESS) { return null; } From d4d8cdc5be45f6f755a565410ab9c52165318770 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Wed, 1 Jul 2026 12:26:18 -0400 Subject: [PATCH 8/8] feat: add beta completions --- completions-cron/src/index.ts | 63 ++++++++++++------- .../types/worker-configuration.d.ts | 4 ++ completions-cron/wrangler.toml | 12 ++++ package.json | 2 +- scripts/deploy.ts | 24 +++++++ 5 files changed, 81 insertions(+), 24 deletions(-) diff --git a/completions-cron/src/index.ts b/completions-cron/src/index.ts index 6ff5d11f..9c87babd 100644 --- a/completions-cron/src/index.ts +++ b/completions-cron/src/index.ts @@ -6,22 +6,25 @@ const BATCH_SIZE = 1000 async function getResourceId( supabase: SupabaseClient, resourceType: string, + prerelease: boolean, cache: Map ): Promise { - if (cache.has(resourceType)) { - return cache.get(resourceType)! + const cacheKey = `${resourceType}:${prerelease}` + if (cache.has(cacheKey)) { + return cache.get(cacheKey)! } const { data, error } = await supabase .from('registry_resources') .select('id') .eq('type', resourceType) + .eq('prerelease', prerelease) if (error || !data?.[0]?.id) { - throw new Error(`Resource type '${resourceType}' not found in registry_resources`) + throw new Error(`Resource type '${resourceType}' (prerelease=${prerelease}) not found in registry_resources`) } - cache.set(resourceType, data[0].id) + cache.set(cacheKey, data[0].id) return data[0].id } @@ -30,6 +33,7 @@ async function processModule( resourceType: string, parameterPath: string, fetchFn: () => Promise, + prerelease: boolean, resourceIdCache: Map ): Promise { console.log(`Processing ${resourceType}${parameterPath}...`) @@ -37,7 +41,7 @@ async function processModule( const values = await fetchFn() console.log(` [${resourceType}${parameterPath}] Fetched ${values.length} values`) - const resourceId = await getResourceId(supabase, resourceType, resourceIdCache) + const resourceId = await getResourceId(supabase, resourceType, prerelease, resourceIdCache) await supabase .from('resource_parameter_completions') @@ -66,31 +70,44 @@ async function processModule( console.log(` [${resourceType}${parameterPath}] Done: inserted ${values.length} completions`) } +async function runCompletions(env: Env): Promise { + const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY) + const prerelease = env.PRERELEASE === 'true' + const resourceIdCache = new Map() + + const results = await Promise.allSettled( + completionModules.map(({ resourceType, parameterPath, fetch }: CompletionModule) => + processModule(supabase, resourceType, parameterPath, fetch, prerelease, resourceIdCache) + ) + ) + + for (const result of results) { + if (result.status === 'rejected') { + console.error('Completion module failed:', result.reason) + } + } + + console.log('Successfully processed all resource completion tasks') +} + export default { - async fetch(req: Request) { + async fetch(req: Request, env: Env, ctx: ExecutionContext) { const url = new URL(req.url) + + if (req.method === 'POST' && url.pathname === '/trigger') { + if (req.headers.get('Authorization') !== env.TRIGGER_SECRET) { + return new Response('Unauthorized', { status: 401 }) + } + ctx.waitUntil(runCompletions(env)) + return new Response('Triggered', { status: 202 }) + } + url.pathname = '/__scheduled' url.searchParams.append('cron', '* * * * *') return new Response(`To test the scheduled handler, ensure you have used the "--test-scheduled" then try running "curl ${url.href}".`) }, async scheduled(_event: ScheduledEvent, env: Env, _ctx: ExecutionContext): Promise { - console.log('hihi') - const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY) - const resourceIdCache = new Map() - - const results = await Promise.allSettled( - completionModules.map(({ resourceType, parameterPath, fetch }: CompletionModule) => - processModule(supabase, resourceType, parameterPath, fetch, resourceIdCache) - ) - ) - - for (const result of results) { - if (result.status === 'rejected') { - console.error('Completion module failed:', result.reason) - } - } - - console.log('Successfully processed all resource completion tasks') + await runCompletions(env) }, } satisfies ExportedHandler diff --git a/completions-cron/types/worker-configuration.d.ts b/completions-cron/types/worker-configuration.d.ts index ddcdff5b..6d484ed6 100644 --- a/completions-cron/types/worker-configuration.d.ts +++ b/completions-cron/types/worker-configuration.d.ts @@ -6,6 +6,10 @@ declare namespace Cloudflare { mainModule: typeof import("../src"); } interface Env { + SUPABASE_URL: string; + SUPABASE_SERVICE_ROLE_KEY: string; + PRERELEASE: string; + TRIGGER_SECRET: string; } } interface Env extends Cloudflare.Env {} diff --git a/completions-cron/wrangler.toml b/completions-cron/wrangler.toml index 6b212c6a..410a010d 100644 --- a/completions-cron/wrangler.toml +++ b/completions-cron/wrangler.toml @@ -10,6 +10,18 @@ head_sampling_rate = 1 [vars] SUPABASE_URL = "https://kdctbvqvqjfquplxhqrm.supabase.co" +PRERELEASE = "false" [triggers] crons = ["0 5 * * *"] + +# Beta environment — deploys as a separate worker: resource-completions-cron-beta +[env.beta] +name = "resource-completions-cron-beta" + +[env.beta.vars] +SUPABASE_URL = "https://kdctbvqvqjfquplxhqrm.supabase.co" +PRERELEASE = "true" + +[env.beta.triggers] +crons = ["0 5 * * *"] diff --git a/package.json b/package.json index 40dd01e5..62cdbbf9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "default", - "version": "1.12.0-beta.4", + "version": "1.12.0-beta.5", "description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux", "main": "dist/index.js", "scripts": { diff --git a/scripts/deploy.ts b/scripts/deploy.ts index debc9cdb..82152723 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -86,6 +86,9 @@ const versionRow = await client.from('registry_plugin_versions').upsert({ await uploadResources(isBeta); if (isBeta) { + console.log('Deploying beta completions worker...') + cp.spawnSync('source ~/.zshrc; npm run build:completions && cd completions-cron && npx wrangler deploy --env beta', { shell: 'zsh', stdio: 'inherit' }) + // Generate embeddings for prerelease resources so the AI agent can find them via semantic search console.log('Triggering vector reindex for prerelease resources...') const reindexKey = process.env.REINDEX_API_KEY @@ -129,6 +132,27 @@ if (!isBeta) { } } +// Trigger an immediate completions run so completions are populated right after deploy +// (the daily cron keeps them updated over time) +console.log('Triggering completions run...') +const workerUrl = isBeta + ? process.env.COMPLETIONS_BETA_WORKER_URL + : process.env.COMPLETIONS_WORKER_URL +const triggerSecret = process.env.COMPLETIONS_TRIGGER_SECRET +if (!workerUrl || !triggerSecret) { + console.warn('COMPLETIONS_WORKER_URL / COMPLETIONS_BETA_WORKER_URL / COMPLETIONS_TRIGGER_SECRET not set — skipping completions trigger') +} else { + const res = await fetch(`${workerUrl}/trigger`, { + method: 'POST', + headers: { Authorization: triggerSecret }, + }) + if (!res.ok) { + console.error(`Completions trigger failed: ${res.status} ${await res.text()}`) + } else { + console.log('Completions trigger accepted (running in background on worker)') + } +} + async function uploadResources(prerelease: boolean) { const Metadata: Array> = require('../dist/metadata.json');