@ramarivera/chofi 0.1.0

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.
Files changed (52) hide show
  1. package/README.md +257 -0
  2. package/dist/cli.d.ts +18 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +1326 -0
  5. package/dist/config.d.ts +10 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +20 -0
  8. package/dist/discovery.d.ts +44 -0
  9. package/dist/discovery.d.ts.map +1 -0
  10. package/dist/discovery.js +151 -0
  11. package/dist/drivers/apple.d.ts +68 -0
  12. package/dist/drivers/apple.d.ts.map +1 -0
  13. package/dist/drivers/apple.js +360 -0
  14. package/dist/drivers/expo.d.ts +14 -0
  15. package/dist/drivers/expo.d.ts.map +1 -0
  16. package/dist/drivers/expo.js +42 -0
  17. package/dist/drivers/idb.d.ts +38 -0
  18. package/dist/drivers/idb.d.ts.map +1 -0
  19. package/dist/drivers/idb.js +52 -0
  20. package/dist/drivers/maestro.d.ts +37 -0
  21. package/dist/drivers/maestro.d.ts.map +1 -0
  22. package/dist/drivers/maestro.js +64 -0
  23. package/dist/drivers/types.d.ts +23 -0
  24. package/dist/drivers/types.d.ts.map +1 -0
  25. package/dist/drivers/types.js +1 -0
  26. package/dist/errors.d.ts +31 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +59 -0
  29. package/dist/events.d.ts +33 -0
  30. package/dist/events.d.ts.map +1 -0
  31. package/dist/events.js +26 -0
  32. package/dist/executor.d.ts +11 -0
  33. package/dist/executor.d.ts.map +1 -0
  34. package/dist/executor.js +17 -0
  35. package/dist/index.d.ts +14 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +13 -0
  38. package/dist/planning.d.ts +18 -0
  39. package/dist/planning.d.ts.map +1 -0
  40. package/dist/planning.js +75 -0
  41. package/dist/runtime.d.ts +157 -0
  42. package/dist/runtime.d.ts.map +1 -0
  43. package/dist/runtime.js +650 -0
  44. package/dist/safety.d.ts +8 -0
  45. package/dist/safety.d.ts.map +1 -0
  46. package/dist/safety.js +84 -0
  47. package/dist/spawn.d.ts +30 -0
  48. package/dist/spawn.d.ts.map +1 -0
  49. package/dist/spawn.js +178 -0
  50. package/dist/tsconfig.tsbuildinfo +1 -0
  51. package/package.json +64 -0
  52. package/sophy.png +0 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1326 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from "commander";
