@linzumi/cli 1.0.18 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,325 @@
1
+ /*
2
+ - Date: 2026-06-29
3
+ Spec: plans/2026-06-29-claude-code-raw-replay-harness-plan.md
4
+ Relationship: Local replay entrypoint for raw agent fixtures. It loads one or
5
+ more NDJSON fixtures, validates coverage, and writes static playground and
6
+ JSON summary artifacts without starting live providers.
7
+ */
8
+ import { mkdir, writeFile } from 'node:fs/promises';
9
+ import { basename, join, resolve } from 'node:path';
10
+ import {
11
+ commanderReplayCoverageRequirements,
12
+ commanderReplayRealCoverageRequirements,
13
+ extractAgentReplayCoverage,
14
+ missingAgentReplayCoverage,
15
+ } from '../src/agentReplayCoverage';
16
+ import {
17
+ loadAgentReplayFixture,
18
+ type AgentReplayFixture,
19
+ } from '../src/agentReplayFixture';
20
+ import { renderAgentReplayPlaygroundHtml } from '../src/agentReplayPlayground';
21
+ import { collectAgentReplayDiagnostics } from '../src/agentReplayDiagnostics';
22
+ import {
23
+ evaluateAgentReplayFeatureReadiness,
24
+ loadAgentReplayManifestForFixture,
25
+ summarizeAgentReplayManifest,
26
+ type AgentReplayFeatureReadiness,
27
+ type AgentReplayManifestSummary,
28
+ } from '../src/agentReplayManifest';
29
+
30
+ type ReplayCliOptions = {
31
+ readonly fixtures: readonly string[];
32
+ readonly outDir: string;
33
+ readonly coverageMode: 'none' | 'real' | 'full';
34
+ readonly html: boolean;
35
+ readonly summary: boolean;
36
+ readonly requiredFeatureId: string | undefined;
37
+ };
38
+
39
+ async function main(): Promise<void> {
40
+ const args = process.argv.slice(2);
41
+
42
+ if (args.includes('--help')) {
43
+ printHelp();
44
+ return;
45
+ }
46
+
47
+ const options = parseArgs(args);
48
+ const fixtures = await Promise.all(options.fixtures.map(loadFixture));
49
+ const manifestSummaries = await loadManifestSummaries(options.fixtures);
50
+ const featureReadiness = await loadManifestReadiness(options.fixtures);
51
+ await mkdir(options.outDir, { recursive: true });
52
+
53
+ if (options.html) {
54
+ for (const fixture of fixtures) {
55
+ const name = fixtureOutputName(fixture.path);
56
+ await writeFile(
57
+ join(options.outDir, `${name}.html`),
58
+ renderAgentReplayPlaygroundHtml(fixture),
59
+ 'utf8'
60
+ );
61
+ }
62
+ }
63
+
64
+ const coverage = extractAgentReplayCoverage(fixtures);
65
+ const diagnostics = combineDiagnostics(
66
+ fixtures.map(collectAgentReplayDiagnostics)
67
+ );
68
+ const requirements =
69
+ options.coverageMode === 'real'
70
+ ? commanderReplayRealCoverageRequirements
71
+ : commanderReplayCoverageRequirements;
72
+ const gaps =
73
+ options.coverageMode === 'none'
74
+ ? []
75
+ : missingAgentReplayCoverage(coverage, requirements);
76
+ const requiredFeatureGate = evaluateRequiredFeatureGate(
77
+ options.requiredFeatureId,
78
+ featureReadiness
79
+ );
80
+ const summary = {
81
+ fixtures: fixtures.map((fixture) => ({
82
+ path: fixture.path,
83
+ records: fixture.records.length,
84
+ })),
85
+ coverageMode: options.coverageMode,
86
+ coverage: coverageSummary(coverage),
87
+ featureCoverage: manifestSummaries,
88
+ featureReadiness,
89
+ diagnostics,
90
+ gaps,
91
+ requiredFeature: requiredFeatureGate,
92
+ };
93
+
94
+ if (options.summary) {
95
+ await writeFile(
96
+ join(options.outDir, 'summary.json'),
97
+ `${JSON.stringify(summary, null, 2)}\n`,
98
+ 'utf8'
99
+ );
100
+ }
101
+
102
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
103
+
104
+ if (
105
+ gaps.length > 0 ||
106
+ diagnostics.droppedEvents.length > 0 ||
107
+ requiredFeatureGate.status === 'failed'
108
+ ) {
109
+ process.exitCode = 1;
110
+ }
111
+ }
112
+
113
+ function combineDiagnostics(
114
+ diagnostics: readonly ReturnType<typeof collectAgentReplayDiagnostics>[]
115
+ ): ReturnType<typeof collectAgentReplayDiagnostics> {
116
+ return {
117
+ droppedEvents: diagnostics.flatMap((entry) => entry.droppedEvents),
118
+ };
119
+ }
120
+
121
+ function parseArgs(args: readonly string[]): ReplayCliOptions {
122
+ const fixtures = valuesFor(args, '--fixture').map((path) => resolve(path));
123
+
124
+ if (fixtures.length === 0) {
125
+ throw new Error('provide at least one --fixture <path>');
126
+ }
127
+
128
+ return {
129
+ fixtures,
130
+ outDir: resolve(argValue(args, '--out-dir') ?? 'agent-replay-output'),
131
+ coverageMode: coverageModeValue(argValue(args, '--coverage') ?? 'full'),
132
+ html: !args.includes('--no-html'),
133
+ summary: !args.includes('--no-summary'),
134
+ requiredFeatureId: argValue(args, '--require-feature'),
135
+ };
136
+ }
137
+
138
+ function coverageModeValue(value: string): ReplayCliOptions['coverageMode'] {
139
+ switch (value) {
140
+ case 'none':
141
+ case 'real':
142
+ case 'full':
143
+ return value;
144
+ default:
145
+ throw new Error(`unsupported --coverage value: ${value}`);
146
+ }
147
+ }
148
+
149
+ async function loadManifestSummaries(
150
+ fixturePaths: readonly string[]
151
+ ): Promise<readonly AgentReplayManifestSummary[]> {
152
+ const manifests: AgentReplayManifestSummary[] = [];
153
+
154
+ for (const fixturePath of fixturePaths) {
155
+ const loaded = await loadAgentReplayManifestForFixture(fixturePath);
156
+
157
+ if (loaded === undefined) {
158
+ continue;
159
+ }
160
+
161
+ if (loaded.type === 'error') {
162
+ throw new Error(JSON.stringify(loaded.error));
163
+ }
164
+
165
+ manifests.push(summarizeAgentReplayManifest(loaded.manifest));
166
+ }
167
+
168
+ return manifests;
169
+ }
170
+
171
+ async function loadManifestReadiness(
172
+ fixturePaths: readonly string[]
173
+ ): Promise<readonly AgentReplayFeatureReadiness[]> {
174
+ const readiness: AgentReplayFeatureReadiness[] = [];
175
+
176
+ for (const fixturePath of fixturePaths) {
177
+ const loaded = await loadAgentReplayManifestForFixture(fixturePath);
178
+
179
+ if (loaded === undefined) {
180
+ continue;
181
+ }
182
+
183
+ if (loaded.type === 'error') {
184
+ throw new Error(JSON.stringify(loaded.error));
185
+ }
186
+
187
+ readiness.push(...evaluateAgentReplayFeatureReadiness(loaded.manifest));
188
+ }
189
+
190
+ return readiness;
191
+ }
192
+
193
+ type RequiredFeatureGate =
194
+ | { readonly status: 'not-required' }
195
+ | {
196
+ readonly status: 'passed';
197
+ readonly featureId: string;
198
+ readonly readiness: AgentReplayFeatureReadiness;
199
+ }
200
+ | {
201
+ readonly status: 'failed';
202
+ readonly featureId: string;
203
+ readonly reason: string;
204
+ readonly readiness?: AgentReplayFeatureReadiness | undefined;
205
+ };
206
+
207
+ function evaluateRequiredFeatureGate(
208
+ featureId: string | undefined,
209
+ readiness: readonly AgentReplayFeatureReadiness[]
210
+ ): RequiredFeatureGate {
211
+ if (featureId === undefined) {
212
+ return { status: 'not-required' };
213
+ }
214
+
215
+ if (readiness.length === 0) {
216
+ return {
217
+ status: 'failed',
218
+ featureId,
219
+ reason: '--require-feature needs a neighboring fixture manifest',
220
+ };
221
+ }
222
+
223
+ const feature = readiness.find((row) => row.featureId === featureId);
224
+
225
+ if (feature === undefined) {
226
+ return {
227
+ status: 'failed',
228
+ featureId,
229
+ reason: `feature ${featureId} is not present in the fixture manifests`,
230
+ };
231
+ }
232
+
233
+ switch (feature.status) {
234
+ case 'implemented':
235
+ case 'ready-to-implement':
236
+ return { status: 'passed', featureId, readiness: feature };
237
+ case 'blocked':
238
+ case 'missing-proof':
239
+ case 'not-started':
240
+ return {
241
+ status: 'failed',
242
+ featureId,
243
+ readiness: feature,
244
+ reason: feature.reason,
245
+ };
246
+ }
247
+ }
248
+
249
+ async function loadFixture(path: string): Promise<AgentReplayFixture> {
250
+ const loaded = await loadAgentReplayFixture(path);
251
+
252
+ if (loaded.type === 'error') {
253
+ throw new Error(JSON.stringify(loaded.error));
254
+ }
255
+
256
+ return loaded.fixture;
257
+ }
258
+
259
+ function valuesFor(args: readonly string[], name: string): readonly string[] {
260
+ const values: string[] = [];
261
+
262
+ for (let index = 0; index < args.length; index += 1) {
263
+ if (args[index] === name) {
264
+ const value = args[index + 1];
265
+
266
+ if (value === undefined) {
267
+ throw new Error(`${name} requires a value`);
268
+ }
269
+
270
+ values.push(value);
271
+ }
272
+ }
273
+
274
+ return values;
275
+ }
276
+
277
+ function argValue(args: readonly string[], name: string): string | undefined {
278
+ const index = args.indexOf(name);
279
+
280
+ if (index === -1) {
281
+ return undefined;
282
+ }
283
+
284
+ const value = args[index + 1];
285
+
286
+ if (value === undefined) {
287
+ throw new Error(`${name} requires a value`);
288
+ }
289
+
290
+ return value;
291
+ }
292
+
293
+ function fixtureOutputName(path: string): string {
294
+ return basename(path).replace(/\.ndjson$/u, '');
295
+ }
296
+
297
+ function coverageSummary(
298
+ coverage: ReturnType<typeof extractAgentReplayCoverage>
299
+ ): Record<string, readonly string[]> {
300
+ return {
301
+ codexNotificationMethods: sorted(coverage.codexNotificationMethods),
302
+ codexRequestMethods: sorted(coverage.codexRequestMethods),
303
+ codexThreadItemTypes: sorted(coverage.codexThreadItemTypes),
304
+ claudeSdkMessageTags: sorted(coverage.claudeSdkMessageTags),
305
+ claudeContentBlockTypes: sorted(coverage.claudeContentBlockTypes),
306
+ claudeToolNames: sorted(coverage.claudeToolNames),
307
+ };
308
+ }
309
+
310
+ function sorted(values: ReadonlySet<string>): readonly string[] {
311
+ return Array.from(values).sort();
312
+ }
313
+
314
+ function printHelp(): void {
315
+ process.stdout.write(
316
+ `Usage: pnpm run replay:agent-fixture -- --fixture <path> [--fixture <path>...] [options]\n\nOptions:\n --out-dir <path> Artifact directory. Default: agent-replay-output\n --coverage none|real|full Coverage gate. Default: full\n --no-html Do not write per-fixture playground HTML\n --no-summary Do not write summary.json\n\nExamples:\n pnpm --filter @linzumi/cli run replay:agent-fixture -- \\\n --fixture test/fixtures/agent-raw-replay/real-probe/codex.ndjson \\\n --fixture test/fixtures/agent-raw-replay/real-probe/claude.ndjson \\\n --coverage real \\\n --out-dir /tmp/agent-replay-real\n`
317
+ );
318
+ }
319
+
320
+ main().catch((error) => {
321
+ process.stderr.write(
322
+ `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`
323
+ );
324
+ process.exitCode = 1;
325
+ });
@@ -0,0 +1,383 @@
1
+ /*
2
+ - Date: 2026-06-29
3
+ Spec: plans/2026-06-29-claude-code-raw-replay-harness-plan.md
4
+ Relationship: Vendors OpenAI Codex app-server generated TypeScript protocol
5
+ schemas as a single declaration bundle so Commander replay/coverage tests can
6
+ type-check against the real JSON-RPC protocol surface without committing the
7
+ upstream one-file-per-type layout.
8
+ */
9
+ import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
10
+ import { existsSync } from 'node:fs';
11
+ import { tmpdir } from 'node:os';
12
+ import { dirname, join, relative, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { spawn } from 'node:child_process';
15
+
16
+ type VendorOptions = {
17
+ readonly sourceRepo: string | undefined;
18
+ readonly sourceUrl: string;
19
+ readonly sourceRef: string;
20
+ readonly outFile: string;
21
+ readonly manifestFile: string;
22
+ readonly keepTemp: boolean;
23
+ readonly check: boolean;
24
+ };
25
+
26
+ type PreparedSource = {
27
+ readonly repoPath: string;
28
+ readonly cleanup: (() => Promise<void>) | undefined;
29
+ };
30
+
31
+ const thisFile = fileURLToPath(import.meta.url);
32
+ const packageRoot = resolve(dirname(thisFile), '..');
33
+ const protocolSubtree = 'codex-rs/app-server-protocol/schema/typescript';
34
+ const defaultSourceUrl = 'https://github.com/openai/codex.git';
35
+ const vendorModuleName = '@linzumi/vendor/codex-app-server-protocol';
36
+ const defaultOutFile = join(
37
+ packageRoot,
38
+ 'src/vendor/codex-app-server-protocol.d.ts'
39
+ );
40
+ const defaultManifestFile = join(
41
+ packageRoot,
42
+ 'src/vendor/codex-app-server-protocol-manifest.json'
43
+ );
44
+
45
+ async function main(): Promise<void> {
46
+ const args = process.argv.slice(2);
47
+
48
+ if (args.includes('--help')) {
49
+ printHelp();
50
+ return;
51
+ }
52
+
53
+ const options = parseArgs(args);
54
+ const source = await prepareSource(options);
55
+
56
+ try {
57
+ const sourceDir = join(source.repoPath, protocolSubtree);
58
+
59
+ if (!existsSync(sourceDir)) {
60
+ throw new Error(
61
+ `Codex protocol schema directory not found: ${sourceDir}`
62
+ );
63
+ }
64
+
65
+ const sourceCommit = await git(source.repoPath, ['rev-parse', 'HEAD']);
66
+ if (options.check) {
67
+ await checkVendoredOutput(options, sourceDir, sourceCommit.trim());
68
+ process.stdout.write(
69
+ `verified Codex app-server protocol declaration bundle\n`
70
+ );
71
+ } else {
72
+ await writeDeclarationBundle(options, sourceDir, sourceCommit.trim());
73
+ await writeManifest(options, sourceCommit.trim());
74
+ process.stdout.write(
75
+ `vendored Codex app-server protocol declaration bundle\n`
76
+ );
77
+ }
78
+ process.stdout.write(` source: ${options.sourceUrl}\n`);
79
+ process.stdout.write(` ref: ${options.sourceRef}\n`);
80
+ process.stdout.write(` commit: ${sourceCommit.trim()}\n`);
81
+ process.stdout.write(` module: ${vendorModuleName}\n`);
82
+ process.stdout.write(` out: ${options.outFile}\n`);
83
+ process.stdout.write(` manifest: ${options.manifestFile}\n`);
84
+ } finally {
85
+ await source.cleanup?.();
86
+ }
87
+ }
88
+
89
+ function parseArgs(args: readonly string[]): VendorOptions {
90
+ return {
91
+ sourceRepo: optionalResolvedArg(args, '--codex-repo'),
92
+ sourceUrl: argValue(args, '--source-url') ?? defaultSourceUrl,
93
+ sourceRef: argValue(args, '--ref') ?? 'main',
94
+ outFile: optionalResolvedArg(args, '--out-file') ?? defaultOutFile,
95
+ manifestFile:
96
+ optionalResolvedArg(args, '--manifest-file') ?? defaultManifestFile,
97
+ keepTemp: args.includes('--keep-temp'),
98
+ check: args.includes('--check'),
99
+ };
100
+ }
101
+
102
+ async function checkVendoredOutput(
103
+ options: VendorOptions,
104
+ sourceDir: string,
105
+ sourceCommit: string
106
+ ): Promise<void> {
107
+ const checkRoot = await mkdtemp(
108
+ join(packageRoot, '.linzumi-codex-protocol-check-')
109
+ );
110
+
111
+ try {
112
+ const expectedOutFile = join(checkRoot, 'codex-app-server-protocol.d.ts');
113
+ const expectedManifestFile = join(
114
+ checkRoot,
115
+ 'codex-app-server-protocol-manifest.json'
116
+ );
117
+ const expectedOptions = {
118
+ ...options,
119
+ outFile: expectedOutFile,
120
+ manifestFile: expectedManifestFile,
121
+ check: false,
122
+ };
123
+
124
+ await writeDeclarationBundle(expectedOptions, sourceDir, sourceCommit);
125
+ await writeManifest(expectedOptions, sourceCommit);
126
+ await normalizeExpectedManifestForCommittedPaths(
127
+ expectedManifestFile,
128
+ options.outFile
129
+ );
130
+ await assertSameFile(expectedOutFile, options.outFile);
131
+ await assertSameFile(expectedManifestFile, options.manifestFile);
132
+ } finally {
133
+ await rm(checkRoot, { recursive: true, force: true });
134
+ }
135
+ }
136
+
137
+ async function normalizeExpectedManifestForCommittedPaths(
138
+ manifestPath: string,
139
+ committedOutFile: string
140
+ ): Promise<void> {
141
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as Record<
142
+ string,
143
+ unknown
144
+ >;
145
+ const normalized = {
146
+ ...manifest,
147
+ declarationFile: relative(packageRoot, committedOutFile),
148
+ };
149
+
150
+ await writeFile(
151
+ manifestPath,
152
+ `${JSON.stringify(normalized, null, 2)}\n`,
153
+ 'utf8'
154
+ );
155
+ }
156
+
157
+ async function assertSameFile(
158
+ expectedPath: string,
159
+ actualPath: string
160
+ ): Promise<void> {
161
+ const expected = await readFile(expectedPath, 'utf8');
162
+ const actual = await readFile(actualPath, 'utf8');
163
+
164
+ if (expected !== actual) {
165
+ throw new Error(
166
+ `vendored Codex protocol drift detected: ${actualPath} does not match regenerated output`
167
+ );
168
+ }
169
+ }
170
+
171
+ async function prepareSource(options: VendorOptions): Promise<PreparedSource> {
172
+ if (options.sourceRepo !== undefined) {
173
+ return { repoPath: options.sourceRepo, cleanup: undefined };
174
+ }
175
+
176
+ const repoPath = await mkdtemp(join(tmpdir(), 'linzumi-openai-codex-'));
177
+ await git(undefined, [
178
+ 'clone',
179
+ '--filter=blob:none',
180
+ '--sparse',
181
+ options.sourceUrl,
182
+ repoPath,
183
+ ]);
184
+ await git(repoPath, ['sparse-checkout', 'set', protocolSubtree]);
185
+ await git(repoPath, ['fetch', '--depth', '1', 'origin', options.sourceRef]);
186
+ await git(repoPath, ['checkout', 'FETCH_HEAD']);
187
+
188
+ return {
189
+ repoPath,
190
+ cleanup: options.keepTemp
191
+ ? undefined
192
+ : async () => {
193
+ await rm(repoPath, { recursive: true, force: true });
194
+ },
195
+ };
196
+ }
197
+
198
+ async function writeDeclarationBundle(
199
+ options: VendorOptions,
200
+ sourceDir: string,
201
+ sourceCommit: string
202
+ ): Promise<void> {
203
+ const bundleRoot = await mkdtemp(
204
+ join(tmpdir(), 'linzumi-codex-protocol-dts-')
205
+ );
206
+
207
+ try {
208
+ const copiedProtocolDir = join(bundleRoot, 'protocol');
209
+ const entryFile = join(bundleRoot, 'entry.ts');
210
+ const rawBundleFile = join(bundleRoot, 'raw-bundle.d.ts');
211
+
212
+ await cp(sourceDir, copiedProtocolDir, { recursive: true });
213
+ await writeFile(
214
+ entryFile,
215
+ [
216
+ "export * from './protocol/index';",
217
+ "export * as v2 from './protocol/v2/index';",
218
+ '',
219
+ ].join('\n'),
220
+ 'utf8'
221
+ );
222
+
223
+ await command(packageRoot, [
224
+ 'pnpm',
225
+ 'exec',
226
+ 'tsc',
227
+ '--declaration',
228
+ '--emitDeclarationOnly',
229
+ '--outFile',
230
+ rawBundleFile,
231
+ '--module',
232
+ 'amd',
233
+ '--moduleResolution',
234
+ 'node',
235
+ '--target',
236
+ 'ES2022',
237
+ '--skipLibCheck',
238
+ entryFile,
239
+ ]);
240
+
241
+ const rawBundle = await readFile(rawBundleFile, 'utf8');
242
+ const bundled = rawBundle
243
+ .replaceAll(
244
+ 'declare module "protocol/',
245
+ `declare module "${vendorModuleName}/`
246
+ )
247
+ .replaceAll('from "protocol/', `from "${vendorModuleName}/`)
248
+ .replace(
249
+ 'declare module "entry" {',
250
+ `declare module "${vendorModuleName}" {`
251
+ );
252
+ const header = [
253
+ '// GENERATED CODE! DO NOT MODIFY BY HAND!',
254
+ '// Bundled from OpenAI Codex app-server protocol TypeScript schemas.',
255
+ `// Source commit: ${sourceCommit}`,
256
+ `// Module: ${vendorModuleName}`,
257
+ '',
258
+ ].join('\n');
259
+
260
+ await mkdir(dirname(options.outFile), { recursive: true });
261
+ await writeFile(options.outFile, `${header}${bundled}`, 'utf8');
262
+ await formatGeneratedFile(options.outFile);
263
+ } finally {
264
+ await rm(bundleRoot, { recursive: true, force: true });
265
+ }
266
+ }
267
+
268
+ async function formatGeneratedFile(path: string): Promise<void> {
269
+ await command(packageRoot, [
270
+ 'pnpm',
271
+ 'exec',
272
+ 'biome',
273
+ 'format',
274
+ '--write',
275
+ '--no-errors-on-unmatched',
276
+ path,
277
+ ]);
278
+ }
279
+
280
+ async function writeManifest(
281
+ options: VendorOptions,
282
+ sourceCommit: string
283
+ ): Promise<void> {
284
+ const manifest = {
285
+ source: 'openai/codex codex-rs/app-server-protocol/schema/typescript',
286
+ sourceUrl: options.sourceUrl,
287
+ requestedRef: options.sourceRef,
288
+ sourceCommit,
289
+ protocolSubtree,
290
+ moduleName: vendorModuleName,
291
+ declarationFile: relative(packageRoot, options.outFile),
292
+ updateCommand:
293
+ 'pnpm --filter @linzumi/cli run vendor:codex-app-server-protocol -- --ref <openai-codex-ref>',
294
+ };
295
+
296
+ await mkdir(dirname(options.manifestFile), { recursive: true });
297
+ await writeFile(
298
+ options.manifestFile,
299
+ `${JSON.stringify(manifest, null, 2)}\n`,
300
+ 'utf8'
301
+ );
302
+ }
303
+
304
+ function optionalResolvedArg(
305
+ args: readonly string[],
306
+ name: string
307
+ ): string | undefined {
308
+ const value = argValue(args, name);
309
+ return value === undefined ? undefined : resolve(value);
310
+ }
311
+
312
+ function argValue(args: readonly string[], name: string): string | undefined {
313
+ const index = args.indexOf(name);
314
+
315
+ if (index === -1) {
316
+ return undefined;
317
+ }
318
+
319
+ const value = args[index + 1];
320
+
321
+ if (value === undefined) {
322
+ throw new Error(`${name} requires a value`);
323
+ }
324
+
325
+ return value;
326
+ }
327
+
328
+ function git(
329
+ cwd: string | undefined,
330
+ args: readonly string[]
331
+ ): Promise<string> {
332
+ return command(cwd, ['git', ...args]);
333
+ }
334
+
335
+ function command(
336
+ cwd: string | undefined,
337
+ args: readonly string[]
338
+ ): Promise<string> {
339
+ return new Promise((resolvePromise, reject) => {
340
+ const [commandName, ...commandArgs] = args;
341
+
342
+ if (commandName === undefined) {
343
+ reject(new Error('command requires at least one argument'));
344
+ return;
345
+ }
346
+
347
+ const child = spawn(commandName, commandArgs, {
348
+ cwd,
349
+ stdio: ['ignore', 'pipe', 'pipe'],
350
+ });
351
+ const stdout: string[] = [];
352
+ const stderr: string[] = [];
353
+
354
+ child.stdout.on('data', (chunk) => stdout.push(String(chunk)));
355
+ child.stderr.on('data', (chunk) => stderr.push(String(chunk)));
356
+ child.on('error', reject);
357
+ child.on('close', (code) => {
358
+ if (code === 0) {
359
+ resolvePromise(stdout.join(''));
360
+ return;
361
+ }
362
+
363
+ reject(
364
+ new Error(
365
+ `${args.join(' ')} failed with exit ${code}: ${stderr.join('')}`
366
+ )
367
+ );
368
+ });
369
+ });
370
+ }
371
+
372
+ function printHelp(): void {
373
+ process.stdout.write(
374
+ `Usage: pnpm run vendor:codex-app-server-protocol -- [options]\n\nOptions:\n --codex-repo <path> Bundle from an existing OpenAI Codex checkout\n --ref <ref> Git ref to vendor when cloning. Default: main\n --source-url <url> Codex git URL. Default: ${defaultSourceUrl}\n --out-file <path> Declaration bundle path. Default: src/vendor/codex-app-server-protocol.d.ts\n --manifest-file <path> Manifest path. Default: src/vendor/codex-app-server-protocol-manifest.json\n --keep-temp Keep temporary clone when cloning\n --check Regenerate into a temp directory and fail if committed files differ\n\nExamples:\n pnpm --filter @linzumi/cli run vendor:codex-app-server-protocol -- --ref main\n pnpm --filter @linzumi/cli run vendor:codex-app-server-protocol -- --codex-repo /tmp/openai-codex\n pnpm --filter @linzumi/cli run vendor:codex-app-server-protocol -- --codex-repo /tmp/openai-codex --check\n`
375
+ );
376
+ }
377
+
378
+ main().catch((error) => {
379
+ process.stderr.write(
380
+ `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`
381
+ );
382
+ process.exitCode = 1;
383
+ });