Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions docs/resources/(resources)/editors-ides/android-cli.mdx
Original file line number Diff line number Diff line change
@@ -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=<path>`. 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 |
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -113,6 +115,8 @@ runPlugin(Plugin.create(
new GitRepositoryResource(),
new GitRepositoriesResource(),
new AndroidStudioResource(),
new AndroidCliResource(),
new AndroidEmulatorResource(),
new AsdfResource(),
new AsdfPluginResource(),
new AsdfInstallResource(),
Expand Down
165 changes: 165 additions & 0 deletions src/resources/android/android-cli/android-cli.ts
Original file line number Diff line number Diff line change
@@ -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=<path>. 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<typeof schema>;

const ANDROIDRC_PATH = path.join(os.homedir(), '.androidrc');

const defaultConfig: Partial<AndroidCliConfig> = {
packages: [],
};

export class AndroidCliResource extends Resource<AndroidCliConfig> {
getSettings(): ResourceSettings<AndroidCliConfig> {
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<AndroidCliConfig>): Promise<Partial<AndroidCliConfig> | null> {
const $ = getPty();

const { status } = await $.spawnSafe('which android');
if (status === SpawnStatus.ERROR) return null;

const result: Partial<AndroidCliConfig> = {};

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<AndroidCliConfig>): Promise<void> {
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<AndroidCliConfig>, _plan: ModifyPlan<AndroidCliConfig>): Promise<void> {
if (pc.name === 'sdkPath') {
if (pc.newValue) {
await this.setSdkPath(pc.newValue as string);
} else {
await this.removeSdkPath();
}
}
}

async destroy(plan: DestroyPlan<AndroidCliConfig>): Promise<void> {
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<void> {
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<void> {
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 */ }
}
}
116 changes: 116 additions & 0 deletions src/resources/android/android-cli/android-emulator.ts
Original file line number Diff line number Diff line change
@@ -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<typeof schema>;

const AVD_DIR = path.join(os.homedir(), '.android', 'avd');

const defaultConfig: Partial<AndroidEmulatorConfig> = {
profile: '<Replace me here!>',
};

export class AndroidEmulatorResource extends Resource<AndroidEmulatorConfig> {
getSettings(): ResourceSettings<AndroidEmulatorConfig> {
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<AndroidEmulatorConfig>): Promise<Partial<AndroidEmulatorConfig> | 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<AndroidEmulatorConfig>): Promise<void> {
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<AndroidEmulatorConfig>): Promise<void> {
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<AndroidEmulatorConfig>): string | undefined {
return params.name ?? params.profile;
}
}
Loading