Skip to content

Commit ad79461

Browse files
authored
refactor(commands): declare a command option once and derive its surfaces (#2421)
* refactor(commands): declare a command option once and derive its surfaces Collapses the pass-through hops the #2410 hop trace named, so an option is stated once and every surface reads that statement instead of a copy. - The snapshot option/flag pair (`customActions` <-> `snapshotCustomActions` and its seven siblings) is declared once in `@agent-device/kernel/snapshot` with both projections, generalising the shipped screenshot pair. The nine hand-written renames now read the declaration. - The dispatch pass-through flags are declared once as `DISPATCH_CONTEXT_FLAG_KEYS`; `DispatchContext` and the daemon's `contextFromFlags` both derive from it, so an identically-named flag no longer costs two files. The mapper keeps only what it decides. - The snapshot capture option keys are one type (`SnapshotCommandOptionFields`), referenced by the public SDK options, the internal request bag and the command runtime options instead of four copies. - An option's prose has one owner: its `FlagDefinition` now carries both audiences (`usageDescription` for --help, `inputDescription` for the tool and SDK field), and `optionField` derives the command's input field -- its JSON schema shape, bounds and description -- from that declaration. The duplicate doc comments in contracts are gone. Adds planted-divergence tests: a divergence planted in the one declaration must move the derived surface, which a second hand-written copy could not do. No help text, MCP tool schema or wire shape changes. * fix(contracts): re-pin SNAPSHOT_OPTION_FLAGS against CommandFlags The kernel declares SNAPSHOT_OPTION_FLAGS but sits below contracts and cannot name CommandFlags, so nothing pinned its values to real flag keys after the `satisfies Record<RoutedSnapshotOption, keyof CommandFlags>` guard on the old src/backend-snapshot-options.ts table was dropped. A misspelled or renamed flag key would compile and snapshotOptionsFromFlags would read `undefined` at every seam, silently dropping the option. Re-pin it in contracts, the lowest point that can name both. * fix(contracts): stop exporting the SNAPSHOT_OPTION_FLAGS pin The compile-time pin added against CommandFlags was exported, which the layering gate's facade-exhaustiveness check flagged: packages/contracts/src/ facades/command.ts re-exports command-flags.ts but omitted it, an accidental narrowing. The pin is not API -- nothing reads its value, it only exists to fail the build on a renamed or misspelled flag key. Keep it unexported and consume it with `void` so oxlint's no-unused-vars stays satisfied without widening the facade. * test(commands): plant the option divergence before the surfaces are built The planted-divergence tests called optionField after command metadata had already been constructed, so they proved only that optionField reads its own argument: a command that went back to a hand-written booleanField('...') would still have passed. The plant now lands on the option declaration in a fresh module graph BEFORE the builders run, and both derived surfaces -- the command metadata table and the MCP tool schema built from it -- are asserted against it. Also restores the public SDK documentation the branch dropped. AppOpenOptions .foreground and CaptureSnapshotOptions.customActions carry their JSDoc again: a .d.ts is read in an editor where no FlagDefinition resolves, and this repo has no step that generates those docs. The prose is the option's one declaration verbatim, and a unit test pins each SDK field's JSDoc to that declaration's inputDescription so the two cannot drift. * fix(kernel): stop bundling SNAPSHOT_OPTION_FLAGS' rationale into the public .d.ts Its JSDoc was internal refactor rationale, not SDK documentation, but rolldown-plugin-dts inlines it wherever CaptureSnapshotOptions resolves through SnapshotCommandOptionFields. Move it to a source comment (stripped from declaration output) and keep one short hover line. Cuts the packaged size growth from +3.3 kB/+2.2 kB to +2.3 kB/+1.8 kB (unpacked/download); no behavior change.
1 parent edbd96e commit ad79461

22 files changed

Lines changed: 655 additions & 188 deletions

‎packages/contracts/src/cli-flags.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ import type { ScreenshotRequestFlags } from './screenshot.ts';
2323
import type { RecordingScope } from './recording-scope.ts';
2424
import type { ReplayRequestFields } from './replay-request-fields.ts';
2525

26+
// This is the flag KEY vocabulary, not where an option is described: an
27+
// option's prose belongs to its one declaration (its `FlagDefinition`, which
28+
// carries both the `--help` and the tool/SDK audience), so a doc comment
29+
// repeated here would be a second copy that drifts. Comments below state only
30+
// facts this type alone knows — that a key has no CLI token, or how two keys
31+
// interact.
2632
export type CliFlags = CloudProviderProfileFields &
2733
RemoteConfigMetroOptions &
2834
ScreenshotRequestFlags &
@@ -120,11 +126,6 @@ export type CliFlags = CloudProviderProfileFields &
120126
saveScript?: boolean | string;
121127
shutdown?: boolean;
122128
relaunch?: boolean;
123-
/**
124-
* Include the initial interactive snapshot in a fresh open response. With
125-
* no app argument, iOS can discover the sole running app on the sole booted
126-
* simulator and fails closed when that environment is ambiguous.
127-
*/
128129
foreground?: boolean;
129130
surface?: SessionSurface;
130131
headless?: boolean;

‎packages/contracts/src/client-app.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,15 @@ export type AppOpenOptions = AgentDeviceRequestOverrides &
6868
relaunch?: boolean;
6969
/** Startup budget in milliseconds: bounds the Simulator boot wait on a cold device. */
7070
timeoutMs?: number;
71+
// Editor documentation for a public type: a `.d.ts` is read where no
72+
// FlagDefinition resolves, and nothing generates these docs. It is not a second
73+
// statement of what the option does — it is the option's ONE declaration (the
74+
// `--foreground` FlagDefinition's `inputDescription`) verbatim, pinned to it by
75+
// `commands/command-input-option-field.test.ts`.
7176
/**
72-
* Include the initial interactive snapshot in a fresh open response. With
73-
* no app argument, iOS can discover the sole running app on the sole booted
74-
* simulator and fails closed when that environment is ambiguous.
77+
* Include an initial interactive snapshot in a fresh open response. With no
78+
* app argument, discover the sole running app on the sole booted iOS
79+
* simulator; ambiguous environments fail closed.
7580
*/
7681
foreground?: boolean;
7782
saveScript?: boolean | string;

‎packages/contracts/src/client-capture.ts‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { SessionSurface } from './session-surface.ts';
44
import type { PublicSnapshotCaptureAnnotations } from './snapshot-capture-annotations.ts';
55
import type { SnapshotDiagnosticsSummary } from './snapshot-diagnostics.ts';
66
import type {
7+
SnapshotCommandOptionFields,
78
SnapshotNode,
89
SnapshotUnchanged,
910
SnapshotVisibility,
@@ -16,15 +17,26 @@ import type {
1617
DeviceCommandBaseOptions,
1718
} from './client-connection.ts';
1819

20+
// The snapshot capture keys and their value types come from the one snapshot
21+
// option declaration (`SnapshotCommandOptionFields`). `customActions` is lifted
22+
// out of that spread only to carry its editor documentation — a `.d.ts` is read
23+
// where no FlagDefinition resolves, and nothing generates these docs. Its type
24+
// still comes from the declaration, and the prose is the option's ONE
25+
// declaration (the `--actions` FlagDefinition's `inputDescription`) verbatim,
26+
// pinned to it by `commands/command-input-option-field.test.ts`.
1927
export type CaptureSnapshotOptions = AgentDeviceRequestOverrides &
20-
AgentDeviceSelectionOptions & {
21-
interactiveOnly?: boolean;
22-
depth?: number;
23-
scope?: string;
24-
raw?: boolean;
25-
/** List accessibility custom actions on merged elements (iOS simulator). */
26-
customActions?: boolean;
27-
forceFull?: boolean;
28+
AgentDeviceSelectionOptions &
29+
Omit<SnapshotCommandOptionFields, 'customActions'> & {
30+
/**
31+
* Name the affordances an element merged away (iOS UIAccessibilityCustomAction,
32+
* React Native accessibilityActions) — a card whose reply/options controls are not
33+
* separate elements still lists them here. The names are for PLANNING, not
34+
* invocation: there is no API to trigger them, so reach the affordance through the
35+
* element detail screen, through the same control exposed as a labeled element
36+
* elsewhere, or by coordinates from its rect. iOS simulator only; costs one
37+
* accessibility round trip per merged element.
38+
*/
39+
customActions?: SnapshotCommandOptionFields['customActions'];
2840
timeoutMs?: number;
2941
/**
3042
* #1271 stage 2 (ADR 0012 amendment): `snapshot` is observation-only and

‎packages/contracts/src/client-request.ts‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,20 @@ import type {
1414
NetworkIncludeMode,
1515
SessionRuntimeHints,
1616
} from '@agent-device/kernel/contracts';
17+
import type { SnapshotCommandOptionFields } from '@agent-device/kernel/snapshot';
1718
import type { DaemonBatchStep } from './batch-step.ts';
1819
import type { ReplayRequestFields } from './replay-request-fields.ts';
1920
import type { AgentDeviceClientConfig, AgentDeviceSelectionOptions } from './client-connection.ts';
2021

2122
export type CommandExecutionOptions = Partial<ScreenshotRequestFlags> &
22-
ReplayRequestFields & {
23+
ReplayRequestFields &
24+
SnapshotCommandOptionFields & {
2325
positionals?: string[];
2426
kind?: string;
2527
out?: string;
2628
artifact?: string;
2729
dsym?: string;
2830
searchPath?: string;
29-
interactiveOnly?: boolean;
30-
depth?: number;
31-
scope?: string;
32-
raw?: boolean;
33-
customActions?: boolean;
34-
forceFull?: boolean;
3531
count?: number;
3632
fps?: number;
3733
recordingScope?: RecordingScope;

‎packages/contracts/src/command-flags.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { CliFlags, DaemonExcludedCliFlag } from './cli-flags.ts';
22
import type { DaemonBatchStep } from './batch-step.ts';
3-
import type { Point } from '@agent-device/kernel/snapshot';
3+
import {
4+
SNAPSHOT_OPTION_FLAGS,
5+
type Point,
6+
type SnapshotOptionKey,
7+
} from '@agent-device/kernel/snapshot';
48

59
// The flag vocabulary a dispatched command is stated in terms of.
610
//
@@ -43,3 +47,14 @@ export type CommandFlags = Omit<CliFlags, DaemonExcludedCliFlag> & {
4347
shardCount?: number;
4448
shardIndex?: number;
4549
};
50+
51+
// Re-pins SNAPSHOT_OPTION_FLAGS (declared once in the kernel, which sits below contracts and
52+
// cannot name CommandFlags) against CommandFlags here: a renamed or misspelled flag key fails to
53+
// compile instead of silently reading `undefined` at every snapshotOptionsFromFlags call site.
54+
// This is a compile-time check, not API — it stays unexported so the command facade doesn't have
55+
// to widen just to carry it.
56+
const snapshotOptionFlagsPinnedToCommandFlags = SNAPSHOT_OPTION_FLAGS satisfies Record<
57+
SnapshotOptionKey,
58+
keyof CommandFlags
59+
>;
60+
void snapshotOptionFlagsPinnedToCommandFlags;

‎packages/kernel/src/snapshot.ts‎

Lines changed: 131 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,138 @@ export type SnapshotOptions = {
8787
customActions?: boolean;
8888
};
8989

90-
export type SnapshotPresentationFlagInput = {
91-
snapshotInteractiveOnly?: boolean;
92-
snapshotDepth?: number;
93-
snapshotScope?: string;
94-
snapshotRaw?: boolean;
95-
snapshotCustomActions?: boolean;
90+
// The snapshot capture family stated ONCE as option key ↔ command-flag key.
91+
//
92+
// The same pair (`customActions` ↔ `snapshotCustomActions`, and its seven
93+
// siblings) used to be re-typed by hand at every seam that carries a snapshot
94+
// request across the option/flag vocabulary line — the CLI reader, the client
95+
// option projection, the daemon capture inputs, the presentation key. Each copy
96+
// restated a fact already stated here and decided nothing, so a new snapshot
97+
// option cost one edit per seam and a missed seam dropped the option silently.
98+
//
99+
// Declared in the kernel because both vocabularies are declared here
100+
// (SnapshotOptions) and above (`CommandFlags` in contracts), so this is
101+
// the lowest point both sides can read. This generalises the shipped
102+
// `screenshotFlagsFromOptions`/`screenshotOptionsFromFlags` pair for the
103+
// screenshot family.
104+
//
105+
// Every projection takes an EXPLICIT key list: a seam admits the options it
106+
// routes and no more, so adding a pair here never silently widens a seam that
107+
// cannot honour it.
108+
//
109+
// Exported only because the exported types below say `typeof` it — this const
110+
// is not itself public API, so the rationale stays a source comment (stripped
111+
// from the published .d.ts) instead of a bundled JSDoc block.
112+
/** The CLI/daemon flag key for each snapshot capture option, by option name. */
113+
export const SNAPSHOT_OPTION_FLAGS = {
114+
interactiveOnly: 'snapshotInteractiveOnly',
115+
depth: 'snapshotDepth',
116+
scope: 'snapshotScope',
117+
raw: 'snapshotRaw',
118+
customActions: 'snapshotCustomActions',
119+
forceFull: 'snapshotForceFull',
120+
includeHiddenContentHints: 'snapshotIncludeHiddenContentHints',
121+
preferredBackend: 'snapshotPreferredBackend',
122+
} as const;
123+
124+
export type SnapshotOptionKey = keyof typeof SNAPSHOT_OPTION_FLAGS;
125+
126+
type SnapshotOptionValues = {
127+
interactiveOnly: boolean;
128+
depth: number;
129+
scope: string;
130+
raw: boolean;
131+
customActions: boolean;
132+
forceFull: boolean;
133+
includeHiddenContentHints: boolean;
134+
preferredBackend: SnapshotPreferredBackend;
96135
};
97136

137+
/** The option-vocabulary view of the declared pairs, narrowed to `TKeys`. */
138+
export type SnapshotOptionFields<TKeys extends SnapshotOptionKey = SnapshotOptionKey> = {
139+
[TKey in TKeys]?: SnapshotOptionValues[TKey];
140+
};
141+
142+
/** The flag-vocabulary view of the declared pairs, narrowed to `TKeys`. */
143+
export type SnapshotOptionFlagFields<TKeys extends SnapshotOptionKey = SnapshotOptionKey> = {
144+
[TKey in TKeys as (typeof SNAPSHOT_OPTION_FLAGS)[TKey]]?: SnapshotOptionValues[TKey];
145+
};
146+
147+
/**
148+
* Option keys a `snapshot`/`diff` command request carries end to end. `scope` is
149+
* resolved against the session before capture, so seams that resolve it spread
150+
* this projection and then override that one key.
151+
*/
152+
export const SNAPSHOT_COMMAND_OPTION_KEYS = [
153+
'interactiveOnly',
154+
'depth',
155+
'scope',
156+
'raw',
157+
'customActions',
158+
'forceFull',
159+
] as const;
160+
161+
/**
162+
* The snapshot capture options a `snapshot`/`diff` request is stated in, in
163+
* every vocabulary that names them: the public SDK type, the internal request
164+
* bag and the command runtime options each reference THIS type instead of
165+
* re-listing the same six keys.
166+
*/
167+
export type SnapshotCommandOptionFields = SnapshotOptionFields<
168+
(typeof SNAPSHOT_COMMAND_OPTION_KEYS)[number]
169+
>;
170+
171+
/** Option keys a daemon runtime capture input carries; `forceFull` is a command-level concern. */
172+
export const SNAPSHOT_CAPTURE_OPTION_KEYS = [
173+
'interactiveOnly',
174+
'preferredBackend',
175+
'depth',
176+
'scope',
177+
'raw',
178+
'customActions',
179+
'includeHiddenContentHints',
180+
] as const;
181+
182+
/** Option keys that identify a presentation; see {@link buildSnapshotPresentationKey}. */
183+
// fallow-ignore-next-line unused-export
184+
export const SNAPSHOT_PRESENTATION_OPTION_KEYS = [
185+
'depth',
186+
'interactiveOnly',
187+
'raw',
188+
'scope',
189+
'customActions',
190+
] as const;
191+
192+
/**
193+
* Reads the declared options out of a flags bag. Every requested key is present
194+
* (possibly `undefined`), matching what the hand-written copies produced.
195+
*/
196+
export function snapshotOptionsFromFlags<const TKeys extends readonly SnapshotOptionKey[]>(
197+
flags: SnapshotOptionFlagFields | undefined,
198+
keys: TKeys,
199+
): SnapshotOptionFields<TKeys[number]> {
200+
return Object.fromEntries(
201+
keys.map((key) => [key, flags?.[SNAPSHOT_OPTION_FLAGS[key]]]),
202+
) as SnapshotOptionFields<TKeys[number]>;
203+
}
204+
205+
/** Writes the declared options back into flag vocabulary, dropping absent values. */
206+
export function snapshotFlagsFromOptions<const TKeys extends readonly SnapshotOptionKey[]>(
207+
options: SnapshotOptionFields | undefined,
208+
keys: TKeys,
209+
): SnapshotOptionFlagFields<TKeys[number]> {
210+
return Object.fromEntries(
211+
keys.flatMap((key) => {
212+
const value = options?.[key];
213+
return value === undefined ? [] : [[SNAPSHOT_OPTION_FLAGS[key], value]];
214+
}),
215+
) as SnapshotOptionFlagFields<TKeys[number]>;
216+
}
217+
218+
export type SnapshotPresentationFlagInput = SnapshotOptionFlagFields<
219+
(typeof SNAPSHOT_PRESENTATION_OPTION_KEYS)[number]
220+
>;
221+
98222
export type RawSnapshotNode = {
99223
index: number;
100224
type?: string;
@@ -382,13 +506,7 @@ export function snapshotPresentationOptionsFromFlags(
382506
flags: SnapshotPresentationFlagInput | undefined,
383507
): SnapshotOptions | undefined {
384508
if (!flags) return undefined;
385-
return {
386-
depth: flags.snapshotDepth,
387-
interactiveOnly: flags.snapshotInteractiveOnly,
388-
raw: flags.snapshotRaw,
389-
scope: flags.snapshotScope,
390-
customActions: flags.snapshotCustomActions,
391-
};
509+
return snapshotOptionsFromFlags(flags, SNAPSHOT_PRESENTATION_OPTION_KEYS);
392510
}
393511

394512
export function centerOfRect(rect: Rect): Point {

‎src/backend-snapshot-options.ts‎

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
1-
import type { CommandFlags } from '@agent-device/contracts/command';
1+
import { snapshotFlagsFromOptions } from '@agent-device/kernel/snapshot';
22
import type { BackendSnapshotOptions } from './backend.ts';
33

44
type RoutedSnapshotOption = keyof Omit<BackendSnapshotOptions, 'includeRects' | 'outPath'>;
55

6-
const SNAPSHOT_OPTION_FLAGS = {
7-
interactiveOnly: 'snapshotInteractiveOnly',
8-
scope: 'snapshotScope',
9-
depth: 'snapshotDepth',
10-
raw: 'snapshotRaw',
11-
customActions: 'snapshotCustomActions',
12-
includeHiddenContentHints: 'snapshotIncludeHiddenContentHints',
13-
preferredBackend: 'snapshotPreferredBackend',
14-
} as const satisfies Record<RoutedSnapshotOption, keyof CommandFlags>;
6+
/**
7+
* Which backend snapshot options route as request flags. Still pinned to
8+
* `BackendSnapshotOptions`, so a new backend option must decide whether it
9+
* routes; the option→flag pairing itself is declared once in the kernel and is
10+
* no longer restated here.
11+
*/
12+
const ROUTED_SNAPSHOT_OPTIONS = {
13+
interactiveOnly: true,
14+
scope: true,
15+
depth: true,
16+
raw: true,
17+
customActions: true,
18+
includeHiddenContentHints: true,
19+
preferredBackend: true,
20+
} as const satisfies Record<RoutedSnapshotOption, true>;
1521

16-
type SnapshotFlagOverrides = Partial<
17-
Pick<CommandFlags, (typeof SNAPSHOT_OPTION_FLAGS)[RoutedSnapshotOption]>
22+
const ROUTED_SNAPSHOT_OPTION_KEYS = Object.keys(ROUTED_SNAPSHOT_OPTIONS) as ReadonlyArray<
23+
keyof typeof ROUTED_SNAPSHOT_OPTIONS
1824
>;
1925

20-
export function snapshotOptionsToFlags(
21-
options: BackendSnapshotOptions | undefined,
22-
): SnapshotFlagOverrides {
23-
if (!options) return {};
24-
return Object.fromEntries(
25-
Object.entries(SNAPSHOT_OPTION_FLAGS).flatMap(([optionKey, flagKey]) => {
26-
const value = options[optionKey as RoutedSnapshotOption];
27-
return value === undefined ? [] : [[flagKey, value]];
28-
}),
29-
) as SnapshotFlagOverrides;
26+
export function snapshotOptionsToFlags(options: BackendSnapshotOptions | undefined) {
27+
return snapshotFlagsFromOptions(options, ROUTED_SNAPSHOT_OPTION_KEYS);
3028
}

‎src/commands/capture/snapshot.ts‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
22
import { SNAPSHOT_BACKEND_CAPABILITIES } from '@agent-device/capture-kit/snapshot-quality-backend-capabilities';
3+
import {
4+
SNAPSHOT_COMMAND_OPTION_KEYS,
5+
snapshotOptionsFromFlags,
6+
} from '@agent-device/kernel/snapshot';
37
import { SNAPSHOT_FLAGS } from '../cli-grammar/flag-groups.ts';
4-
import { booleanField, integerField, stringField } from '../command-input.ts';
8+
import { booleanField, integerField, optionField, stringField } from '../command-input.ts';
59
import {
610
commonInputFromFlags,
711
direct,
@@ -32,9 +36,7 @@ const snapshotCommandMetadata = defineFieldCommandMetadata(
3236
depth: integerField(),
3337
scope: stringField(),
3438
raw: booleanField(),
35-
customActions: booleanField(
36-
'Name the affordances an element merged away (iOS UIAccessibilityCustomAction, React Native accessibilityActions) — a card whose reply/options controls are not separate elements still lists them here. The names are for PLANNING, not invocation: there is no API to trigger them, so reach the affordance through the element detail screen, through the same control exposed as a labeled element elsewhere, or by coordinates from its rect. iOS simulator only; costs one accessibility round trip per merged element.',
37-
),
39+
customActions: optionField('snapshotCustomActions'),
3840
forceFull: booleanField(),
3941
timeoutMs: integerField('Maximum wall-clock time for the snapshot command.'),
4042
// #1271 stage 2: `snapshot` is observation-only, so a repair-armed heal
@@ -64,12 +66,7 @@ const snapshotCliSchema = {
6466
export const snapshotCliReader: CliReader = (_positionals, flags) => ({
6567
...commonInputFromFlags(flags),
6668
...observationRecordInputFromFlags(flags),
67-
interactiveOnly: flags.snapshotInteractiveOnly,
68-
depth: flags.snapshotDepth,
69-
scope: flags.snapshotScope,
70-
raw: flags.snapshotRaw,
71-
customActions: flags.snapshotCustomActions,
72-
forceFull: flags.snapshotForceFull,
69+
...snapshotOptionsFromFlags(flags, SNAPSHOT_COMMAND_OPTION_KEYS),
7370
timeoutMs: flags.timeoutMs,
7471
});
7572

‎src/commands/cli-grammar/flag-definitions-action.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [
280280
usageLabel: '--foreground',
281281
usageDescription:
282282
'open: keep normal app/device selection and return an initial snapshot; without an app, resolve the sole running app on the sole booted iOS simulator',
283+
inputDescription:
284+
'Include an initial interactive snapshot in a fresh open response. With no app argument, discover the sole running app on the sole booted iOS simulator; ambiguous environments fail closed.',
283285
},
284286
{
285287
key: 'restart',

0 commit comments

Comments
 (0)