Skip to content

Commit 6845db9

Browse files
committed
refactor: split command projection from cli grammar
1 parent a9f6835 commit 6845db9

7 files changed

Lines changed: 109 additions & 91 deletions

File tree

AGENTS.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ Single-context repo. Read `CONTEXT.md` for domain language and testing/architect
7171
- command surface and shared schemas: `src/commands/command-surface.ts`, `src/commands/command-contract.ts`, `src/commands/command-input.ts`
7272
- typed client command execution: `src/commands/client-command-contracts.ts`
7373
- command families: `src/commands/interaction-command-contracts.ts`, `src/commands/batch-command-contract.ts`, with other typed client contracts in `src/commands/client-command-contracts.ts`
74-
- CLI positional/flag grammar and daemon request projection: `src/commands/cli-grammar.ts` and `src/commands/cli-grammar/*`
74+
- CLI positional/flag grammar: `src/commands/cli-grammar.ts` and `src/commands/cli-grammar/*`
75+
- typed input to daemon request projection: `src/commands/command-projection.ts`
7576
- CLI/client/runtime output projection: `src/commands/cli-output.ts`, `src/commands/client-output.ts`, `src/commands/runtime-output.ts`
7677
- Do not reintroduce CLI-shaped command adapters or schemas as a second source of truth. CLI, Node.js, and MCP should project from command contracts.
7778
- Keep `src/daemon/request-router.ts` as request orchestration: auth, diagnostics scope, request admission, locking, handler chain, and fallback dispatch.
@@ -130,12 +131,13 @@ Single-context repo. Read `CONTEXT.md` for domain language and testing/architect
130131
A new snapshot/command flag touches only the layers that need to understand it. Follow this checklist in order:
131132

132133
1. `src/utils/cli-flags.ts`: add to `CliFlags`, `FLAG_DEFINITIONS`, and the relevant exported flag group (e.g. `SNAPSHOT_FLAGS`). Add the flag to `CLI_COMMAND_OVERRIDES` in `src/utils/cli-command-overrides.ts` for each command that supports it; command names/descriptions come from command contracts unless CLI help needs a specific override.
133-
2. `src/commands/cli-grammar/*`: read the CLI flag into command input and write it into the daemon request only if the flag affects daemon execution.
134-
3. `src/commands/*-command-contracts.ts`: add or update the command input schema only if the option should be available through Node.js or MCP as structured input.
135-
4. `src/client-types.ts`: update the public typed client option only when the Node.js interface exposes the option.
136-
5. `src/client-normalizers.ts`: update daemon flag normalization only when the request still needs a public-to-internal option translation.
137-
6. `src/daemon/context.ts` and `src/core/dispatch-context.ts`: add the field only when it flows into platform dispatch.
138-
7. Handler/platform modules: thread the option only after the command surface and grammar prove it belongs there.
134+
2. `src/commands/cli-grammar/*`: read the CLI flag into command input when the CLI accepts it.
135+
3. `src/commands/command-projection.ts` and command-family projection helpers: write the input into the daemon request only if the flag affects daemon execution.
136+
4. `src/commands/*-command-contracts.ts`: add or update the command input schema only if the option should be available through Node.js or MCP as structured input.
137+
5. `src/client-types.ts`: update the public typed client option only when the Node.js interface exposes the option.
138+
6. `src/client-normalizers.ts`: update daemon flag normalization only when the request still needs a public-to-internal option translation.
139+
7. `src/daemon/context.ts` and `src/core/dispatch-context.ts`: add the field only when it flows into platform dispatch.
140+
8. Handler/platform modules: thread the option only after the command surface, grammar, and projection prove it belongs there.
139141

140142
Command-only flags (like `find --first`) that do not flow to the platform layer usually stop at steps 1-3.
141143

