Skip to content
Merged
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
391 changes: 27 additions & 364 deletions src/app.ts

Large diffs are not rendered by default.

11 changes: 0 additions & 11 deletions src/device/device-feature-index.ts

This file was deleted.

87 changes: 87 additions & 0 deletions src/device/device-feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Command } from 'commander'
import type {
DeviceInstallCommandOptions,
DeviceListCommandOptions,
DeviceOpenCommandOptions,
DeviceTuneCommandOptions,
} from './data-access/device-types.ts'
import { runDeviceInstall } from './device-feature-install.ts'
import { runDeviceList } from './device-feature-list.ts'
import { runDeviceOpen } from './device-feature-open.ts'
import { runDeviceTune } from './device-feature-tune.ts'

export type DeviceCommandDeps = {
runDeviceInstall?: (options: DeviceInstallCommandOptions) => Promise<void>
runDeviceList?: (options: DeviceListCommandOptions) => Promise<void>
runDeviceOpen?: (options: DeviceOpenCommandOptions) => Promise<void>
runDeviceTune?: (options: DeviceTuneCommandOptions) => Promise<void>
}

export function createDeviceCommand({
runDeviceInstall: runDeviceInstallCommand = runDeviceInstall,
runDeviceList: runDeviceListCommand = runDeviceList,
runDeviceOpen: runDeviceOpenCommand = runDeviceOpen,
runDeviceTune: runDeviceTuneCommand = runDeviceTune,
}: DeviceCommandDeps = {}): Command {
const deviceCommand = new Command('device').description('Work with connected devices and emulators')

deviceCommand.action(() => {
deviceCommand.outputHelp()
})

const deviceInstallCommand = deviceCommand
.command('install [apks...]')
.description('Install APKs from files, directories, or the APK catalog')
.option('--all', 'Install on every connected device')
.option('--device <serial>', 'Target a device serial')
.option('--downgrade', 'Allow version downgrades (adb install -d)')
.option('--force', 'Re-download catalog APKs even when cached')
.option('--grant', 'Grant all runtime permissions (adb install -g)')
.option('--list', 'List the APKs available in the catalog')
.option('-v, --verbose', 'Verbose output')
.action(async (apks: string[] | undefined, options: Omit<DeviceInstallCommandOptions, 'apks'>) => {
if (options.all && options.device) {
deviceInstallCommand.error(
`error: The --all flag can't be used in combination with --device. Please specify only one.`,
)
}

await runDeviceInstallCommand({ ...options, apks: apks ?? [] })
})

deviceCommand
.command('list')
.description('List connected devices and emulators')
.option('--json', 'Print a stable JSON report')
.action(async (options: DeviceListCommandOptions) => {
await runDeviceListCommand(options)
})

deviceCommand
.command('open [url]')
.description('Open a URL, port, or deep link on a connected device')
.option('--device <serial>', 'Target a device serial')
.option('--no-forward', 'Do not create an adb reverse for localhost URLs')
.option('-v, --verbose', 'Explain URL and port forwarding decisions')
.action(async (url: string | undefined, options: Omit<DeviceOpenCommandOptions, 'url'>) => {
await runDeviceOpenCommand({ ...options, url })
})

const deviceTuneCommand = deviceCommand
.command('tune')
.description('Apply agent-friendly tweaks to a connected device or emulator')
.option('--all', 'Tune every connected device')
.option('--device <serial>', 'Target a device serial')
.option('-y, --yes', 'Apply every tweak without prompting')
.action(async (options: DeviceTuneCommandOptions) => {
if (options.all && options.device) {
deviceTuneCommand.error(
`error: The --all flag can't be used in combination with --device. Please specify only one.`,
)
}

await runDeviceTuneCommand(options)
})

return deviceCommand
}
27 changes: 27 additions & 0 deletions src/doctor/data-access/doctor-report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { DoctorCapabilities, DoctorCheckResult, DoctorReport } from './doctor-check-result.ts'

