From c09a98f422b51adf7893b62090d800326a413357 Mon Sep 17 00:00:00 2001 From: Stanislaw Karpiejczyk Date: Fri, 28 Aug 2026 14:58:23 +0200 Subject: [PATCH] feat(cli): target device for app install Signed-off-by: Stanislaw Karpiejczyk --- .changeset/device-targeted-install.md | 6 ++++ packages/cli/README.md | 5 +-- packages/cli/src/commands/app.ts | 5 +-- packages/cli/tests/app.test.ts | 46 +++++++++++++++++++++++++++ packages/core/src/hdc/app.ts | 8 ++++- packages/core/tests/exec.test.ts | 6 ++-- 6 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 .changeset/device-targeted-install.md create mode 100644 packages/cli/tests/app.test.ts diff --git a/.changeset/device-targeted-install.md b/.changeset/device-targeted-install.md new file mode 100644 index 0000000..dd481f7 --- /dev/null +++ b/.changeset/device-targeted-install.md @@ -0,0 +1,6 @@ +--- +"@oniroproject/core": minor +"@oniroproject/oniro-app": minor +--- + +Add device targeting to app installation. `installApp` now accepts `deviceSerial`, and `oniro-app app install` exposes it as `--device `. diff --git a/packages/cli/README.md b/packages/cli/README.md index 4242e65..d609a02 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -162,8 +162,8 @@ These commands target a connected device or emulator over `hdc`. Most accept `-- | Command | Description | | --- | --- | -| `app install [project-dir] [--hap ]` | Install the signed `.hap` via `hdc`. `--hap` overrides the resolved path. | -| `app launch [project-dir] [--module ] [--ability ]` | Launch the app. `--ability` defaults to the module's `mainElement` / first visible ability. | +| `app install [project-dir] [--hap ] [--device ]` | Install the signed `.hap` via `hdc`. `--hap` overrides the resolved path. | +| `app launch [project-dir] [--module ] [--ability ] [--device ]` | Launch the app. `--ability` defaults to the module's `mainElement` / first visible ability. | | `app apply [project-dir] --bundle [options]` | Install a HAP **and verify the running process took the change** — handles sign-info mismatch, asset-cache invalidation, and persistent-bundle restart. | | `app uninstall [--device ]` | Uninstall an app by bundle name. | | `app stop [--device ]` | Force-stop an app by bundle name. | @@ -178,6 +178,7 @@ These commands target a connected device or emulator over `hdc`. Most accept `-- | `--installed-hap ` | Local copy of the currently-installed HAP, to enable the asset-cache reboot diff. | | `--system` | Treat as a persistent/system bundle (refuses to uninstall on a sign-info mismatch). | | `--allow-uninstall` | Permit uninstalling a system bundle on sign-info mismatch (dangerous). | +| `--device ` | Target device serial. | | `--json` | Emit the result (`method`, pre/post-install pid, `cacheCleared`) as JSON. | #### `file` — transfer files to/from the device diff --git a/packages/cli/src/commands/app.ts b/packages/cli/src/commands/app.ts index fdf899e..4e34625 100644 --- a/packages/cli/src/commands/app.ts +++ b/packages/cli/src/commands/app.ts @@ -21,11 +21,12 @@ export function registerAppCommand(program: Command): void { .command('install [project-dir]') .description('Install the signed .hap on the connected device/emulator via hdc.') .option('--hap ', 'Override the .hap path (relative to project-dir or absolute).') - .action(async (projectDir: string | undefined, opts: { hap?: string }) => { + .option('--device ', 'Target device serial.') + .action(async (projectDir: string | undefined, opts: { hap?: string; device?: string }) => { const { config, logger } = getRuntime(); const dir = path.resolve(projectDir ?? process.cwd()); logger.info(`Installing app from ${dir}...`); - await installApp({ config, projectDir: dir, hapPath: opts.hap, logger }); + await installApp({ config, projectDir: dir, hapPath: opts.hap, deviceSerial: opts.device, logger }); logger.info('App installed.'); }); diff --git a/packages/cli/tests/app.test.ts b/packages/cli/tests/app.test.ts new file mode 100644 index 0000000..b79681f --- /dev/null +++ b/packages/cli/tests/app.test.ts @@ -0,0 +1,46 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import * as path from 'node:path'; +import { Command } from 'commander'; + +const mocks = vi.hoisted(() => ({ + installApp: vi.fn(), +})); + +vi.mock('@oniroproject/core', async () => { + const actual = await vi.importActual('@oniroproject/core'); + return { ...actual, installApp: mocks.installApp }; +}); + +import { noopLogger, noopProgress, staticConfig } from '@oniroproject/core'; +import { registerAppCommand } from '../src/commands/app.js'; +import { setRuntime } from '../src/lib/runtime.js'; + +describe('app install', () => { + beforeEach(() => { + mocks.installApp.mockReset(); + mocks.installApp.mockResolvedValue({ installed: true, bundleName: '', hapPath: '', output: '' }); + setRuntime({ config: staticConfig(), logger: noopLogger, progress: noopProgress }); + }); + + afterEach(() => { + setRuntime(null); + }); + + it('forwards --device to installApp', async () => { + const program = new Command(); + registerAppCommand(program); + + await program.parseAsync( + ['app', 'install', 'project', '--hap', 'app.hap', '--device', 'ABC123'], + { from: 'user' }, + ); + + expect(mocks.installApp).toHaveBeenCalledWith({ + config: expect.any(Object), + projectDir: path.resolve('project'), + hapPath: 'app.hap', + deviceSerial: 'ABC123', + logger: noopLogger, + }); + }); +}); diff --git a/packages/core/src/hdc/app.ts b/packages/core/src/hdc/app.ts index 30a5235..dfc10a2 100644 --- a/packages/core/src/hdc/app.ts +++ b/packages/core/src/hdc/app.ts @@ -19,6 +19,7 @@ export interface InstallAppOptions { * Falls back to the `hapPath` config key, then to the well-known default. */ hapPath?: string; + deviceSerial?: string; logger?: Logger; } @@ -41,7 +42,12 @@ export async function installApp(opts: InstallAppOptions): Promise staticConfig({ cmdToolsPath: root }); - it('installApp sends `install ` as discrete args', async () => { + it('installApp sends the target and `install ` as discrete args', async () => { const hapPath = path.join(projectDir, 'app.hap'); fs.writeFileSync(hapPath, 'fake'); - await installApp({ config: config(), projectDir, hapPath }); - // No throw means exit 0 from the fake hdc. (Output is logged, not returned in P1.) + const result = await installApp({ config: config(), projectDir, hapPath, deviceSerial: 'ABC123' }); + expect(result.output.split('\n').filter(Boolean)).toEqual(['-t', 'ABC123', 'install', hapPath]); }); it('launchApp passes a metacharacter-laden ability as ONE argv element (no host injection)', async () => {