@@ -274,7 +276,8 @@ Command-only flags (like `find --first`) that do not flow to the platform layer
274276
- Request routing/policy: `src/daemon/request-router.ts`, `src/daemon/request-admission.ts`, `src/daemon/request-generic-dispatch.ts`
275277
- Dispatcher + capability map: `src/core/dispatch.ts`, `src/core/dispatch-context.ts`, `src/core/dispatch-interactions.ts`, `src/core/capabilities.ts`
276278
- Command catalog + command surface: `src/command-catalog.ts`, `src/commands/command-surface.ts`, `src/commands/command-contract.ts`, `src/commands/client-command-contracts.ts`
277-
- CLI grammar + daemon request projection: `src/commands/cli-grammar.ts`, `src/commands/cli-grammar/*`
279+
- CLI grammar: `src/commands/cli-grammar.ts`, `src/commands/cli-grammar/*`
280+
- Daemon request projection: `src/commands/command-projection.ts`
278281
- Platform backends: `src/platforms/ios/*`, `ios-runner/*`, `src/platforms/android/*`
279282

280283
## Pull Requests

src/client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { sendToDaemon } from './daemon-client.ts';
22
import { prepareMetroRuntime, reloadMetro } from './client-metro.ts';
33
import { INTERNAL_COMMANDS } from './command-catalog.ts';
4-
import { prepareDaemonCommandRequest, type DaemonCommandName } from './commands/cli-grammar.ts';
4+
import {
5+
prepareDaemonCommandRequest,
6+
type DaemonCommandName,
7+
} from './commands/command-projection.ts';
58
import { throwDaemonError } from './daemon-error.ts';
69
import {
710
buildFlags,

src/commands/batch-command-contract.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { BatchRunOptions, BatchStep } from '../client-types.ts';
22
import { DEFAULT_BATCH_MAX_STEPS, validateAndNormalizeBatchSteps } from '../core/batch.ts';
33
import { defineCommand, type JsonSchema } from './command-contract.ts';
4-
import { prepareBatchStep, type DaemonCommandName } from './cli-grammar.ts';
4+
import { prepareBatchStep, type DaemonCommandName } from './command-projection.ts';
55
import {
66
assertAllowedKeys,
77
commonToClientOptions,

src/commands/cli-grammar.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
export { readInputFromCli } from './cli-grammar/registry.ts';
12
export {
23
prepareBatchStep,
34
prepareDaemonCommandRequest,
4-
readInputFromCli,
55
batchCommandNames,
66
type BatchCommandName,
77
type DaemonCommandName,
8-
} from './cli-grammar/registry.ts';
8+
} from './command-projection.ts';

src/commands/cli-grammar/registry.ts

Lines changed: 11 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
1-
import { PUBLIC_COMMANDS } from '../../command-catalog.ts';
2-
import { buildFlags } from '../../client-normalizers.ts';
31
import type { BatchStep } from '../../client-types.ts';
42
import type { CliFlags } from '../../utils/command-schema.ts';
53
import { AppError } from '../../utils/errors.ts';
6-
import { appCliReaders, appDaemonWriters } from './apps.ts';
7-
import { captureCliReaders, captureDaemonWriters } from './capture.ts';
8-
import { commandNameSet, commonInputFromFlags, request } from './common.ts';
9-
import { gestureCliReaders, gestureDaemonWriters } from './gesture.ts';
10-
import { interactionCliReaders, interactionDaemonWriters } from './interactions.ts';
4+
import { isBatchCommandName, type BatchCommandName } from '../command-projection.ts';
5+
import { appCliReaders } from './apps.ts';
6+
import { captureCliReaders } from './capture.ts';
7+
import { commonInputFromFlags } from './common.ts';
8+
import { gestureCliReaders } from './gesture.ts';
9+
import { interactionCliReaders } from './interactions.ts';
1110
import { metroCliReaders } from './metro.ts';
12-
import { observabilityCliReaders, observabilityDaemonWriters } from './observability.ts';
13-
import { replayCliReaders, replayDaemonWriters } from './replay.ts';
14-
import { selectorCliReaders, selectorDaemonWriters } from './selectors.ts';
15-
import { systemCliReaders, systemDaemonWriters } from './system.ts';
16-
import type { CliReader, DaemonWriter, DaemonCommandRequest, CommandInput } from './types.ts';
11+
import { observabilityCliReaders } from './observability.ts';
12+
import { replayCliReaders } from './replay.ts';
13+
import { selectorCliReaders } from './selectors.ts';
14+
import { systemCliReaders } from './system.ts';
15+
import type { CliReader } from './types.ts';
1716

1817
const cliReaders = {
1918
...appCliReaders,
@@ -34,51 +33,6 @@ const cliReaders = {
3433
}),
3534
} satisfies Record<string, CliReader>;
3635