export function buildDoctorReport(checks: DoctorCheckResult[]): DoctorReport {
const capabilities = deriveCapabilities(checks)
return {
capabilities,
checks,
ready: capabilities.projectCreation && capabilities.androidBuild,
recommendations: [...new Set(checks.flatMap(({ recommendation }) => (recommendation ? [recommendation] : [])))],
}
}

export function deriveCapabilities(checks: DoctorCheckResult[]): DoctorCapabilities {
const passes = (name: string) => checks.some((check) => check.name === name && check.status === 'pass')
const androidBuild = ['Android SDK', 'Android platforms', 'Build Tools', 'Java', 'Java compiler', 'adb'].every(passes)
const projectCreation = ['Node.js', 'Package managers'].every(passes)
return {
androidBuild,
emulator: androidBuild && ['Android emulators', 'Emulator', 'avdmanager'].every(passes),
physicalDevice: androidBuild && passes('Physical devices'),
projectCreation,
}
}

export function getDoctorExitCode(report: DoctorReport) {
return report.checks.some(({ status }) => status === 'fail') ? 1 : 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import {
checkOperatingSystem,
checkPackageManagers,
} from './data-access/check-system-and-javascript.ts'
import type { DoctorCapabilities, DoctorCheckResult, DoctorReport } from './data-access/doctor-check-result.ts'
import type { DoctorReport } from './data-access/doctor-check-result.ts'
import { type DoctorEnvironment, defaultDoctorEnvironment } from './data-access/doctor-environment.ts'
import { buildDoctorReport, getDoctorExitCode } from './data-access/doctor-report.ts'
import { renderDoctorReport } from './ui/doctor-ui-report.ts'

export type DoctorCommandOptions = { json?: boolean; verbose?: boolean }
Expand Down Expand Up @@ -43,35 +44,9 @@ export async function createDoctorReport(
return buildDoctorReport(checks)
}

export function buildDoctorReport(checks: DoctorCheckResult[]): DoctorReport {
const capabilities = deriveCapabilities(checks)
return {
capabilities,
checks,
ready: capabilities.projectCreation && capabilities.androidBuild,
recommendations: [...new Set(checks.flatMap(({ recommendation }) => (recommendation ? [recommendation] : [])))],
}
}

export function deriveCapabilities(checks: DoctorCheckResult[]): DoctorCapabilities {
const passes = (name: string) => checks.some((check) => check.name === name && check.status === 'pass')
const androidBuild = ['Android SDK', 'Android platforms', 'Build Tools', 'Java', 'Java compiler', 'adb'].every(passes)
const projectCreation = ['Node.js', 'Package managers'].every(passes)
return {
androidBuild,
emulator: androidBuild && ['Android emulators', 'Emulator', 'avdmanager'].every(passes),
physicalDevice: androidBuild && passes('Physical devices'),
projectCreation,
}
}

export async function runDoctor(options: DoctorCommandOptions = {}) {
const report = await createDoctorReport()
if (options.json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
else renderDoctorReport(report, options.verbose)
return getDoctorExitCode(report)
}

export function getDoctorExitCode(report: DoctorReport) {
return report.checks.some(({ status }) => status === 'fail') ? 1 : 0
}
16 changes: 16 additions & 0 deletions src/doctor/doctor-feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Command } from 'commander'
import { type DoctorCommandOptions, runDoctor } from './doctor-feature-check.ts'

export type DoctorCommandDeps = {
runDoctor?: (options: DoctorCommandOptions) => Promise<number>
}

export function createDoctorCommand({ runDoctor: runDoctorCommand = runDoctor }: DoctorCommandDeps = {}): Command {
return new Command('doctor')
.description('Check local development dependencies')
.option('--json', 'Print a stable JSON report')
.option('--verbose', 'Include resolved paths and diagnostic details')
.action(async (options: DoctorCommandOptions) => {
process.exitCode = await runDoctorCommand(options)
})
}
24 changes: 0 additions & 24 deletions src/emulator/emulator-feature-index.ts

This file was deleted.

157 changes: 157 additions & 0 deletions src/emulator/emulator-feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Command } from 'commander'
import { parseIntegerOption } from '../core/ui/core-ui-command-options.ts'
import type {
EmulatorCreateCommandOptions,
EmulatorDeleteCommandOptions,
EmulatorImagesCommandOptions,
EmulatorImagesDeleteCommandOptions,
EmulatorImagesInstallCommandOptions,
EmulatorListCommandOptions,
EmulatorStartCommandOptions,
EmulatorStatusCommandOptions,
EmulatorStopCommandOptions,
EmulatorTuneCommandOptions,
} from './data-access/emulator-types.ts'
import { runEmulatorCreate } from './emulator-feature-create.ts'
import { runEmulatorDelete } from './emulator-feature-delete.ts'
import { runEmulatorImages, runEmulatorImagesDelete, runEmulatorImagesInstall } from './emulator-feature-images.ts'
import { runEmulatorList } from './emulator-feature-list.ts'
import { runEmulatorStart } from './emulator-feature-start.ts'
import { runEmulatorStatus } from './emulator-feature-status.ts'
import { runEmulatorStop } from './emulator-feature-stop.ts'
import { runEmulatorTune } from './emulator-feature-tune.ts'

