Skip to content

Commit ef27c41

Browse files
committed
fix: keep legacy cli batch steps working
1 parent a88264d commit ef27c41

6 files changed

Lines changed: 141 additions & 12 deletions

File tree

src/__tests__/cli-batch.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,26 @@ test('batch --steps-file parses file payload', async () => {
6262
assert.equal((req.flags?.batchSteps ?? [])[0]?.command, 'wait');
6363
});
6464

65+
test('batch accepts legacy positionals/flags steps with deprecation warning', async () => {
66+
const result = await runCliCapture([
67+
'batch',
68+
'--steps',
69+
'[{"command":"open","positionals":["settings"],"flags":{"platform":"ios"}}]',
70+
'--json',
71+
]);
72+
assert.equal(result.code, null);
73+
assert.match(result.stderr, /positionals\/flags are deprecated.*next major version/);
74+
assert.equal(result.calls.length, 1);
75+
const req = result.calls[0];
76+
assert.equal(req.command, 'batch');
77+
assert.deepEqual((req.flags?.batchSteps ?? [])[0], {
78+
command: 'open',
79+
positionals: ['settings'],
80+
flags: { platform: 'ios' },
81+
runtime: undefined,
82+
});
83+
});
84+
6585
test('batch --steps-file returns clear error for missing file', async () => {
6686
const result = await runCliCapture([
6787
'batch',

src/cli.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { materializeRemoteConnectionForCommand } from './cli/commands/connection-runtime.ts';
1515
import { tryRunClientBackedCommand } from './cli/commands/router.ts';
1616
import { runReactDevtoolsCommand } from './cli/commands/react-devtools.ts';
17+
import { readCliBatchStepsJson } from './cli/batch-steps.ts';
1718
import {
1819
createRequestId,
1920
emitDiagnostic,
@@ -384,16 +385,7 @@ function readBatchSteps(flags: ReturnType<typeof resolveCliOptions>['flags']): B
384385
);
385386
}
386387
}
387-
let parsed: unknown;
388-
try {
389-
parsed = JSON.parse(raw);
390-
} catch {
391-
throw new AppError('INVALID_ARGS', 'Batch steps must be valid JSON.');
392-
}
393-
if (!Array.isArray(parsed) || parsed.length === 0) {
394-
throw new AppError('INVALID_ARGS', 'Batch steps must be a non-empty JSON array.');
395-
}
396-
return parsed as BatchStep[];
388+
return readCliBatchStepsJson(raw);
397389
}
398390

399391
function isDaemonStartupFailure(error: AppError): boolean {

src/cli/batch-steps.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import type { BatchStep } from '../client-types.ts';
2+
import { readInputFromCli } from '../commands/cli-grammar.ts';
3+
import type { CliFlags } from '../utils/command-schema.ts';
4+
import { AppError } from '../utils/errors.ts';
5+
6+
type LegacyCliBatchStep = {
7+
command: string;
8+
positionals?: string[];
9+
flags?: Record<string, unknown>;
10+
runtime?: unknown;
11+
};
12+
13+
export function readCliBatchStepsJson(raw: string): BatchStep[] {
14+
let parsed: unknown;
15+
try {
16+
parsed = JSON.parse(raw);
17+
} catch {
18+
throw new AppError('INVALID_ARGS', 'Batch steps must be valid JSON.');
19+
}
20+
if (!Array.isArray(parsed) || parsed.length === 0) {
21+
throw new AppError('INVALID_ARGS', 'Batch steps must be a non-empty JSON array.');
22+
}
23+
return normalizeCliBatchSteps(parsed);
24+
}
25+
26+
function normalizeCliBatchSteps(steps: unknown[]): BatchStep[] {
27+
let sawLegacyStep = false;
28+
const normalized = steps.map((step, index) => {
29+
if (isStructuredBatchStep(step)) return step;
30+
const legacyStep = readLegacyCliBatchStep(step, index + 1);
31+
sawLegacyStep = true;
32+
return legacyStepToStructuredStep(legacyStep);
33+
});
34+
if (sawLegacyStep) {
35+
process.stderr.write(
36+
'Warning: batch steps using positionals/flags are deprecated and will be removed in the next major version. Use {"command":"...","input":{...}} steps instead.\n',
37+
);
38+
}
39+
return normalized;
40+
}
41+
42+
function legacyStepToStructuredStep(legacyStep: LegacyCliBatchStep): BatchStep {
43+
const input = readInputFromCli(
44+
legacyStep.command,
45+
legacyStep.positionals ?? [],
46+
cliFlagsFromBatchStep(legacyStep.flags),
47+
);
48+
return {
49+
command: legacyStep.command,
50+
input,
51+
...(legacyStep.runtime === undefined ? {} : { runtime: legacyStep.runtime }),
52+
};
53+
}
54+
55+
function isStructuredBatchStep(step: unknown): step is BatchStep {
56+
return step !== null && typeof step === 'object' && !Array.isArray(step) && 'input' in step;
57+
}
58+
59+
function readLegacyCliBatchStep(step: unknown, stepNumber: number): LegacyCliBatchStep {
60+
if (!step || typeof step !== 'object' || Array.isArray(step)) {
61+
throw new AppError('INVALID_ARGS', `Invalid batch step ${stepNumber}.`);
62+
}
63+
const record = step as Record<string, unknown>;
64+
assertLegacyBatchStepKeys(record, stepNumber);
65+
const command = typeof record.command === 'string' ? record.command.trim().toLowerCase() : '';
66+
if (!command) throw new AppError('INVALID_ARGS', `Batch step ${stepNumber} requires command.`);
67+
const positionals = readLegacyPositionals(record.positionals, stepNumber);
68+
const flags = readLegacyFlags(record.flags, stepNumber);
69+
return {
70+
command,
71+
...(positionals === undefined ? {} : { positionals }),
72+
...(flags === undefined ? {} : { flags }),
73+
...(record.runtime === undefined ? {} : { runtime: record.runtime }),
74+
};
75+
}
76+
77+
function assertLegacyBatchStepKeys(record: Record<string, unknown>, stepNumber: number): void {
78+
const unknownKeys = Object.keys(record).filter(
79+
(key) => !['command', 'positionals', 'flags', 'runtime'].includes(key),
80+
);
81+
if (unknownKeys.length > 0) {
82+
throw new AppError(
83+
'INVALID_ARGS',
84+
`Batch step ${stepNumber} has unknown legacy field(s): ${unknownKeys.join(', ')}.`,
85+
);
86+
}
87+
}
88+
89+
function readLegacyPositionals(value: unknown, stepNumber: number): string[] | undefined {
90+
if (value === undefined) return undefined;
91+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
92+
throw new AppError(
93+
'INVALID_ARGS',
94+
`Batch step ${stepNumber} positionals must contain only strings.`,
95+
);
96+
}
97+
return value;
98+
}
99+
100+
function readLegacyFlags(value: unknown, stepNumber: number): Record<string, unknown> | undefined {
101+
if (value === undefined) return undefined;
102+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
103+
throw new AppError('INVALID_ARGS', `Batch step ${stepNumber} flags must be an object.`);
104+
}
105+
return value as Record<string, unknown>;
106+
}
107+
108+
function cliFlagsFromBatchStep(flags: Record<string, unknown> | undefined): CliFlags {
109+
return {
110+
json: false,
111+
help: false,
112+
version: false,
113+
...(flags as Partial<CliFlags> | undefined),
114+
};
115+
}