37-
const daemonWriters = {
38-
...appDaemonWriters,
39-
...captureDaemonWriters,
40-
...interactionDaemonWriters,
41-
...gestureDaemonWriters,
42-
...selectorDaemonWriters,
43-
...observabilityDaemonWriters,
44-
...replayDaemonWriters,
45-
...systemDaemonWriters,
46-
batch: (input) =>
47-
request(PUBLIC_COMMANDS.batch, [], {
48-
...input,
49-
batchSteps: input.steps,
50-
batchOnError: input.onError,
51-
batchMaxSteps: input.maxSteps,
52-
}),
53-
} satisfies Record<string, DaemonWriter>;
54-
55-
export type DaemonCommandName = keyof typeof daemonWriters;
56-
type NonBatchCommandName =
57-
| 'replay'
58-
| 'batch'
59-
| 'gesture-pan'
60-
| 'gesture-fling'
61-
| 'gesture-pinch'
62-
| 'gesture-rotate'
63-
| 'gesture-transform';
64-
export type BatchCommandName = Exclude<DaemonCommandName, NonBatchCommandName>;
65-
66-
const nonBatchCommandNames = commandNameSet([
67-
'replay',
68-
'batch',
69-
'gesture-pan',
70-
'gesture-fling',
71-
'gesture-pinch',
72-
'gesture-rotate',
73-
'gesture-transform',
74-
] as const satisfies readonly NonBatchCommandName[]);
75-
76-
export const batchCommandNames = (Object.keys(daemonWriters) as DaemonCommandName[]).filter(
77-
(name): name is BatchCommandName => !nonBatchCommandNames.has(name),
78-
);
79-
80-
const batchNames = commandNameSet(batchCommandNames);
81-
8236
export function readInputFromCli(
8337
command: string,
8438
positionals: string[],
@@ -89,23 +43,6 @@ export function readInputFromCli(
8943
return reader(positionals, flags);
9044
}
9145

92-
export function prepareBatchStep(command: DaemonCommandName, input: CommandInput): BatchStep {
93-
const prepared = prepareDaemonCommandRequest(command, input);
94-
return {
95-
command: prepared.command,
96-
positionals: prepared.positionals,
97-
flags: buildFlags(prepared.options),
98-
runtime: prepared.options.runtime,
99-
};
100-
}
101-
102-
export function prepareDaemonCommandRequest(
103-
command: DaemonCommandName,
104-
input: CommandInput,
105-
): DaemonCommandRequest {
106-
return daemonWriters[command](input);
107-
}
108-
10946
function readBatchStepsFromCli(
11047
steps: BatchStep[],
11148
): Array<{ command: string; input: Record<string, unknown> }> {
@@ -130,10 +67,6 @@ function readBatchCliCommand(command: string, stepNumber: number): BatchCommandN
13067
);
13168
}
13269