export type EmulatorCommandDeps = {
runEmulatorCreate?: (options: EmulatorCreateCommandOptions) => Promise<void>
runEmulatorDelete?: (options: EmulatorDeleteCommandOptions) => Promise<void>
runEmulatorImages?: (options: EmulatorImagesCommandOptions) => Promise<void>
runEmulatorImagesDelete?: (options: EmulatorImagesDeleteCommandOptions) => Promise<void>
runEmulatorImagesInstall?: (options: EmulatorImagesInstallCommandOptions) => Promise<void>
runEmulatorList?: (options: EmulatorListCommandOptions) => Promise<void>
runEmulatorStart?: (options: EmulatorStartCommandOptions) => Promise<void>
runEmulatorStatus?: (options: EmulatorStatusCommandOptions) => Promise<void>
runEmulatorStop?: (options: EmulatorStopCommandOptions) => Promise<void>
runEmulatorTune?: (options: EmulatorTuneCommandOptions) => Promise<void>
}

export function createEmulatorCommand({
runEmulatorCreate: runEmulatorCreateCommand = runEmulatorCreate,
runEmulatorDelete: runEmulatorDeleteCommand = runEmulatorDelete,
runEmulatorImages: runEmulatorImagesCommand = runEmulatorImages,
runEmulatorImagesDelete: runEmulatorImagesDeleteCommand = runEmulatorImagesDelete,
runEmulatorImagesInstall: runEmulatorImagesInstallCommand = runEmulatorImagesInstall,
runEmulatorList: runEmulatorListCommand = runEmulatorList,
runEmulatorStart: runEmulatorStartCommand = runEmulatorStart,
runEmulatorStatus: runEmulatorStatusCommand = runEmulatorStatus,
runEmulatorStop: runEmulatorStopCommand = runEmulatorStop,
runEmulatorTune: runEmulatorTuneCommand = runEmulatorTune,
}: EmulatorCommandDeps = {}): Command {
const emulatorCommand = new Command('emulator').alias('emu').description('Manage Android emulators')

emulatorCommand.action(() => {
emulatorCommand.outputHelp()
})

emulatorCommand
.command('create [name]')
.description('Create or update an Android emulator')
.option('--data-size <size>', 'Data partition size')
.option('--device <device>', 'Android device profile id')
.option('--profile <profile>', 'Solana Mobile emulator profile')
.option('--ram-mb <megabytes>', 'RAM size in MB', parseIntegerOption)
.option('--sdcard-size <size>', 'SD card size')
.option('--sdk-root <path>', 'Android SDK root')
.option('--start', 'Start the emulator after creating it')
.option('--system-image <package>', 'Android system image package')
.option('--tune', 'Apply emulator tweaks after starting (requires --start)')
.option('-v, --verbose', 'Verbose output')
.option('--vm-heap-mb <megabytes>', 'VM heap size in MB', parseIntegerOption)
.action(async (name: string | undefined, options: Omit<EmulatorCreateCommandOptions, 'name'>) => {
await runEmulatorCreateCommand({ ...options, name })
})

emulatorCommand
.command('delete [names...]')
.description('Delete Android emulators')
.option('--sdk-root <path>', 'Android SDK root')
.action(async (names: string[] | undefined, options: Omit<EmulatorDeleteCommandOptions, 'names'>) => {
await runEmulatorDeleteCommand({ ...options, names: names ?? [] })
})

const emulatorImagesCommand = emulatorCommand.command('images').description('Manage Android system images')

emulatorImagesCommand.action(() => {
emulatorImagesCommand.outputHelp()
})

emulatorImagesCommand
.command('delete [systemImages...]')
.description('Delete installed Android system images')
.option('--sdk-root <path>', 'Android SDK root')
.option('-v, --verbose', 'Verbose output')
.action(
async (systemImages: string[] | undefined, options: Omit<EmulatorImagesDeleteCommandOptions, 'systemImages'>) => {
await runEmulatorImagesDeleteCommand({ ...options, systemImages: systemImages ?? [] })
},
)

emulatorImagesCommand
.command('install [systemImage]')
.description('Install an Android system image')
.option('--all', 'Show all available system images')
.option('--sdk-root <path>', 'Android SDK root')
.option('-v, --verbose', 'Verbose output')
.action(
async (systemImage: string | undefined, options: Omit<EmulatorImagesInstallCommandOptions, 'systemImage'>) => {
await runEmulatorImagesInstallCommand({ ...options, systemImage })
},
)

emulatorImagesCommand
.command('list')
.description('List installed Android system images')
.option('--sdk-root <path>', 'Android SDK root')
.action(async (options: EmulatorImagesCommandOptions) => {
await runEmulatorImagesCommand(options)
})

emulatorCommand
.command('list')
.description('List installed Android emulators')
.action(async (options: EmulatorListCommandOptions) => {
await runEmulatorListCommand(options)
})

emulatorCommand
.command('start [name]')
.description('Start an Android emulator')
.option('--sdk-root <path>', 'Android SDK root')
.option('--tune', 'Apply emulator tweaks after starting')
.action(async (name: string | undefined, options: Omit<EmulatorStartCommandOptions, 'name'>) => {
await runEmulatorStartCommand({ ...options, name })
})

emulatorCommand
.command('status [nameOrSerial]')
.description('Show Android emulator status')
.action(async (nameOrSerial: string | undefined) => {
await runEmulatorStatusCommand({ nameOrSerial })
})

emulatorCommand
.command('stop [nameOrSerial]')
.description('Stop a running Android emulator')
.action(async (nameOrSerial: string | undefined) => {
await runEmulatorStopCommand({ nameOrSerial })
})

emulatorCommand
.command('tune [nameOrSerial]')
.description('Apply agent-friendly tweaks to a running Android emulator')
.option('-y, --yes', 'Apply every tweak without prompting')
.action(async (nameOrSerial: string | undefined, options: Omit<EmulatorTuneCommandOptions, 'nameOrSerial'>) => {
await runEmulatorTuneCommand({ ...options, nameOrSerial })
})

return emulatorCommand
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ import { startPlaygroundServer } from './data-access/playground-server.ts'
import type { PlaygroundCommandOptions } from './data-access/playground-types.ts'
import { renderPlaygroundEvent } from './ui/playground-ui-messages.ts'

export { parsePlaygroundClusterId } from './data-access/playground-clusters.ts'
export type { PlaygroundClusterId, PlaygroundCommandOptions } from './data-access/playground-types.ts'

interface RunPlaygroundDependencies extends AdbDependencies, PromptDependencies {
cancel?: (message: string) => void
formatCommand?: typeof formatCliCommand
Expand Down
Loading
Loading