Skip to content

Commit ddc9ea1

Browse files
committed
refactor: split semantic grammar modules
1 parent 9e3fb5e commit ddc9ea1

13 files changed

Lines changed: 1782 additions & 1547 deletions

File tree

src/commands/semantic-grammar.ts

Lines changed: 24 additions & 1547 deletions
Large diffs are not rendered by default.
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { INTERNAL_COMMANDS, PUBLIC_COMMANDS } from '../../command-catalog.ts';
2+
import type { AppPushOptions, AppTriggerEventOptions } from '../../client-types.ts';
3+
import type { CliFlags } from '../../utils/command-schema.ts';
4+
import { AppError } from '../../utils/errors.ts';
5+
import { parseGitHubActionsArtifactInstallSourceSpec } from '../../utils/install-source-config.ts';
6+
import { assertResolvedAppsFilter } from '../app-inventory-contract.ts';
7+
import {
8+
commonInputFromFlags,
9+
direct,
10+
optionalString,
11+
readJsonObject,
12+
request,
13+
requiredDaemonString,
14+
requiredString,
15+
} from './common.ts';
16+
import type { CliReader, DaemonWriter, SemanticRequestInput } from './types.ts';
17+
18+
export const appCliReaders = {
19+
devices: (_positionals, flags) => commonInputFromFlags(flags),
20+
apps: (_positionals, flags) => ({
21+
...commonInputFromFlags(flags),
22+
appsFilter: assertResolvedAppsFilter(flags.appsFilter),
23+
}),
24+
session: (positionals, flags) => ({
25+
...commonInputFromFlags(flags),
26+
action: positionals[0] ?? 'list',
27+
}),
28+
boot: (_positionals, flags) => ({
29+
...commonInputFromFlags(flags),
30+
headless: flags.headless,
31+
}),
32+
open: (positionals, flags) => ({
33+
...commonInputFromFlags(flags),
34+
app: positionals[0],
35+
url: positionals[1],
36+
surface: flags.surface,
37+
activity: flags.activity,
38+
launchConsole: flags.launchConsole,
39+
relaunch: flags.relaunch,
40+
saveScript: flags.saveScript,
41+
noRecord: flags.noRecord,
42+
}),
43+
close: (positionals, flags) => ({
44+
...commonInputFromFlags(flags),
45+
app: positionals[0],
46+
shutdown: flags.shutdown,
47+
saveScript: flags.saveScript,
48+
}),
49+
install: installInputFromCli,
50+
reinstall: installInputFromCli,
51+
'install-from-source': (positionals, flags) => ({
52+
...commonInputFromFlags(flags),
53+
source: resolveInstallSource(positionals, flags),
54+
retainPaths: flags.retainPaths,
55+
retentionMs: flags.retentionMs,
56+
}),
57+
push: (positionals, flags) => ({
58+
...commonInputFromFlags(flags),
59+
app: requiredString(positionals[0], 'push requires bundleOrPackage'),
60+
payload: requiredString(positionals[1], 'push requires payloadOrJson'),
61+
}),
62+
'trigger-app-event': (positionals, flags) => ({
63+
...commonInputFromFlags(flags),
64+
event: requiredString(positionals[0], 'trigger-app-event requires event'),
65+
payload: positionals[1]
66+
? readJsonObject(positionals[1], 'trigger-app-event payload')
67+
: undefined,
68+
}),
69+
} satisfies Record<string, CliReader>;
70+
71+
export const appDaemonWriters = {
72+
devices: direct(PUBLIC_COMMANDS.devices),
73+
boot: direct(PUBLIC_COMMANDS.boot),
74+
apps: direct(PUBLIC_COMMANDS.apps),
75+
open: direct(PUBLIC_COMMANDS.open, openPositionals),
76+
close: direct(PUBLIC_COMMANDS.close, (input) => optionalString(input.app)),
77+
install: direct(PUBLIC_COMMANDS.install, (input) => requiredPair(input.app, input.appPath)),
78+
reinstall: direct(PUBLIC_COMMANDS.reinstall, (input) => requiredPair(input.app, input.appPath)),
79+
'install-from-source': (input) =>
80+
request(INTERNAL_COMMANDS.installSource, [], {
81+
...input,
82+
installSource: input.source,
83+
retainMaterializedPaths: input.retainPaths,
84+
materializedPathRetentionMs: input.retentionMs,
85+
}),
86+
push: direct(PUBLIC_COMMANDS.push, (input) => pushPositionals(input as AppPushOptions)),
87+
'trigger-app-event': direct(PUBLIC_COMMANDS.triggerAppEvent, (input) =>
88+
triggerEventPositionals(input as AppTriggerEventOptions),
89+
),
90+
} satisfies Record<string, DaemonWriter>;
91+
92+
function installInputFromCli(
93+
positionals: string[],
94+
flags: CliFlags,
95+
command = 'install',
96+
): Record<string, unknown> {
97+
return {
98+
...commonInputFromFlags(flags),
99+
app: requiredString(positionals[0], `${command} requires app`),
100+
appPath: requiredString(positionals[1], `${command} requires path`),
101+
};
102+
}
103+
104+
function openPositionals(input: SemanticRequestInput): string[] {
105+
if (!input.app) return [];
106+
return input.url ? [input.app, input.url] : [input.app];
107+
}
108+
109+
function requiredPair(first: unknown, second: unknown): string[] {
110+
return [
111+
requiredDaemonString(first, 'missing first positional'),
112+
requiredDaemonString(second, 'missing second positional'),
113+
];
114+
}
115+
116+
function pushPositionals(input: AppPushOptions): string[] {
117+
return [
118+
input.app,
119+
typeof input.payload === 'string' ? input.payload : JSON.stringify(input.payload),
120+
];
121+
}
122+
123+
function triggerEventPositionals(input: AppTriggerEventOptions): string[] {
124+
return [input.event, ...(input.payload ? [JSON.stringify(input.payload)] : [])];
125+
}
126+
127+
// fallow-ignore-next-line complexity
128+
function resolveInstallSource(positionals: string[], flags: CliFlags) {
129+
const url = positionals[0]?.trim();
130+
if (positionals.length > 1) {
131+
throw new AppError(
132+
'INVALID_ARGS',
133+
'install-from-source accepts either one <url> positional or --github-actions-artifact',
134+
);
135+
}
136+
const githubArtifactSource = flags.githubActionsArtifact
137+
? parseGitHubActionsArtifactInstallSourceSpec(flags.githubActionsArtifact)
138+
: undefined;
139+
const configuredSource = flags.installSource;
140+
const sourceCount = (url ? 1 : 0) + (githubArtifactSource ? 1 : 0) + (configuredSource ? 1 : 0);
141+
if (sourceCount !== 1) {
142+
throw new AppError(
143+
'INVALID_ARGS',
144+
'install-from-source requires exactly one source: <url>, --github-actions-artifact, or config installSource',
145+
);
146+
}
147+
if (!url && flags.header && flags.header.length > 0) {
148+
throw new AppError(
149+
'INVALID_ARGS',
150+
'install-from-source --header is only supported for URL sources',
151+
);
152+
}
153+
if (githubArtifactSource) return githubArtifactSource;
154+
if (configuredSource) return configuredSource;
155+
return {
156+
kind: 'url' as const,
157+
url: url!,
158+
headers: parseInstallSourceHeaders(flags.header),
159+
};
160+
}
161+
162+
function parseInstallSourceHeaders(
163+
headerFlags: CliFlags['header'],
164+
): Record<string, string> | undefined {
165+
if (!headerFlags || headerFlags.length === 0) return undefined;
166+
const headers: Record<string, string> = {};
167+
for (const rawHeader of headerFlags) {
168+
const separator = rawHeader.indexOf(':');
169+
if (separator <= 0) {
170+
throw new AppError(
171+
'INVALID_ARGS',
172+
`Invalid --header value "${rawHeader}". Expected "name:value".`,
173+
);
174+
}
175+
const name = rawHeader.slice(0, separator).trim();
176+
const value = rawHeader.slice(separator + 1).trim();
177+
if (!name) {
178+
throw new AppError(
179+
'INVALID_ARGS',
180+
`Invalid --header value "${rawHeader}". Header name cannot be empty.`,
181+
);
182+
}
183+
headers[name] = value;
184+
}
185+
return headers;
186+
}

0 commit comments

Comments
 (0)