src/utils/cli-help.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const AGENT_QUICKSTART_LINES = [
4848
'Clipboard limits: iOS Allow Paste cannot be automated through XCUITest; prefill with clipboard write. Android non-ASCII should use fill/type, not raw adb input.',
4949
'After mutation: refs are stale. If the next target is known, use its selector directly; otherwise refresh with snapshot -i, scoped with -s when a stable container is known.',
5050
'Raw coordinates are fallback-only: use snapshot -i -c --json rects when iOS refs no-op or child refs are missing.',
51-
'Batch JSON steps use "command" and structured "input"; never "positionals", "flags", "args", or "step".',
51+
'Batch JSON steps use "command" and structured "input"; legacy "positionals"/"flags" steps still run in CLI but are deprecated until the next major version.',
5252
'Navigation: app-owned back uses back; system back uses back --system.',
5353
'Verification commands must name the expected text/selector; bare screenshots/snapshots are not enough.',
5454
'Debug evidence: logs clear --restart/mark/path; trace start ./path; trace stop ./path; network dump --include headers.',
@@ -206,7 +206,7 @@ Validation and evidence:
206206
Stable known flow: batch ./steps.json, not workflow batch.
207207
Inline batch JSON example:
208208
agent-device batch --steps '[{"command":"open","input":{"app":"settings"}},{"command":"wait","input":{"kind":"duration","durationMs":100}}]'
209-
Batch step keys are command, input, and optional runtime. Put command arguments inside input using the same fields as the MCP/Node command.
209+
Batch step keys are command, input, and optional runtime. Put command arguments inside input using the same fields as the MCP/Node command. CLI still accepts legacy positionals/flags steps with a deprecation warning until the next major version.
210210
Android animations: settings animations off/on, not animations disable/restore.
211211
Debug logs: logs clear --restart, logs mark, reproduce, then logs path; do not split clear/restart into separate stop/start commands.
212212
Network headers: network dump --include headers; do not write network log headers.

website/docs/docs/batching.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Notes:
5353

5454
- `input` is required and uses the same fields as the matching MCP/Node command.
5555
- Unknown top-level step fields are rejected. Supported keys are `command`, `input`, and `runtime`.
56+
- CLI `--steps` and `--steps-file` still accept the legacy `positionals`/`flags` step shape with a deprecation warning. That compatibility path will be removed in the next major version.
5657
- nested `batch` and `replay` steps are rejected.
5758
- `--on-error stop` is the supported behavior.
5859

website/docs/docs/commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ agent-device batch --steps '[{"command":"open","input":{"app":"settings"}}]'
362362
- `batch` runs a JSON array of steps in a single daemon request.
363363
- Each step has `command`, `input`, and optional `runtime`.
364364
- `input` uses the same fields as the matching MCP/Node command.
365+
- Legacy CLI step payloads with `positionals`/`flags` still run with a deprecation warning and will be removed in the next major version.
365366
- Unknown top-level step fields are rejected.
366367
- Stop-on-first-error is the supported behavior (`--on-error stop`).
367368
- Use `--max-steps <n>` to tighten per-request safety limits.

0 commit comments

Comments
 (0)