Skip to content

Commit fcbf267

Browse files
committed
refactor: share semantic cli output projections
1 parent 5d7cd2f commit fcbf267

7 files changed

Lines changed: 289 additions & 161 deletions

File tree

src/cli/commands/generic.ts

Lines changed: 30 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,18 @@
1-
import {
2-
serializeCloseResult,
3-
serializeDeployResult,
4-
serializeDevice,
5-
serializeInstallFromSourceResult,
6-
serializeOpenResult,
7-
serializeSessionListEntry,
8-
serializeSnapshotResult,
9-
} from '../../client-shared.ts';
10-
import type {
11-
AgentDeviceClient,
12-
AgentDeviceDevice,
13-
AgentDeviceSession,
14-
AppCloseResult,
15-
AppDeployResult,
16-
AppInstallFromSourceResult,
17-
AppOpenResult,
18-
CaptureSnapshotResult,
19-
CommandRequestResult,
20-
SessionCloseResult,
21-
} from '../../client.ts';
1+
import type { AgentDeviceClient, CommandRequestResult } from '../../client.ts';
222
import { announceReplayTestRun } from '../../cli-test.ts';
23-
import { runSemanticCliCommand } from '../../commands/semantic-cli.ts';
243
import {
4+
runSemanticCliCommand,
5+
runSemanticCliCommandWithOutput,
6+
} from '../../commands/semantic-cli.ts';
7+
import {
8+
listSemanticCliOutputCommandNames,
259
listSemanticCommandNames,
2610
type SemanticCliCommand,
2711
} from '../../commands/semantic-command-surface.ts';
28-
import { assertResolvedAppsFilter } from '../../commands/app-inventory-contract.ts';
12+
import type { SemanticCliOutput } from '../../commands/semantic-contract.ts';
2913
import type { CliFlags } from '../../utils/command-schema.ts';
30-
import { AppError } from '../../utils/errors.ts';
31-
import { formatSnapshotText } from '../../utils/output.ts';
3214
import { writeCommandCliOutput } from './output.ts';
33-
import { writeCommandMessage, writeCommandOutput } from './shared.ts';
15+
import { writeCommandOutput } from './shared.ts';
3416
import type { PublicCommandName } from '../../command-catalog.ts';
3517
import type { ClientCommandHandler } from './router-types.ts';
3618

@@ -40,101 +22,12 @@ type GenericClientCommandRunner = (params: {
4022
flags: CliFlags;
4123
}) => Promise<CommandRequestResult>;
4224

43-
const formattedSemanticCommandHandlers = {
44-
devices: createFormattedSemanticHandler('devices', {
45-
write: ({ flags, result }) => {
46-
const devices = result as unknown as AgentDeviceDevice[];
47-
const data = { devices: devices.map(serializeDevice) };
48-
writeCommandOutput(flags, data, () => devices.map(formatDeviceLine).join('\n'));
49-
},
50-
}),
51-
apps: createFormattedSemanticHandler('apps', {
52-
write: ({ flags, result }) => {
53-
const appsFilter = assertResolvedAppsFilter(flags.appsFilter);
54-
const apps = result as unknown as string[];
55-
const data = { apps };
56-
writeCommandOutput(flags, data, () => {
57-
if (!flags.json) {
58-
process.stderr.write(
59-
appsFilter === 'all'
60-
? 'Showing all apps, including system apps.\n'
61-
: 'Showing user-installed apps. Use --all to include system apps.\n',
62-
);
63-
}
64-
if (apps.length > 0) return apps.join('\n');
65-
return appsFilter === 'all' ? 'No apps found.' : 'No user-installed apps found.';
66-
});
67-
},
68-
}),
69-
session: createFormattedSemanticHandler('session', {
70-
beforeRun: ({ positionals }) => {
71-
const subcommand = positionals[0] ?? 'list';
72-
if (subcommand !== 'list') {
73-
throw new AppError('INVALID_ARGS', 'session only supports list');
74-
}
75-
},
76-
write: ({ flags, result }) => {
77-
const sessions = (result as { sessions: AgentDeviceSession[] }).sessions;
78-
const data = { sessions: sessions.map(serializeSessionListEntry) };
79-
writeCommandOutput(flags, data, () => JSON.stringify(data, null, 2));
80-
},
81-
}),
82-
open: createFormattedSemanticHandler('open', {
83-
write: ({ flags, result }) => {
84-
writeCommandMessage(flags, serializeOpenResult(result as AppOpenResult));
85-
},
86-
}),
87-
close: createFormattedSemanticHandler('close', {
88-
write: ({ flags, result }) => {
89-
writeCommandMessage(
90-
flags,
91-
serializeCloseResult(result as AppCloseResult | SessionCloseResult),
92-
);
93-
},
94-
}),
95-
install: createFormattedSemanticHandler('install', {
96-
write: ({ flags, result }) => {
97-
writeCommandMessage(flags, serializeDeployResult(result as AppDeployResult));
98-
},
99-
}),
100-
reinstall: createFormattedSemanticHandler('reinstall', {
101-
write: ({ flags, result }) => {
102-
writeCommandMessage(flags, serializeDeployResult(result as AppDeployResult));
103-
},
104-
}),
105-
'install-from-source': createFormattedSemanticHandler('install-from-source', {
106-
write: ({ flags, result }) => {
107-
writeCommandMessage(
108-
flags,
109-
serializeInstallFromSourceResult(result as AppInstallFromSourceResult),
110-
);
111-
},
112-
}),
113-
snapshot: createFormattedSemanticHandler('snapshot', {
114-
positionals: () => [],
115-
write: ({ flags, result }) => {
116-
const data = serializeSnapshotResult(result as CaptureSnapshotResult);
117-
// Programmatic SDK callers can see `unchanged`; CLI --json hides it for schema compatibility.
118-
const outputData = flags.json ? withoutUnchanged(data) : data;
119-
writeCommandOutput(flags, outputData, () =>
120-
formatSnapshotText(outputData, {
121-
raw: flags.snapshotRaw,
122-
flatten: flags.snapshotInteractiveOnly,
123-
}),
124-
);
125-
},
126-
}),
127-
metro: createFormattedSemanticHandler('metro', {
128-
write: ({ positionals, flags, result }) => {
129-
const action = (positionals[0] ?? '').toLowerCase();
130-
writeCommandOutput(flags, result, () =>
131-
action === 'reload'
132-
? `Reloaded React Native apps via ${(result as { reloadUrl?: unknown }).reloadUrl}`
133-
: JSON.stringify(result, null, 2),
134-
);
135-
},
136-
}),
137-
} satisfies Partial<Record<SemanticCliCommand, ClientCommandHandler>>;
25+
const formattedSemanticCommandHandlers = Object.fromEntries(
26+
listSemanticCliOutputCommandNames().map((command) => [
27+
command,
28+
createFormattedSemanticHandler(command),
29+
]),
30+
) as Partial<Record<SemanticCliCommand, ClientCommandHandler>>;
13831