3
+ import { createChofiEvent, NdjsonEventWriter } from "./events.js";
4
+ import { discoverMobileProjectContext } from "./discovery.js";
5
+ import { createCommandRunner, summarizeOutput } from "./executor.js";
6
+ import { createIosRunPlan, createMaestroPlan, doctorChecks } from "./planning.js";
7
+ import { RuntimeController } from "./runtime.js";
8
+ import { isConfirmed, requireConfirmation } from "./safety.js";
9
+ import { readConfig, writeConfig } from "./config.js";
10
+ import { ChofiError, classifyError, ToolOutputError } from "./errors.js";
11
+ class CliError extends Error {
12
+ exitCode;
13
+ phase;
14
+ constructor(message, exitCode, phase) {
15
+ super(message);
16
+ this.exitCode = exitCode;
17
+ this.phase = phase;
18
+ }
19
+ }
20
+ function createProgram(ctx, deps) {
21
+ const program = new Command("chofi")
22
+ .description("Sophia's mobile control surface")
23
+ .configureOutput({ writeOut: () => { }, writeErr: () => { } })
24
+ .exitOverride();
25
+ const globalJson = new Option("--json", "Emit NDJSON events (required for all commands)")
26
+ .makeOptionMandatory(true);
27
+ program.addOption(globalJson);
28
+ // === context ===
29
+ program
30
+ .command("context")
31
+ .description("Show project metadata and tool availability")
32
+ .action(async () => {
33
+ const discovered = ctx.context();
34
+ ctx.writer.write(createChofiEvent({
35
+ clock: ctx.clock,
36
+ event: "summary",
37
+ phase: "context",
38
+ status: "passed",
39
+ data: discovered
40
+ }));
41
+ });
42
+ // === doctor ===
43
+ program
44
+ .command("doctor")
45
+ .description("Run safe health checks")
46
+ .action(async () => {
47
+ const code = await runDoctorCommand({
48
+ clock: ctx.clock,
49
+ context: ctx.context(),
50
+ runner: ctx.runner,
51
+ writer: ctx.writer
52
+ });
53
+ if (code !== 0)
54
+ throw new CliError("", code);
55
+ });
56
+ // === plan ===
57
+ const planCmd = program.command("plan").description("Non-executing command plans");
58
+ planCmd
59
+ .command("run ios")
60
+ .description("Show what an iOS run would do")
61
+ .action(async () => {
62
+ const plan = createIosRunPlan(ctx.context());
63
+ emitPlan(ctx, plan);
64
+ });
65
+ planCmd
66
+ .command("maestro")
67
+ .description("Show what Maestro flows would run")
68
+ .action(async () => {
69
+ const plan = createMaestroPlan(ctx.context());
70
+ emitPlan(ctx, plan);
71
+ });
72
+ // === build / test / clean ===
73
+ const buildOpts = [
74
+ new Option("--scheme <scheme>", "Xcode scheme name").default(process.env.CHOFI_SCHEME, "from CHOFI_SCHEME env var"),
75
+ new Option("--workspace <path>", "Xcode workspace path").default(process.env.CHOFI_WORKSPACE, "from CHOFI_WORKSPACE env var"),
76
+ new Option("--project <path>", "Xcode project path").default(process.env.CHOFI_PROJECT, "from CHOFI_PROJECT env var"),
77
+ new Option("--destination <dest>", "xcodebuild destination").default(process.env.CHOFI_DESTINATION, "from CHOFI_DESTINATION env var")
78
+ ];
79
+ program
80
+ .command("build")
81
+ .description("Build the project via xcodebuild")
82
+ .addOption(buildOpts[0])
83
+ .addOption(buildOpts[1])
84
+ .addOption(buildOpts[2])
85
+ .addOption(buildOpts[3])
86
+ .option("--progress", "Stream build phases as they happen")
87
+ .action(async (options) => {
88
+ const scheme = options.scheme;
89
+ if (!scheme) {
90
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "summary", phase: "build", status: "failed", data: { message: "--scheme is required (or set CHOFI_SCHEME)" } }));
91
+ throw new CliError("", 2);
92
+ }
93
+ if (options.progress) {
94
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "command_started", phase: "build", data: { cwd: ctx.context().mobilePath, scheme } }));
95
+ try {
96
+ const c = ctx.context();
97
+ await ctx.runtime.buildWithProgress(c.mobilePath, scheme, (phase) => {
98
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "command_completed", phase: "build.progress", status: "passed", data: phase }));
99
+ }, { destination: options.destination, workspace: options.workspace, project: options.project });
100
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "command_completed", phase: "build", status: "passed", data: {} }));
101
+ }
102
+ catch (error) {
103
+ const chofiError = error instanceof ChofiError ? error : new ChofiError(error instanceof Error ? error.message : String(error), "UNKNOWN_ERROR");
104
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "command_failed", phase: "build", status: "failed", errorType: classifyError(chofiError).code, data: { message: chofiError.message, recoverySuggestion: classifyError(chofiError).recoverySuggestion } }));
105
+ throw new CliError("", 1);
106
+ }
107
+ }
108
+ else {
109
+ await runExecution({
110
+ clock: ctx.clock,
111
+ writer: ctx.writer,
112
+ phase: "build",
113
+ action: async () => {
114
+ const c = ctx.context();
115
+ await ctx.runtime.build(c.mobilePath, scheme, { destination: options.destination, workspace: options.workspace, project: options.project });
116
+ }
117
+ });
118
+ }
119
+ });
120
+ const testCmd = program
121
+ .command("test")
122
+ .description("Run tests via xcodebuild")
123
+ .addOption(buildOpts[0])
124
+ .addOption(buildOpts[1])
125
+ .addOption(buildOpts[2])
126
+ .addOption(buildOpts[3])
127
+ .option("--with-results", "Return structured test results via xcresulttool")
128
+ .option("--only <tests...>", "Run only specific tests (e.g., MyTests/LoginTests)")
129
+ .option("--skip <tests...>", "Skip specific tests")
130
+ .option("--retry", "Retry failed tests once")
131
+ .option("--progress", "Stream test progress in real-time")
132
+ .action(async (options) => {
133
+ const scheme = options.scheme;
134
+ if (!scheme) {
135
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "summary", phase: "test", status: "failed", data: { message: "--scheme is required (or set CHOFI_SCHEME)" } }));
136
+ throw new CliError("", 2);
137
+ }
138
+ const testOpts = {
139
+ destination: options.destination,
140
+ workspace: options.workspace,
141
+ project: options.project,
142
+ only: options.only,
143
+ skip: options.skip,
144
+ retry: options.retry
145
+ };
146
+ if (options.progress) {
147
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "command_started", phase: "test", data: { cwd: ctx.context().mobilePath, scheme } }));
148
+ try {
149
+ const c = ctx.context();
150
+ await ctx.runtime.testWithProgress(c.mobilePath, scheme, (progress) => {
151
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "command_completed", phase: "test.progress", status: "passed", data: progress }));
152
+ }, testOpts);
153
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "command_completed", phase: "test", status: "passed", data: {} }));
154
+ }
155
+ catch (error) {
156
+ const chofiError = error instanceof ChofiError ? error : new ChofiError(error instanceof Error ? error.message : String(error), "UNKNOWN_ERROR");
157
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "command_failed", phase: "test", status: "failed", errorType: classifyError(chofiError).code, data: { message: chofiError.message, recoverySuggestion: classifyError(chofiError).recoverySuggestion } }));
158
+ throw new CliError("", 1);
159
+ }
160
+ }
161
+ else if (options.withResults) {
162
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "command_started", phase: "test", data: { cwd: ctx.context().mobilePath, scheme } }));
163
+ try {
164
+ const c = ctx.context();
165
+ const results = await ctx.runtime.testWithResults(c.mobilePath, scheme, testOpts);
166
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "command_completed", phase: "test", status: "passed", data: { results } }));
167
+ }
168
+ catch (error) {
169
+ const chofiError = error instanceof ChofiError ? error : new ChofiError(error instanceof Error ? error.message : String(error), "UNKNOWN_ERROR");
170
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "command_failed", phase: "test", status: "failed", errorType: classifyError(chofiError).code, data: { message: chofiError.message, recoverySuggestion: classifyError(chofiError).recoverySuggestion } }));
171
+ throw new CliError("", 1);
172
+ }
173
+ }
174
+ else {
175
+ await runExecution({
176
+ clock: ctx.clock,
177
+ writer: ctx.writer,
178
+ phase: "test",
179
+ action: async () => {
180
+ const c = ctx.context();
181
+ await ctx.runtime.test(c.mobilePath, scheme, testOpts);
182
+ }
183
+ });
184
+ }
185
+ });
186
+ testCmd
187
+ .command("discover")
188
+ .description("Discover test cases without running them")
189
+ .addOption(buildOpts[0])
190
+ .addOption(buildOpts[1])
191
+ .addOption(buildOpts[2])
192
+ .addOption(buildOpts[3])
193
+ .action(async (options) => {
194
+ const scheme = options.scheme;
195
+ if (!scheme) {
196
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "summary", phase: "test.discover", status: "failed", data: { message: "--scheme is required (or set CHOFI_SCHEME)" } }));
197
+ throw new CliError("", 2);
198
+ }
199
+ await runExecution({
200
+ clock: ctx.clock,
201
+ writer: ctx.writer,
202
+ phase: "test.discover",
203
+ action: async () => {
204
+ const c = ctx.context();
205
+ const tests = await ctx.runtime.discoverTests(c.mobilePath, scheme, { destination: options.destination, workspace: options.workspace, project: options.project });
206
+ return { tests };
207
+ },
208
+ mapResult: (result) => ({ tests: result.tests })
209
+ });
210
+ });
211
+ testCmd
212
+ .command("results <path>")
213
+ .description("Parse test results from an .xcresult bundle")
214
+ .action(async (path) => {
215
+ await runExecution({
216
+ clock: ctx.clock,
217
+ writer: ctx.writer,
218
+ phase: "test.results",
219
+ action: async () => {
220
+ const results = await ctx.runtime.getTestResults(path);
221
+ return { results };
222
+ },
223
+ mapResult: (result) => ({ results: result.results })
224
+ });
225
+ });
226
+ program
227
+ .command("clean")
228
+ .description("Clean build artifacts via xcodebuild")
229
+ .addOption(buildOpts[0])
230
+ .addOption(buildOpts[1])
231
+ .addOption(buildOpts[2])
232
+ .option("--derived-data", "Delete DerivedData directory")
233
+ .option("--xcode-derived-data", "Delete Xcode DerivedData")
234
+ .option("--xcode-cache", "Delete Xcode cache")
235
+ .action(async (options) => {
236
+ const scheme = options.scheme;
237
+ if (!scheme && !options.derivedData && !options.xcodeDerivedData && !options.xcodeCache) {
238
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "summary", phase: "clean", status: "failed", data: { message: "--scheme is required (or set CHOFI_SCHEME), or use --derived-data/--xcode-cache" } }));
239
+ throw new CliError("", 2);
240
+ }
241
+ await runExecution({
242
+ clock: ctx.clock,
243
+ writer: ctx.writer,
244
+ phase: "clean",
245
+ action: async () => {
246
+ const c = ctx.context();
247
+ await ctx.runtime.clean(c.mobilePath, scheme ?? "", { workspace: options.workspace, project: options.project, derivedData: options.derivedData, xcodeDerivedData: options.xcodeDerivedData, xcodeCache: options.xcodeCache });
248
+ }
249
+ });
250
+ });
251
+ // === logs ===
252
+ program
253
+ .command("logs <udid>")
254
+ .description("Stream logs from a device or simulator")
255
+ .option("--timeout <ms>", "Timeout in milliseconds", "30000")
256
+ .action(async (udid, options) => {
257
+ const timeoutMs = Number.parseInt(options.timeout, 10);
258
+ await runExecution({
259
+ clock: ctx.clock,
260
+ writer: ctx.writer,
261
+ phase: "logs.stream",
262
+ action: async () => {
263
+ const output = await ctx.runtime.streamLogs(udid, timeoutMs);
264
+ return { output };
265
+ },
266
+ mapResult: (result) => ({ logs: result.output })
267
+ });
268
+ });
269
+ // === stop ===
270
+ program
271
+ .command("stop <udid> [bundleId]")
272
+ .description("Terminate a running app (omit bundleId with --all to stop all)")
273
+ .option("--all", "Stop all running apps")
274
+ .option("--force", "Force kill (SIGKILL) instead of graceful termination")
275
+ .action(async (udid, bundleId, options) => {
276
+ if (options.all) {
277
+ await runExecution({
278
+ clock: ctx.clock,
279
+ writer: ctx.writer,
280
+ phase: "app.terminate-all",
281
+ action: async () => {
282
+ await ctx.runtime.appTerminateAll(udid, options.force ?? false);
283
+ }
284
+ });
285
+ return;
286
+ }
287
+ if (!bundleId) {
288
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "summary", phase: "app.terminate", status: "failed", data: { message: "bundleId is required (or use --all)" } }));
289
+ throw new CliError("", 2);
290
+ }
291
+ await runSimAction({
292
+ clock: ctx.clock,
293
+ runtime: ctx.runtime,
294
+ writer: ctx.writer,
295
+ phase: "app.terminate",
296
+ action: () => ctx.runtime.appTerminate(udid, bundleId, options.force ?? false),
297
+ target: udid
298
+ });
299
+ });
300
+ // === sim ===
301
+ const simCmd = program.command("sim").description("Simulator management");
302
+ simCmd
303
+ .command("list")
304
+ .description("List available simulators")
305
+ .action(async () => {
306
+ await runSimList({ clock: ctx.clock, runtime: ctx.runtime, writer: ctx.writer });
307
+ });
308
+ simCmd
309
+ .command("boot <target>")
310
+ .description("Boot a simulator by name or UDID")
311
+ .action(async (target) => {
312
+ await runSimAction({
313
+ clock: ctx.clock,
314
+ runtime: ctx.runtime,
315
+ writer: ctx.writer,
316
+ phase: "sim.boot",
317
+ action: () => ctx.runtime.simBoot(target),
318
+ target
319
+ });
320
+ });
321
+ simCmd
322
+ .command("shutdown <target>")
323
+ .description("Shutdown a simulator")
324
+ .action(async (target) => {
325
+ await runSimAction({
326
+ clock: ctx.clock,
327
+ runtime: ctx.runtime,
328
+ writer: ctx.writer,
329
+ phase: "sim.shutdown",
330
+ action: () => ctx.runtime.simShutdown(target),
331
+ target
332
+ });
333
+ });
334
+ simCmd
335
+ .command("open <target>")
336
+ .description("Open Simulator app for a device")
337
+ .action(async (target) => {
338
+ await runSimAction({
339
+ clock: ctx.clock,
340
+ runtime: ctx.runtime,
341
+ writer: ctx.writer,
342
+ phase: "sim.open",
343
+ action: () => ctx.runtime.simOpen(target),
344
+ target
345
+ });
346
+ });
347
+ simCmd
348
+ .command("erase <target>")
349
+ .description("Erase simulator contents and settings")
350
+ .option("--confirm", "Explicitly confirm destructive operation")
351
+ .option("--yes", "Alias for --confirm")
352
+ .action(async (target, options) => {
353
+ if (!options.confirm && !options.yes) {
354
+ ctx.writer.write(createChofiEvent({
355
+ clock: ctx.clock,
356
+ event: "summary",
357
+ phase: "sim.erase",
358
+ status: "failed",
359
+ data: { message: requireConfirmation("sim.erase") }
360
+ }));
361
+ throw new CliError("", 1);
362
+ }
363
+ await runSimAction({
364
+ clock: ctx.clock,
365
+ runtime: ctx.runtime,
366
+ writer: ctx.writer,
367
+ phase: "sim.erase",
368
+ action: () => ctx.runtime.simErase(target),
369
+ target
370
+ });
371
+ });
372
+ simCmd
373
+ .command("screenshot <target>")
374
+ .description("Take a screenshot of a simulator")
375
+ .option("--output <path>", "Output file path", "./screenshot.png")
376
+ .option("--format <format>", "Screenshot format (png/jpeg)", "png")
377
+ .action(async (target, options) => {
378
+ await runExecution({
379
+ clock: ctx.clock,
380
+ writer: ctx.writer,
381
+ phase: "sim.screenshot",
382
+ action: async () => {
383
+ if (options.format === "jpeg" || options.format === "jpg") {
384
+ const result = await ctx.runtime.apple.screenshotToBuffer(target, "jpeg");
385
+ if (result.exitCode !== 0) {
386
+ throw new ToolOutputError("simctl", result.stderr || "Screenshot failed");
387
+ }
388
+ return { outputPath: options.output, data: result.stdout };
389
+ }
390
+ await ctx.runtime.simScreenshot(target, options.output);
391
+ return { outputPath: options.output };
392
+ },
393
+ mapResult: (result) => ({ outputPath: result.outputPath })
394
+ });
395
+ });
396
+ simCmd
397
+ .command("create <name> <deviceType> <runtime>")
398
+ .description("Create a new simulator")
399
+ .action(async (name, deviceType, runtime) => {
400
+ await runExecution({
401
+ clock: ctx.clock,
402
+ writer: ctx.writer,
403
+ phase: "sim.create",
404
+ action: async () => {
405
+ const udid = await ctx.runtime.simCreate(name, deviceType, runtime);
406
+ return { udid };
407
+ },
408
+ mapResult: (result) => ({ udid: result.udid })
409
+ });
410
+ });
411
+ simCmd
412
+ .command("delete <target>")
413
+ .description("Delete a simulator")
414
+ .option("--confirm", "Explicitly confirm destructive operation")
415
+ .option("--yes", "Alias for --confirm")
416
+ .action(async (target, options) => {
417
+ if (!options.confirm && !options.yes) {
418
+ ctx.writer.write(createChofiEvent({
419
+ clock: ctx.clock,
420
+ event: "summary",
421
+ phase: "sim.delete",
422
+ status: "failed",
423
+ data: { message: requireConfirmation("sim.delete") }
424
+ }));
425
+ throw new CliError("", 1);
426
+ }
427
+ await runSimAction({
428
+ clock: ctx.clock,
429
+ runtime: ctx.runtime,
430
+ writer: ctx.writer,
431
+ phase: "sim.delete",
432
+ action: () => ctx.runtime.simDelete(target),
433
+ target
434
+ });
435
+ });
436
+ simCmd
437
+ .command("prune")
438
+ .description("Delete unavailable simulators")
439
+ .option("--confirm", "Explicitly confirm destructive operation")
440
+ .option("--yes", "Alias for --confirm")
441
+ .action(async (options) => {
442
+ if (!options.confirm && !options.yes) {
443
+ ctx.writer.write(createChofiEvent({
444
+ clock: ctx.clock,
445
+ event: "summary",
446
+ phase: "sim.prune",
447
+ status: "failed",
448
+ data: { message: requireConfirmation("sim.prune") }
449
+ }));
450
+ throw new CliError("", 1);
451
+ }
452
+ await runSimAction({
453
+ clock: ctx.clock,
454
+ runtime: ctx.runtime,
455
+ writer: ctx.writer,
456
+ phase: "sim.prune",
457
+ action: () => ctx.runtime.simPrune(),
458
+ target: "unavailable"
459
+ });
460
+ });
461
+ simCmd
462
+ .command("runtime")
463
+ .description("List available simulator runtimes")
464
+ .action(async () => {
465
+ await runExecution({
466
+ clock: ctx.clock,
467
+ writer: ctx.writer,
468
+ phase: "sim.runtime",
469
+ action: async () => {
470
+ const runtimes = await ctx.runtime.simRuntimeList();
471
+ return { runtimes };
472
+ },
473
+ mapResult: (result) => ({ runtimes: result.runtimes })
474
+ });
475
+ });
476
+ simCmd
477
+ .command("device-types")
478
+ .description("List available simulator device types")
479
+ .action(async () => {
480
+ await runExecution({
481
+ clock: ctx.clock,
482
+ writer: ctx.writer,
483
+ phase: "sim.device-types",
484
+ action: async () => {
485
+ const types = await ctx.runtime.simDeviceTypeList();
486
+ return { types };
487
+ },
488
+ mapResult: (result) => ({ types: result.types })
489
+ });
490
+ });
491
+ simCmd
492
+ .command("clone <source> <name>")
493
+ .description("Clone a simulator")
494
+ .action(async (source, name) => {
495
+ await runExecution({
496
+ clock: ctx.clock,
497
+ writer: ctx.writer,
498
+ phase: "sim.clone",
499
+ action: async () => {
500
+ const udid = await ctx.runtime.simClone(source, name);
501
+ return { udid };
502
+ },
503
+ mapResult: (result) => ({ udid: result.udid })
504
+ });
505
+ });
506
+ simCmd
507
+ .command("clear-cache <target>")
508
+ .description("Clear simulator caches")
509
+ .action(async (target) => {
510
+ await runSimAction({
511
+ clock: ctx.clock,
512
+ runtime: ctx.runtime,
513
+ writer: ctx.writer,
514
+ phase: "sim.clear-cache",
515
+ action: () => ctx.runtime.simClearCache(target),
516
+ target
517
+ });
518
+ });
519
+ simCmd
520
+ .command("set-appearance <target> <appearance>")
521
+ .description("Set simulator appearance (dark/light)")
522
+ .action(async (target, appearance) => {
523
+ if (appearance !== "dark" && appearance !== "light") {
524
+ ctx.writer.write(createChofiEvent({ clock: ctx.clock, event: "summary", phase: "sim.set-appearance", status: "failed", data: { message: "appearance must be 'dark' or 'light'" } }));
525
+ throw new CliError("", 2);
526
+ }
527
+ await runSimAction({
528
+ clock: ctx.clock,
529
+ runtime: ctx.runtime,
530
+ writer: ctx.writer,
531
+ phase: "sim.set-appearance",
532
+ action: () => ctx.runtime.simSetAppearance(target, appearance),
533
+ target
534
+ });
535
+ });
536
+ simCmd
537
+ .command("record <udid>")
538
+ .description("Record simulator video")
539
+ .option("--output <path>", "Output file path", "./recording.mp4")
540
+ .option("--duration <seconds>", "Recording duration in seconds", "10")
541
+ .action(async (udid, options) => {
542
+ await runExecution({
543
+ clock: ctx.clock,
544
+ writer: ctx.writer,
545
+ phase: "sim.record",
546
+ action: async () => {
547
+ await ctx.runtime.simStartRecording(udid, options.output);
548
+ const durationMs = Number.parseInt(options.duration, 10) * 1000;
549
+ await new Promise((resolve) => setTimeout(resolve, durationMs));
550
+ await ctx.runtime.simStopRecording(udid);
551
+ return { outputPath: options.output };
552
+ },
553
+ mapResult: (result) => ({ outputPath: result.outputPath })
554
+ });
555
+ });
556
+ simCmd
557
+ .command("add-media <udid> <paths...>")
558
+ .description("Add photos/videos to simulator Photos app")
559
+ .action(async (udid, paths) => {
560
+ await runSimAction({
561
+ clock: ctx.clock,
562
+ runtime: ctx.runtime,
563
+ writer: ctx.writer,
564
+ phase: "sim.add-media",
565
+ action: () => ctx.runtime.simAddMedia(udid, paths),
566
+ target: udid
567
+ });
568
+ });
569
+ // === device ===
570
+ const deviceCmd = program.command("device").description("Physical device management");
571
+ deviceCmd
572
+ .command("list")
573
+ .description("List physical devices")
574
+ .option("--platform <platform>", "Filter by platform (iOS, watchOS)")
575
+ .action(async (options) => {
576
+ await runExecution({
577
+ clock: ctx.clock,
578
+ writer: ctx.writer,
579
+ phase: "device.list",
580
+ action: async () => {
581
+ const devices = await ctx.runtime.deviceList(options.platform);
582
+ return { devices };
583
+ },
584
+ mapResult: (result) => ({ devices: result.devices })
585
+ });
586
+ });
587
+ // === apps ===
588
+ const appsCmd = program.command("apps").description("App management");
589
+ appsCmd
590
+ .command("list <udid>")
591
+ .description("List running apps on a simulator/device")
592
+ .action(async (udid) => {
593
+ await runExecution({
594
+ clock: ctx.clock,
595
+ writer: ctx.writer,
596
+ phase: "apps.list",
597
+ action: async () => {
598
+ const apps = await ctx.runtime.appListRunning(udid);
599
+ return { apps };
600
+ },
601
+ mapResult: (result) => ({ apps: result.apps })
602
+ });
603
+ });
604
+ appsCmd
605
+ .command("prune <udid>")
606
+ .description("Remove stale/uninstalled app entries from registry")
607
+ .action(async (udid) => {
608
+ await runExecution({
609
+ clock: ctx.clock,
610
+ writer: ctx.writer,
611
+ phase: "apps.prune",
612
+ action: async () => {
613
+ const apps = await ctx.runtime.appListRunning(udid);
614
+ const pruned = apps.filter((a) => a.pid > 0);
615
+ return { pruned: pruned.length };
616
+ },
617
+ mapResult: (result) => ({ pruned: result.pruned })
618
+ });
619
+ });
620
+ // === app ===
621
+ const appCmd = program.command("app").description("App lifecycle on simulator/device");
622
+ appCmd
623
+ .command("install <udid> <appPath>")
624
+ .description("Install app on simulator/device")
625
+ .action(async (udid, appPath) => {
626
+ await runSimAction({
627
+ clock: ctx.clock,
628
+ runtime: ctx.runtime,
629
+ writer: ctx.writer,
630
+ phase: "app.install",
631
+ action: () => ctx.runtime.appInstall(udid, appPath),
632
+ target: udid
633
+ });
634
+ });
635
+ appCmd
636
+ .command("launch <udid> <bundleId>")
637
+ .description("Launch app on simulator/device")
638
+ .action(async (udid, bundleId) => {
639
+ await runSimAction({
640
+ clock: ctx.clock,
641
+ runtime: ctx.runtime,
642
+ writer: ctx.writer,
643
+ phase: "app.launch",
644
+ action: () => ctx.runtime.appLaunch(udid, bundleId),
645
+ target: udid
646
+ });
647
+ });
648
+ appCmd
649
+ .command("terminate <udid> <bundleId>")
650
+ .description("Terminate app on simulator/device")
651
+ .option("--force", "Force kill (SIGKILL)")
652
+ .action(async (udid, bundleId, options) => {
653
+ await runSimAction({
654
+ clock: ctx.clock,
655
+ runtime: ctx.runtime,
656
+ writer: ctx.writer,
657
+ phase: "app.terminate",
658
+ action: () => ctx.runtime.appTerminate(udid, bundleId, options.force ?? false),
659
+ target: udid
660
+ });
661
+ });
662
+ appCmd
663
+ .command("uninstall <udid> <bundleId>")
664
+ .description("Uninstall app from simulator/device")
665
+ .action(async (udid, bundleId) => {
666
+ await runSimAction({
667
+ clock: ctx.clock,
668
+ runtime: ctx.runtime,
669
+ writer: ctx.writer,
670
+ phase: "app.uninstall",
671
+ action: () => ctx.runtime.appUninstall(udid, bundleId),
672
+ target: udid
673
+ });
674
+ });
675
+ // === project ===
676
+ const projectCmd = program.command("project").description("Project introspection");
677
+ projectCmd
678
+ .command("build-config")
679
+ .description("List available build configurations")
680
+ .addOption(buildOpts[1])
681
+ .addOption(buildOpts[2])
682
+ .action(async (options) => {
683
+ await runExecution({
684
+ clock: ctx.clock,
685
+ writer: ctx.writer,
686
+ phase: "project.build-config",
687
+ action: async () => {
688
+ const c = ctx.context();
689
+ const configs = await ctx.runtime.projectBuildConfigs(c.mobilePath, { workspace: options.workspace, project: options.project });
690
+ return { configs };
691
+ },
692
+ mapResult: (result) => ({ configs: result.configs })
693
+ });
694
+ });
695
+ projectCmd
696
+ .command("schemes")
697
+ .description("Auto-detect available schemes")
698
+ .addOption(buildOpts[1])
699
+ .addOption(buildOpts[2])
700
+ .action(async (options) => {
701
+ await runExecution({
702
+ clock: ctx.clock,
703
+ writer: ctx.writer,
704
+ phase: "project.schemes",
705
+ action: async () => {
706
+ const c = ctx.context();
707
+ const schemes = await ctx.runtime.detectSchemes(c.mobilePath, { workspace: options.workspace, project: options.project });
708
+ return { schemes };
709
+ },
710
+ mapResult: (result) => ({ schemes: result.schemes })
711
+ });
712
+ });
713
+ // === run ===
714
+ const runCmd = program.command("run").description("Run the app on a platform");
715
+ runCmd
716
+ .command("ios")
717
+ .description("Run iOS app (requires --confirm)")
718
+ .option("--confirm", "Explicitly confirm")
719
+ .option("--yes", "Alias for --confirm")
720
+ .option("--no-build", "Skip build, launch existing app")
721
+ .option("--launch-env <env...>", "Environment variables for launch (KEY=VALUE)")
722
+ .action(async (options) => {
723
+ if (!options.confirm && !options.yes) {
724
+ ctx.writer.write(createChofiEvent({
725
+ clock: ctx.clock,
726
+ event: "summary",
727
+ phase: "run.ios",
728
+ status: "failed",
729
+ data: { message: requireConfirmation("run.ios") }
730
+ }));
731
+ throw new CliError("", 1);
732
+ }
733
+ await runExecution({
734
+ clock: ctx.clock,
735
+ writer: ctx.writer,
736
+ phase: "run.ios",
737
+ action: async () => {
738
+ const c = ctx.context();
739
+ if (!options.noBuild) {
740
+ await ctx.runtime.prebuild(c.repoRoot, "ios");
741
+ }
742
+ await ctx.runtime.runExpoIos(c.mobilePath);
743
+ }
744
+ });
745
+ });
746
+ runCmd
747
+ .command("android")
748
+ .description("Run Android app (requires --confirm)")
749
+ .option("--confirm", "Explicitly confirm")
750
+ .option("--yes", "Alias for --confirm")
751
+ .action(async (options) => {
752
+ if (!options.confirm && !options.yes) {
753
+ ctx.writer.write(createChofiEvent({
754
+ clock: ctx.clock,
755
+ event: "summary",
756
+ phase: "run.android",
757
+ status: "failed",
758
+ data: { message: requireConfirmation("run.android") }
759
+ }));
760
+ throw new CliError("", 1);
761
+ }
762
+ await runExecution({
763
+ clock: ctx.clock,
764
+ writer: ctx.writer,
765
+ phase: "run.android",
766
+ action: async () => {
767
+ const c = ctx.context();
768
+ await ctx.runtime.prebuild(c.repoRoot, "android");
769
+ await ctx.runtime.runExpoAndroid(c.mobilePath);
770
+ }
771
+ });
772
+ });
773
+ // === maestro ===
774
+ const maestroCmd = program.command("maestro").description("Maestro UI automation");
775
+ maestroCmd
776
+ .command("check")
777
+ .description("Check Maestro installation")
778
+ .action(async () => {
779
+ const availability = ctx.context().maestro;
780
+ writeToolCheck(ctx.writer, ctx.clock, availability);
781
+ if (availability.status !== "available") {
782
+ throw new CliError("", 1);
783
+ }
784
+ });
785
+ maestroCmd
786
+ .command("run <flow>")
787
+ .description("Run a Maestro flow")
788
+ .action(async (flow) => {
789
+ await runExecution({
790
+ clock: ctx.clock,
791
+ writer: ctx.writer,
792
+ phase: "maestro.run",
793
+ action: async () => {
794
+ const c = ctx.context();
795
+ await ctx.runtime.runMaestroTest(flow, { cwd: c.repoRoot });
796
+ }
797
+ });
798
+ });
799
+ maestroCmd
800
+ .command("continuous <flow>")
801
+ .description("Run a Maestro flow in continuous mode")
802
+ .action(async (flow) => {
803
+ await runExecution({
804
+ clock: ctx.clock,
805
+ writer: ctx.writer,
806
+ phase: "maestro.continuous",
807
+ action: async () => {
808
+ const c = ctx.context();
809
+ await ctx.runtime.runMaestroContinuous(flow, { cwd: c.repoRoot });
810
+ }
811
+ });
812
+ });
813
+ maestroCmd
814
+ .command("hierarchy")
815
+ .description("Get UI hierarchy")
816
+ .action(async () => {
817
+ await runExecution({
818
+ clock: ctx.clock,
819
+ writer: ctx.writer,
820
+ phase: "maestro.hierarchy",
821
+ action: async () => {
822
+ const c = ctx.context();
823
+ const output = await ctx.runtime.runMaestroHierarchy({ cwd: c.repoRoot });
824
+ return { output };
825
+ },
826
+ mapResult: (result) => ({ hierarchy: result.output })
827
+ });
828
+ });
829
+ maestroCmd
830
+ .command("record <flow>")
831
+ .description("Record a Maestro flow")
832
+ .action(async (flow) => {
833
+ await runExecution({
834
+ clock: ctx.clock,
835
+ writer: ctx.writer,
836
+ phase: "maestro.record",
837
+ action: async () => {
838
+ const c = ctx.context();
839
+ await ctx.runtime.runMaestroRecord(flow, { cwd: c.repoRoot });
840
+ }
841
+ });
842
+ });
843
+ maestroCmd
844
+ .command("driver-setup")
845
+ .description("Setup Maestro driver")
846
+ .action(async () => {
847
+ await runExecution({
848
+ clock: ctx.clock,
849
+ writer: ctx.writer,
850
+ phase: "maestro.driver-setup",
851
+ action: async () => {
852
+ const c = ctx.context();
853
+ await ctx.runtime.maestroDriverSetup(c.repoRoot);
854
+ }
855
+ });
856
+ });
857
+ maestroCmd
858
+ .command("start-device")
859
+ .description("Start a simulator or emulator for Maestro")
860
+ .option("--platform <platform>", "Platform: ios or android")
861
+ .option("--device <device>", "Device identifier")
862
+ .action(async (options) => {
863
+ await runExecution({
864
+ clock: ctx.clock,
865
+ writer: ctx.writer,
866
+ phase: "maestro.start-device",
867
+ action: async () => {
868
+ const c = ctx.context();
869
+ await ctx.runtime.maestroStartDevice({
870
+ cwd: c.repoRoot,
871
+ platform: options.platform,
872
+ device: options.device
873
+ });
874
+ }
875
+ });
876
+ });
877
+ // === idb ===
878
+ const idbCmd = program.command("idb").description("idb integration — accessibility, UI, crashes, permissions, location");
879
+ idbCmd
880
+ .command("accessibility <udid>")
881
+ .description("Dump accessibility tree via idb")
882
+ .option("--point <x,y>", "Describe element at point (format: x,y)")
883
+ .action(async (udid, options) => {
884
+ await runExecution({
885
+ clock: ctx.clock,
886
+ writer: ctx.writer,
887
+ phase: "idb.accessibility",
888
+ action: async () => {
889
+ if (options.point) {
890
+ const [x, y] = options.point.split(",").map(Number);
891
+ const element = await ctx.runtime.idbDescribePoint(udid, x, y);
892
+ return { element };
893
+ }
894
+ const tree = await ctx.runtime.idbDescribeAll(udid);
895
+ return { tree };
896
+ },
897
+ mapResult: (result) => (options.point ? result.element : result.tree)
898
+ });
899
+ });
900
+ idbCmd
901
+ .command("tap <udid> <x> <y>")
902
+ .description("Tap at screen coordinates via idb")
903
+ .action(async (udid, x, y) => {
904
+ await runExecution({
905
+ clock: ctx.clock,
906
+ writer: ctx.writer,
907
+ phase: "idb.tap",
908
+ action: async () => {
909
+ await ctx.runtime.idbTap(udid, Number(x), Number(y));
910
+ }
911
+ });
912
+ });
913
+ idbCmd
914
+ .command("swipe <udid> <x1> <y1> <x2> <y2>")
915
+ .description("Swipe from point to point via idb")
916
+ .action(async (udid, x1, y1, x2, y2) => {
917
+ await runExecution({
918
+ clock: ctx.clock,
919
+ writer: ctx.writer,
920
+ phase: "idb.swipe",
921
+ action: async () => {
922
+ await ctx.runtime.idbSwipe(udid, Number(x1), Number(y1), Number(x2), Number(y2));
923
+ }
924
+ });
925
+ });
926
+ idbCmd
927
+ .command("button <udid> <button>")
928
+ .description("Press a hardware button via idb (home, volume_up, volume_down, lock)")
929
+ .action(async (udid, button) => {
930
+ await runExecution({
931
+ clock: ctx.clock,
932
+ writer: ctx.writer,
933
+ phase: "idb.button",
934
+ action: async () => {
935
+ await ctx.runtime.idbPressButton(udid, button);
936
+ }
937
+ });
938
+ });
939
+ idbCmd
940
+ .command("text <udid> <text>")
941
+ .description("Input text via idb")
942
+ .action(async (udid, text) => {
943
+ await runExecution({
944
+ clock: ctx.clock,
945
+ writer: ctx.writer,
946
+ phase: "idb.text",
947
+ action: async () => {
948
+ await ctx.runtime.idbInputText(udid, text);
949
+ }
950
+ });
951
+ });
952
+ idbCmd
953
+ .command("crash-list <udid>")
954
+ .description("List crash logs via idb")
955
+ .action(async (udid) => {
956
+ await runExecution({
957
+ clock: ctx.clock,
958
+ writer: ctx.writer,
959
+ phase: "idb.crash-list",
960
+ action: async () => {
961
+ const crashes = await ctx.runtime.idbListCrashes(udid);
962
+ return { crashes };
963
+ },
964
+ mapResult: (result) => ({ crashes: result.crashes })
965
+ });
966
+ });
967
+ idbCmd
968
+ .command("crash-show <udid> <name>")
969
+ .description("Show crash log contents via idb")
970
+ .action(async (udid, name) => {
971
+ await runExecution({
972
+ clock: ctx.clock,
973
+ writer: ctx.writer,
974
+ phase: "idb.crash-show",
975
+ action: async () => {
976
+ const content = await ctx.runtime.idbShowCrash(udid, name);
977
+ return { content };
978
+ },
979
+ mapResult: (result) => ({ content: result.content })
980
+ });
981
+ });
982
+ idbCmd
983
+ .command("approve <udid> <bundleId> <permissions...>")
984
+ .description("Approve permissions via idb (photos, camera, contacts, url, location, notification)")
985
+ .action(async (udid, bundleId, permissions) => {
986
+ await runExecution({
987
+ clock: ctx.clock,
988
+ writer: ctx.writer,
989
+ phase: "idb.approve",
990
+ action: async () => {
991
+ await ctx.runtime.idbApprove(udid, bundleId, permissions);
992
+ }
993
+ });
994
+ });
995
+ idbCmd
996
+ .command("location <udid> <latitude> <longitude>")
997
+ .description("Set simulator location via idb")
998
+ .action(async (udid, latitude, longitude) => {
999
+ await runExecution({
1000
+ clock: ctx.clock,
1001
+ writer: ctx.writer,
1002
+ phase: "idb.location",
1003
+ action: async () => {
1004
+ await ctx.runtime.idbSetLocation(udid, Number(latitude), Number(longitude));
1005
+ }
1006
+ });
1007
+ });
1008
+ idbCmd
1009
+ .command("focus <udid>")
1010
+ .description("Bring simulator window to front via idb")
1011
+ .action(async (udid) => {
1012
+ await runExecution({
1013
+ clock: ctx.clock,
1014
+ writer: ctx.writer,
1015
+ phase: "idb.focus",
1016
+ action: async () => {
1017
+ await ctx.runtime.idbFocus(udid);
1018
+ }
1019
+ });
1020
+ });
1021
+ // === config ===
1022
+ const configCmd = program.command("config").description("Configuration management");
1023
+ configCmd
1024
+ .command("get <key>")
1025
+ .description("Get a config value")
1026
+ .action(async (key) => {
1027
+ const c = ctx.context();
1028
+ const config = readConfig(c.repoRoot);
1029
+ const value = config[key];
1030
+ ctx.writer.write(createChofiEvent({
1031
+ clock: ctx.clock,
1032
+ event: "summary",
1033
+ phase: "config.get",
1034
+ status: "passed",
1035
+ data: { key, value }
1036
+ }));
1037
+ });
1038
+ configCmd
1039
+ .command("set <key> <value>")
1040
+ .description("Set a config value")
1041
+ .action(async (key, value) => {
1042
+ const c = ctx.context();
1043
+ const config = readConfig(c.repoRoot);
1044
+ const updated = { ...config, [key]: value };
1045
+ writeConfig(c.repoRoot, updated);
1046
+ ctx.writer.write(createChofiEvent({
1047
+ clock: ctx.clock,
1048
+ event: "summary",
1049
+ phase: "config.set",
1050
+ status: "passed",
1051
+ data: { key, value }
1052
+ }));
1053
+ });
1054
+ configCmd
1055
+ .command("reset")
1056
+ .description("Reset config to defaults")
1057
+ .option("--confirm", "Explicitly confirm destructive operation")
1058
+ .option("--yes", "Alias for --confirm")
1059
+ .action(async (options) => {
1060
+ if (!options.confirm && !options.yes) {
1061
+ ctx.writer.write(createChofiEvent({
1062
+ clock: ctx.clock,
1063
+ event: "summary",
1064
+ phase: "config.reset",
1065
+ status: "failed",
1066
+ data: { message: requireConfirmation("config.reset") }
1067
+ }));
1068
+ throw new CliError("", 1);
1069
+ }
1070
+ const c = ctx.context();
1071
+ writeConfig(c.repoRoot, {});
1072
+ ctx.writer.write(createChofiEvent({
1073
+ clock: ctx.clock,
1074
+ event: "summary",
1075
+ phase: "config.reset",
1076
+ status: "passed",
1077
+ data: {}
1078
+ }));
1079
+ });
1080
+ return program;
1081
+ }
1082
+ export async function runChofiCli(argv, dependencies = {}) {
1083
+ const writer = dependencies.writer ?? new NdjsonEventWriter();
1084
+ const clock = dependencies.clock;
1085
+ const runtime = dependencies.runtime ??
1086
+ new RuntimeController({ spawner: dependencies.spawner });
1087
+ const context = () => discoverMobileProjectContext({
1088
+ cwd: dependencies.cwd,
1089
+ pathEnv: dependencies.pathEnv,
1090
+ toolChecker: dependencies.toolChecker
1091
+ });
1092
+ const ctx = {
1093
+ clock,
1094
+ runtime,
1095
+ writer,
1096
+ context,
1097
+ runner: dependencies.runner ?? createCommandRunner(dependencies.spawner)
1098
+ };
1099
+ const program = createProgram(ctx, dependencies);
1100
+ try {
1101
+ await program.parseAsync(argv, { from: "user" });
1102
+ return 0;
1103
+ }
1104
+ catch (error) {
1105
+ if (error instanceof CliError) {
1106
+ return error.exitCode;
1107
+ }
1108
+ if (error instanceof Error && "exitCode" in error && typeof error.exitCode === "number") {
1109
+ const isMissingJson = error.message.includes("required option '--json'");
1110
+ const code = isMissingJson ? 2 : error.exitCode === 1 && error.message.includes("unknown command") ? 2 : error.exitCode;
1111
+ writer.write(createChofiEvent({
1112
+ clock,
1113
+ event: "summary",
1114
+ phase: "usage",
1115
+ status: "failed",
1116
+ data: {
1117
+ message: isMissingJson
1118
+ ? "chofi requires --json for every command. Example: chofi context --json"
1119
+ : error.message
1120
+ }
1121
+ }));
1122
+ return code;
1123
+ }
1124
+ writer.write(createChofiEvent({
1125
+ clock,
1126
+ event: "summary",
1127
+ phase: "usage",
1128
+ status: "failed",
1129
+ data: { message: String(error) }
1130
+ }));
1131
+ return 2;
1132
+ }
1133
+ }
1134
+ function emitPlan(ctx, plan) {
1135
+ ctx.writer.write(createChofiEvent({
1136
+ clock: ctx.clock,
1137
+ event: "plan",
1138
+ phase: plan.phase,
1139
+ status: "planned",
1140
+ data: plan
1141
+ }));
1142
+ ctx.writer.write(createChofiEvent({
1143
+ clock: ctx.clock,
1144
+ event: "summary",
1145
+ phase: plan.phase,
1146
+ status: "planned",
1147
+ data: {
1148
+ commandCount: plan.commands.length,
1149
+ notes: plan.notes
1150
+ }
1151
+ }));
1152
+ }
1153
+ async function runDoctorCommand(input) {
1154
+ const checks = [];
1155
+ for (const command of doctorChecks(input.context)) {
1156
+ input.writer.write(createChofiEvent({
1157
+ clock: input.clock,
1158
+ event: "command_started",
1159
+ phase: "doctor",
1160
+ data: commandStartedData(command)
1161
+ }));
1162
+ const result = await input.runner(command);
1163
+ const status = result.exitCode === 0 ? "passed" : "failed";
1164
+ checks.push({
1165
+ name: command.name,
1166
+ status,
1167
+ exitCode: result.exitCode,
1168
+ durationMs: result.durationMs
1169
+ });
1170
+ input.writer.write(createChofiEvent({
1171
+ clock: input.clock,
1172
+ event: result.exitCode === 0 ? "command_completed" : "command_failed",
1173
+ phase: "doctor",
1174
+ status,
1175
+ data: {
1176
+ ...commandStartedData(command),
1177
+ durationMs: result.durationMs,
1178
+ exitCode: result.exitCode,
1179
+ stderr: summarizeOutput(result.stderr),
1180
+ stdout: summarizeOutput(result.stdout)
1181
+ }
1182
+ }));
1183
+ }
1184
+ const status = checks.every((check) => check.exitCode === 0)
1185
+ ? "passed"
1186
+ : "failed";
1187
+ input.writer.write(createChofiEvent({
1188
+ clock: input.clock,
1189
+ event: "summary",
1190
+ phase: "doctor",
1191
+ status,
1192
+ data: { checks }
1193
+ }));
1194
+ return status === "passed" ? 0 : 1;
1195
+ }
1196
+ async function runSimList(input) {
1197
+ try {
1198
+ const devices = await input.runtime.simList();
1199
+ input.writer.write(createChofiEvent({
1200
+ clock: input.clock,
1201
+ event: "summary",
1202
+ phase: "sim.list",
1203
+ status: "passed",
1204
+ data: { devices }
1205
+ }));
1206
+ return 0;
1207
+ }
1208
+ catch (error) {
1209
+ input.writer.write(createChofiEvent({
1210
+ clock: input.clock,
1211
+ event: "summary",
1212
+ phase: "sim.list",
1213
+ status: "failed",
1214
+ data: { message: String(error) }
1215
+ }));
1216
+ return 1;
1217
+ }
1218
+ }
1219
+ async function runSimAction(input) {
1220
+ input.writer.write(createChofiEvent({
1221
+ clock: input.clock,
1222
+ event: "command_started",
1223
+ phase: input.phase,
1224
+ data: { target: input.target }
1225
+ }));
1226
+ try {
1227
+ await input.action();
1228
+ input.writer.write(createChofiEvent({
1229
+ clock: input.clock,
1230
+ event: "command_completed",
1231
+ phase: input.phase,
1232
+ status: "passed",
1233
+ data: { target: input.target }
1234
+ }));
1235
+ return 0;
1236
+ }
1237
+ catch (error) {
1238
+ input.writer.write(createChofiEvent({
1239
+ clock: input.clock,
1240
+ event: "command_failed",
1241
+ phase: input.phase,
1242
+ status: "failed",
1243
+ data: { target: input.target, message: String(error) }
1244
+ }));
1245
+ return 1;
1246
+ }
1247
+ }
1248
+ async function runExecution(input) {
1249
+ input.writer.write(createChofiEvent({
1250
+ clock: input.clock,
1251
+ event: "command_started",
1252
+ phase: input.phase,
1253
+ data: {}
1254
+ }));
1255
+ try {
1256
+ const result = await input.action();
1257
+ const data = input.mapResult ? input.mapResult(result) : {};
1258
+ input.writer.write(createChofiEvent({
1259
+ clock: input.clock,
1260
+ event: "command_completed",
1261
+ phase: input.phase,
1262
+ status: "passed",
1263
+ data
1264
+ }));
1265
+ input.writer.write(createChofiEvent({
1266
+ clock: input.clock,
1267
+ event: "summary",
1268
+ phase: input.phase,
1269
+ status: "passed",
1270
+ data
1271
+ }));
1272
+ return 0;
1273
+ }
1274
+ catch (error) {
1275
+ const chofiError = classifyError(error);
1276
+ const errorData = {
1277
+ message: chofiError.message,
1278
+ recoverySuggestion: chofiError.recoverySuggestion
1279
+ };
1280
+ input.writer.write(createChofiEvent({
1281
+ clock: input.clock,
1282
+ event: "command_failed",
1283
+ phase: input.phase,
1284
+ status: "failed",
1285
+ errorType: chofiError.code,
1286
+ data: errorData
1287
+ }));
1288
+ input.writer.write(createChofiEvent({
1289
+ clock: input.clock,
1290
+ event: "summary",
1291
+ phase: input.phase,
1292
+ status: "failed",
1293
+ errorType: chofiError.code,
1294
+ data: errorData
1295
+ }));
1296
+ throw new CliError(chofiError.message, 1);
1297
+ }
1298
+ }
1299
+ function commandStartedData(command) {
1300
+ return {
1301
+ name: command.name,
1302
+ command: command.command,
1303
+ args: command.args,
1304
+ cwd: command.cwd
1305
+ };
1306
+ }
1307
+ function writeToolCheck(writer, clock, availability) {
1308
+ const status = availability.status === "available" ? "available" : "missing";
1309
+ writer.write(createChofiEvent({
1310
+ clock,
1311
+ event: "tool_checked",
1312
+ phase: "maestro.check",
1313
+ status,
1314
+ data: availability
1315
+ }));
1316
+ writer.write(createChofiEvent({
1317
+ clock,
1318
+ event: "summary",
1319
+ phase: "maestro.check",
1320
+ status,
1321
+ data: { maestro: availability }
1322
+ }));
1323
+ }
1324
+ if (import.meta.url === `file://${process.argv[1]}`) {
1325
+ process.exitCode = await runChofiCli(process.argv.slice(2));
1326
+ }