Skip to content

Commit a4f61ae

Browse files
committed
Refactor CLI command schema and strict flag validation
1 parent 37d1faf commit a4f61ae

6 files changed

Lines changed: 880 additions & 242 deletions

File tree

src/cli.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parseArgs, usage } from './utils/args.ts';
1+
import { parseArgs, toDaemonFlags, usage } from './utils/args.ts';
22
import { asAppError, AppError } from './utils/errors.ts';
33
import { formatSnapshotText, printHumanError, printJson } from './utils/output.ts';
44
import { readVersion } from './utils/version.ts';
@@ -10,6 +10,9 @@ import path from 'node:path';
1010

1111
export async function runCli(argv: string[]): Promise<void> {
1212
const parsed = parseArgs(argv);
13+
for (const warning of parsed.warnings) {
14+
process.stderr.write(`Warning: ${warning}\n`);
15+
}
1316

1417
if (parsed.flags.version) {
1518
process.stdout.write(`${readVersion()}\n`);
@@ -22,6 +25,7 @@ export async function runCli(argv: string[]): Promise<void> {
2225
}
2326

2427
const { command, positionals, flags } = parsed;
28+
const daemonFlags = toDaemonFlags(flags);
2529
const sessionName = flags.session ?? process.env.AGENT_DEVICE_SESSION ?? 'default';
2630
const logTailStopper = flags.verbose && !flags.json ? startDaemonLogTail() : null;
2731
try {
@@ -34,7 +38,7 @@ export async function runCli(argv: string[]): Promise<void> {
3438
session: sessionName,
3539
command: 'session_list',
3640
positionals: [],
37-
flags: {},
41+
flags: daemonFlags,
3842
});
3943
if (!response.ok) throw new AppError(response.error.code as any, response.error.message);
4044
if (flags.json) printJson({ success: true, data: response.data ?? {} });
@@ -47,7 +51,7 @@ export async function runCli(argv: string[]): Promise<void> {
4751
session: sessionName,
4852
command: command!,
4953
positionals,
50-
flags,
54+
flags: daemonFlags,
5155
});
5256

5357
if (response.ok) {

src/core/capabilities.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,7 @@ export function isCommandSupportedOnDevice(command: string, device: DeviceInfo):
4949
const kind = (device.kind ?? 'unknown') as keyof KindMatrix;
5050
return byPlatform[kind] === true;
5151
}
52+
53+
export function listCapabilityCommands(): string[] {
54+
return Object.keys(COMMAND_CAPABILITY_MATRIX).sort();
55+
}

src/core/dispatch.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,9 @@ import { runIosRunnerCommand } from '../platforms/ios/runner-client.ts';
1717
import { snapshotAx } from '../platforms/ios/ax-snapshot.ts';
1818
import { setIosSetting } from '../platforms/ios/index.ts';
1919
import type { RawSnapshotNode } from '../utils/snapshot.ts';
20+
import type { DaemonFlags } from '../utils/command-schema.ts';
2021

21-
export type CommandFlags = {
22-
session?: string;
23-
platform?: 'ios' | 'android';
24-
device?: string;
25-
udid?: string;
26-
serial?: string;
27-
out?: string;
28-
activity?: string;
29-
verbose?: boolean;
30-
snapshotInteractiveOnly?: boolean;
31-
snapshotCompact?: boolean;
32-
snapshotDepth?: number;
33-
snapshotScope?: string;
34-
snapshotRaw?: boolean;
35-
snapshotBackend?: 'ax' | 'xctest';
36-
saveScript?: boolean;
37-
relaunch?: boolean;
38-
noRecord?: boolean;
39-
appsFilter?: 'launchable' | 'user-installed' | 'all';
40-
appsMetadata?: boolean;
41-
replayUpdate?: boolean;
42-
};
22+
export type CommandFlags = DaemonFlags;
4323

4424
export async function resolveTargetDevice(flags: CommandFlags): Promise<DeviceInfo> {
4525
const selector = {

src/utils/__tests__/args.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import test from 'node:test';
22
import assert from 'node:assert/strict';
33
import { parseArgs, usage } from '../args.ts';
4+
import { AppError } from '../errors.ts';
5+
import { getCliCommandNames } from '../command-schema.ts';
6+
import { listCapabilityCommands } from '../../core/capabilities.ts';
47

58
test('parseArgs recognizes --relaunch', () => {
69
const parsed = parseArgs(['open', 'settings', '--relaunch']);
@@ -11,4 +14,69 @@ test('parseArgs recognizes --relaunch', () => {
1114

1215
test('usage includes --relaunch flag', () => {
1316
assert.match(usage(), /--relaunch/);
17+
assert.match(usage(), /pinch <scale> \[x\] \[y\]/);
18+
assert.match(usage(), /--metadata/);
19+
});
20+
21+
test('every capability command has a parser schema entry', () => {
22+
const schemaCommands = new Set(getCliCommandNames());
23+
for (const command of listCapabilityCommands()) {
24+
assert.equal(schemaCommands.has(command), true, `Missing schema for command: ${command}`);
25+
}
26+
});
27+
28+
test('compat mode warns and strips unsupported pilot-command flags', () => {
29+
const parsed = parseArgs(['press', '10', '20', '--depth', '2'], { strictFlags: false });
30+
assert.equal(parsed.command, 'press');
31+
assert.equal(parsed.flags.snapshotDepth, undefined);
32+
assert.equal(parsed.warnings.length, 1);
33+
assert.match(parsed.warnings[0], /not supported for command press/);
34+
});
35+
36+
test('strict mode rejects unsupported pilot-command flags', () => {
37+
assert.throws(
38+
() => parseArgs(['press', '10', '20', '--depth', '2'], { strictFlags: true }),
39+
(error) =>
40+
error instanceof AppError &&
41+
error.code === 'INVALID_ARGS' &&
42+
error.message.includes('not supported for command press'),
43+
);
44+
});
45+
46+
test('snapshot command accepts command-specific flags', () => {
47+
const parsed = parseArgs(['snapshot', '-i', '-c', '--depth', '3', '-s', 'Login'], { strictFlags: true });
48+
assert.equal(parsed.command, 'snapshot');
49+
assert.equal(parsed.flags.snapshotInteractiveOnly, true);
50+
assert.equal(parsed.flags.snapshotCompact, true);
51+
assert.equal(parsed.flags.snapshotDepth, 3);
52+
assert.equal(parsed.flags.snapshotScope, 'Login');
53+
});
54+
55+
test('unknown short flags are rejected', () => {
56+
assert.throws(
57+
() => parseArgs(['press', '10', '20', '-x'], { strictFlags: true }),
58+
(error) => error instanceof AppError && error.code === 'INVALID_ARGS' && error.message === 'Unknown flag: -x',
59+
);
60+
});
61+
62+
test('all commands participate in strict command-flag validation', () => {
63+
assert.throws(
64+
() => parseArgs(['open', 'Settings', '--depth', '1'], { strictFlags: true }),
65+
(error) =>
66+
error instanceof AppError &&
67+
error.code === 'INVALID_ARGS' &&
68+
error.message.includes('not supported for command open'),
69+
);
70+
});
71+
72+
test('invalid enum/range errors are deterministic', () => {
73+
assert.throws(
74+
() => parseArgs(['snapshot', '--backend', 'foo'], { strictFlags: true }),
75+
(error) =>
76+
error instanceof AppError && error.code === 'INVALID_ARGS' && error.message === 'Invalid backend: foo',
77+
);
78+
assert.throws(
79+
() => parseArgs(['snapshot', '--depth', '-1'], { strictFlags: true }),
80+
(error) => error instanceof AppError && error.code === 'INVALID_ARGS' && error.message === 'Invalid depth: -1',
81+
);
1482
});

0 commit comments

Comments
 (0)