13932
export const dedicatedSemanticCommandHandlers = formattedSemanticCommandHandlers;
14033

@@ -188,42 +81,31 @@ function createGenericClientCommandHandler(
18881
};
18982
}
19083

191-
function createFormattedSemanticHandler(
192-
command: SemanticCliCommand,
193-
options: {
194-
positionals?: (positionals: string[]) => string[];
195-
beforeRun?: (params: { positionals: string[]; flags: CliFlags }) => void;
196-
write: (params: {
197-
positionals: string[];
198-
flags: CliFlags;
199-
result: Awaited<ReturnType<typeof runSemanticCliCommand>>;
200-
}) => void;
201-
},
202-
): ClientCommandHandler {
84+
function createFormattedSemanticHandler(command: SemanticCliCommand): ClientCommandHandler {
20385
return async ({ positionals, flags, client }) => {
204-
options.beforeRun?.({ positionals, flags });
205-
const semanticPositionals = options.positionals?.(positionals) ?? positionals;
206-
const result = await runSemanticCliCommand({
86+
const { cliOutput } = await runSemanticCliCommandWithOutput({
20787
client,
20888
command,
209-
positionals: semanticPositionals,
89+
positionals,
21090
flags,
21191
});
212-
options.write({ positionals, flags, result });
92+
if (!cliOutput) {
93+
throw new Error(`Missing CLI output formatter for semantic command: ${command}`);
94+
}
95+
writeSemanticCliOutput(flags, cliOutput);
21396
return true;
21497
};
21598
}
21699

217-
function formatDeviceLine(device: AgentDeviceDevice): string {
218-
const kind = device.kind ? ` ${device.kind}` : '';
219-
const target = device.target ? ` target=${device.target}` : '';
220-
const booted = typeof device.booted === 'boolean' ? ` booted=${device.booted}` : '';
221-
return `${device.name} (${device.platform}${kind}${target})${booted}`;
222-
}
223-
224-
function withoutUnchanged(data: Record<string, unknown>): Record<string, unknown> {
225-
const { unchanged: _unchanged, ...outputData } = data;
226-
return outputData;
100+
function writeSemanticCliOutput(flags: CliFlags, output: SemanticCliOutput): void {
101+
if (!flags.json && output.stderr) {
102+
process.stderr.write(output.stderr);
103+
}
104+
writeCommandOutput(
105+
flags,
106+
flags.json ? (output.jsonData ?? output.data) : output.data,
107+
() => output.text,
108+
);
227109
}
228110

229111
function isGenericSemanticCliCommand(command: SemanticCliCommand): boolean {

src/commands/semantic-cli.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { AgentDeviceClient, CommandRequestResult } from '../client.ts';
22
import { readSemanticInputFromCli } from './semantic-grammar.ts';
3-
import { runSemanticCommand, type SemanticCliCommand } from './semantic-command-surface.ts';
3+
import {
4+
formatSemanticCliOutput,
5+
runSemanticCommand,
6+
type SemanticCliCommand,
7+
} from './semantic-command-surface.ts';
8+
import type { SemanticCliOutput } from './semantic-contract.ts';
49
import type { CliFlags } from '../utils/command-schema.ts';
510

611
type SemanticCliRunOptions = {
@@ -13,6 +18,26 @@ type SemanticCliRunOptions = {
1318
export async function runSemanticCliCommand(
1419
options: SemanticCliRunOptions,
1520
): Promise<CommandRequestResult> {
21+
return (await runSemanticCliCommandWithOutput(options)).result;
22+
}
23+
24+
export async function runSemanticCliCommandWithOutput(options: SemanticCliRunOptions): Promise<{
25+
result: CommandRequestResult;
26+
cliOutput?: SemanticCliOutput;
27+
}> {
1628
const input = readSemanticInputFromCli(options.command, options.positionals, options.flags);
17-
return (await runSemanticCommand(options.client, options.command, input)) as CommandRequestResult;
29+
const result = (await runSemanticCommand(
30+
options.client,
31+
options.command,
32+
input,
33+
)) as CommandRequestResult;
34+
return {
35+
result,
36+
cliOutput: formatSemanticCliOutput({
37+
name: options.command,
38+
input,
39+
result,
40+
positionals: options.positionals,
41+
}),
42+
};
1843
}

0 commit comments

Comments
 (0)