133-
function isBatchCommandName(name: string): name is BatchCommandName {
134-
return batchNames.has(name);
135-
}
136-
13770
function cliFlagsFromBatchStep(flags: BatchStep['flags']): CliFlags {
13871
return {
13972
json: false,

src/commands/command-projection.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { PUBLIC_COMMANDS } from '../command-catalog.ts';
2+
import { buildFlags } from '../client-normalizers.ts';
3+
import type { BatchStep } from '../client-types.ts';
4+
import { appDaemonWriters } from './cli-grammar/apps.ts';
5+
import { captureDaemonWriters } from './cli-grammar/capture.ts';
6+
import { commandNameSet, request } from './cli-grammar/common.ts';
7+
import { gestureDaemonWriters } from './cli-grammar/gesture.ts';
8+
import { interactionDaemonWriters } from './cli-grammar/interactions.ts';
9+
import { observabilityDaemonWriters } from './cli-grammar/observability.ts';
10+
import { replayDaemonWriters } from './cli-grammar/replay.ts';
11+
import { selectorDaemonWriters } from './cli-grammar/selectors.ts';
12+
import { systemDaemonWriters } from './cli-grammar/system.ts';
13+
import type { CommandInput, DaemonCommandRequest, DaemonWriter } from './cli-grammar/types.ts';
14+
15+
const daemonWriters = {
16+
...appDaemonWriters,
17+
...captureDaemonWriters,
18+
...interactionDaemonWriters,
19+
...gestureDaemonWriters,
20+
...selectorDaemonWriters,
21+
...observabilityDaemonWriters,
22+
...replayDaemonWriters,
23+
...systemDaemonWriters,
24+
batch: (input) =>
25+
request(PUBLIC_COMMANDS.batch, [], {
26+
...input,
27+
batchSteps: input.steps,
28+
batchOnError: input.onError,
29+
batchMaxSteps: input.maxSteps,
30+
}),
31+
} satisfies Record<string, DaemonWriter>;
32+
33+
export type DaemonCommandName = keyof typeof daemonWriters;
34+
type NonBatchCommandName =
35+
| 'replay'
36+
| 'batch'
37+
| 'gesture-pan'
38+
| 'gesture-fling'
39+
| 'gesture-pinch'
40+
| 'gesture-rotate'
41+
| 'gesture-transform';
42+
export type BatchCommandName = Exclude<DaemonCommandName, NonBatchCommandName>;
43+
44+
const nonBatchCommandNames = commandNameSet([
45+
'replay',
46+
'batch',
47+
'gesture-pan',
48+
'gesture-fling',
49+
'gesture-pinch',
50+
'gesture-rotate',
51+
'gesture-transform',
52+
] as const satisfies readonly NonBatchCommandName[]);
53+
54+
export const batchCommandNames = (Object.keys(daemonWriters) as DaemonCommandName[]).filter(
55+
(name): name is BatchCommandName => !nonBatchCommandNames.has(name),
56+
);
57+
58+
const batchNames = commandNameSet(batchCommandNames);
59+
60+
export function isBatchCommandName(name: string): name is BatchCommandName {
61+
return batchNames.has(name);
62+
}
63+
64+
export function prepareBatchStep(command: DaemonCommandName, input: CommandInput): BatchStep {
65+
const prepared = prepareDaemonCommandRequest(command, input);
66+
return {
67+
command: prepared.command,
68+
positionals: prepared.positionals,
69+
flags: buildFlags(prepared.options),
70+
runtime: prepared.options.runtime,
71+
};
72+
}
73+
74+
export function prepareDaemonCommandRequest(
75+
command: DaemonCommandName,
76+
input: CommandInput,
77+
): DaemonCommandRequest {
78+
return daemonWriters[command](input);
79+
}

src/commands/command-surface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createBatchCommand } from './batch-command-contract.ts';
44
import { clientCommandDefinitions } from './client-command-contracts.ts';
55
import type { JsonSchema } from './command-contract.ts';
66
import { interactionCommandDefinitions } from './interaction-command-contracts.ts';
7-
import { batchCommandNames, type BatchCommandName } from './cli-grammar.ts';
7+
import { batchCommandNames, type BatchCommandName } from './command-projection.ts';
88

99
type AnyExecutableCommand = {
1010
name: string;

0 commit comments

Comments
 (0)