mimetic-cli 0.1.4 → 0.1.6

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 (49) hide show
  1. package/README.md +67 -12
  2. package/dist/env-file.d.ts +14 -0
  3. package/dist/env-file.js +108 -0
  4. package/dist/env-file.js.map +1 -0
  5. package/dist/feedback.d.ts +7 -5
  6. package/dist/feedback.js +61 -4
  7. package/dist/feedback.js.map +1 -1
  8. package/dist/init-templates.js +29 -0
  9. package/dist/init-templates.js.map +1 -1
  10. package/dist/lab-app-runner.d.ts +78 -0
  11. package/dist/lab-app-runner.js +403 -0
  12. package/dist/lab-app-runner.js.map +1 -0
  13. package/dist/labs.d.ts +67 -0
  14. package/dist/labs.js +257 -0
  15. package/dist/labs.js.map +1 -0
  16. package/dist/observer-assets.js +473 -25
  17. package/dist/observer-assets.js.map +1 -1
  18. package/dist/observer.d.ts +6 -0
  19. package/dist/observer.js +49 -8
  20. package/dist/observer.js.map +1 -1
  21. package/dist/oss-lab.d.ts +1 -1
  22. package/dist/oss-lab.js +6 -6
  23. package/dist/oss-lab.js.map +1 -1
  24. package/dist/oss-meta-lab.d.ts +113 -1
  25. package/dist/oss-meta-lab.js +2756 -203
  26. package/dist/oss-meta-lab.js.map +1 -1
  27. package/dist/oss-remote-telemetry.d.ts +77 -0
  28. package/dist/oss-remote-telemetry.js +393 -0
  29. package/dist/oss-remote-telemetry.js.map +1 -0
  30. package/dist/program.d.ts +8 -0
  31. package/dist/program.js +668 -70
  32. package/dist/program.js.map +1 -1
  33. package/dist/run.d.ts +105 -3
  34. package/dist/run.js +684 -22
  35. package/dist/run.js.map +1 -1
  36. package/docs/architecture/local-codex-tui-actor.md +9 -6
  37. package/docs/architecture/oss-lab-poc.md +119 -47
  38. package/docs/architecture/project-layout.md +40 -6
  39. package/docs/contracts/feedback.md +15 -12
  40. package/docs/contracts/policy.md +9 -2
  41. package/docs/contracts/run-bundle.md +62 -0
  42. package/docs/contracts/schemas.md +21 -0
  43. package/docs/goals/current.md +50 -17
  44. package/docs/product/open-source-install-experience.md +63 -8
  45. package/docs/ramp/README.md +26 -8
  46. package/docs/roadmap/world-class-open-source-v0.md +41 -20
  47. package/package.json +8 -6
  48. package/skills/mimetic-cli/SKILL.md +89 -4
  49. package/skills/mimetic-cli/agents/openai.yaml +1 -1
package/dist/program.js CHANGED
@@ -2,11 +2,13 @@ import { readFileSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { Command, Option } from "commander";
5
+ import { loadEnvFile } from "./env-file.js";
5
6
  import { draftFeedback, listFeedback, renderIssueMarkdown, renderIssueUrl, verifyFeedback } from "./feedback.js";
6
7
  import { runInit } from "./init.js";
8
+ import { inspectLabManifest, listLabManifests, resolveLabManifest } from "./labs.js";
7
9
  import { renderObserver, serveObserver } from "./observer.js";
8
10
  import { DEFAULT_OSS_REPOS, runOssLab } from "./oss-lab.js";
9
- import { runOssMetaLab } from "./oss-meta-lab.js";
11
+ import { cleanupOssMetaLabSandboxes, runOssMetaLab, startOssMetaLabLiveRefresh } from "./oss-meta-lab.js";
10
12
  import { doctor, listRuns, readReview, runDryRun, verifyRun } from "./run.js";
11
13
  export const CLI_RESPONSE_SCHEMA = "mimetic.cli-response.v1";
12
14
  function readCliVersion() {
@@ -51,9 +53,12 @@ export const plannedCommands = [
51
53
  issue: "https://github.com/danielgwilson/mimetic-cli/issues/7",
52
54
  docs: commonDocs,
53
55
  options: [
56
+ { flags: "[lab]", description: "Optional lab id or .yaml path." },
54
57
  { flags: "--dry-run", description: "Generate contract proof without browser, keys, or provider spend." },
58
+ { flags: "--app-url <url>", description: "Capture live desktop/mobile browser evidence against a running loopback app URL." },
55
59
  { flags: "--actor codex-tui|codex-exec", description: "Explicitly opt into a local Codex actor." },
56
- { flags: "--sims <count>", description: "Simulation count. Codex exec supports 1-4 lanes; Codex TUI supports 1." },
60
+ { flags: "--sims <count>", description: "Simulation count. Codex exec runs requested lanes with bounded concurrency; Codex TUI supports 1." },
61
+ { flags: "--env-file <path>", description: "Load a local env file for this run without persisting values." },
57
62
  { flags: "--cwd <path>", description: "Target project directory.", defaultValue: "." }
58
63
  ]
59
64
  },
@@ -81,8 +86,11 @@ export const plannedCommands = [
81
86
  issue: "https://github.com/danielgwilson/mimetic-cli/issues/10",
82
87
  docs: commonDocs,
83
88
  options: [
89
+ { flags: "[lab]", description: "Optional lab id or .yaml path to run and observe." },
90
+ { flags: "--lab <id-or-path>", description: "Explicit lab id or .yaml path." },
84
91
  { flags: "--run <id>", description: "Watch an existing run id or latest pointer." },
85
92
  { flags: "--sims <count>", description: "Start a fresh synthetic run with this many sims before rendering.", defaultValue: "4 when --run is omitted" },
93
+ { flags: "--env-file <path>", description: "Load a local env file for this watch without persisting values." },
86
94
  { flags: "--open", description: "Open the observer in the default browser.", defaultValue: "true for human output" },
87
95
  { flags: "--detach", description: "Render/open once and exit without attached watch server." },
88
96
  { flags: "--port <port>", description: "Local observer server port when following.", defaultValue: "0" },
@@ -113,10 +121,12 @@ export function createProgram(io = {}) {
113
121
  "",
114
122
  "Examples:",
115
123
  " mimetic watch",
124
+ " mimetic watch first-run",
125
+ " mimetic watch --lab .mimetic/labs/local.yaml",
116
126
  " mimetic watch --run latest --detach",
117
127
  " mimetic watch --json --no-open",
118
- " mimetic lab oss --repos developit/mitt,lukeed/clsx",
119
- " mimetic lab oss-smoke --limit 1 --keep",
128
+ " mimetic lab list",
129
+ " mimetic lab run first-run --json --no-open",
120
130
  " mimetic verify --run latest --json",
121
131
  "",
122
132
  "Public-safety boundary:",
@@ -180,15 +190,56 @@ function registerDoctorCommand(parent, io) {
180
190
  function registerRunCommand(parent, io) {
181
191
  parent
182
192
  .command("run")
193
+ .argument("[lab]", "Optional lab id or .yaml path.")
183
194
  .description("Run a persona/scenario simulation or synthetic dry-run bundle.")
184
195
  .option("--dry-run", "Generate contract proof without browser, keys, or provider spend.")
196
+ .option("--app-url <url>", "Capture live desktop/mobile browser evidence against a running loopback app URL.")
185
197
  .addOption(new Option("--actor <actor>", "Explicit live actor to run.").choices(["codex-tui", "codex-exec"]))
186
- .option("--sims <count>", "Simulation count. Codex exec supports 1-4 lanes; Codex TUI supports 1.")
198
+ .option("--sims <count>", "Simulation count. Codex exec runs requested lanes with bounded concurrency; Codex TUI supports 1.")
187
199
  .option("--timeout-ms <ms>", "Local actor timeout in milliseconds.", String(240_000))
188
200
  .option("--cwd <path>", "Target project directory.", ".")
201
+ .option("--env-file <path>", "Load a local env file for this run without persisting values.")
189
202
  .option("--run-id <id>", "Explicit run id for deterministic fixture tests.")
190
203
  .option("--json", "Print a machine-readable JSON response.")
191
- .action(async (options, command) => {
204
+ .action(async (lab, options, command) => {
205
+ if (!await applyEnvFileOption({
206
+ command,
207
+ cwd: options.cwd,
208
+ envFile: options.envFile,
209
+ io
210
+ })) {
211
+ return;
212
+ }
213
+ if (lab) {
214
+ if (options.appUrl !== undefined || options.actor !== undefined) {
215
+ const result = {
216
+ schema: "mimetic.run-result.v1",
217
+ ok: false,
218
+ cwd: options.cwd,
219
+ warnings: [],
220
+ error: {
221
+ code: "MIMETIC_APP_URL_OPTION_CONFLICT",
222
+ message: "Use lab manifests with lab-compatible options only; --app-url and --actor belong to direct `mimetic run`."
223
+ }
224
+ };
225
+ writeResult(command, io, result, formatRunHuman);
226
+ io.setExitCode(2);
227
+ return;
228
+ }
229
+ await runLabCommand({
230
+ command,
231
+ io,
232
+ lab,
233
+ mode: "run",
234
+ options: {
235
+ cwd: options.cwd,
236
+ ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun }),
237
+ ...(options.runId === undefined ? {} : { runId: options.runId }),
238
+ ...(options.sims === undefined ? {} : { sims: options.sims })
239
+ }
240
+ });
241
+ return;
242
+ }
192
243
  const simCount = options.sims === undefined ? undefined : parsePositiveInteger(options.sims);
193
244
  const timeoutMs = options.timeoutMs === undefined ? undefined : parseTimeoutMs(options.timeoutMs);
194
245
  if (options.sims !== undefined && simCount === null) {
@@ -199,7 +250,7 @@ function registerRunCommand(parent, io) {
199
250
  warnings: [],
200
251
  error: {
201
252
  code: "MIMETIC_INVALID_SIM_COUNT",
202
- message: "--sims must be an integer between 1 and 64."
253
+ message: "--sims must be a positive integer."
203
254
  }
204
255
  };
205
256
  writeResult(command, io, result, formatRunHuman);
@@ -224,6 +275,7 @@ function registerRunCommand(parent, io) {
224
275
  const result = await runDryRun({
225
276
  cwd: options.cwd,
226
277
  ...(options.actor === undefined ? {} : { actor: options.actor }),
278
+ ...(options.appUrl === undefined ? {} : { appUrl: options.appUrl }),
227
279
  ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun }),
228
280
  ...(options.runId === undefined ? {} : { runId: options.runId }),
229
281
  ...(simCount === undefined || simCount === null ? {} : { simCount }),
@@ -274,11 +326,22 @@ function registerRunsCommand(parent, io) {
274
326
  function registerWatchCommand(parent, io) {
275
327
  parent
276
328
  .command("watch")
329
+ .argument("[lab]", "Optional lab id or .yaml path to run and observe.")
277
330
  .description("Run sims, open the observer, and keep the shell attached.")
331
+ .option("--lab <id-or-path>", "Explicit lab id or .yaml path.")
278
332
  .option("--run <id>", "Watch an existing run id or latest pointer.")
333
+ .option("--dry-run", "Lab only: render contract evidence without live provider spend.")
279
334
  .option("--sims <count>", "Start a fresh synthetic run with this many sims before rendering. Defaults to 4 when --run is omitted.")
335
+ .option("--count <count>", "Lab only: override headed desktop lane count.")
336
+ .option("--limit <count>", "Lab only: override smoke lab repo limit.")
337
+ .option("--repo <owner/repo>", "Lab only: GitHub repo slug. Repeatable.", collectRepeated, [])
338
+ .option("--repos <owner/repo,...>", "Lab only: comma-separated GitHub repo slugs.")
339
+ .option("--redact-repos", "Lab only: redact repo labels in durable artifacts.")
340
+ .option("--no-redact-repos", "Lab only: persist repo labels. Use only for public-safe runs.")
341
+ .option("--keep", "Lab only: keep disposable clone sandbox for debugging.")
280
342
  .option("--run-id <id>", "Explicit run id for deterministic fixture tests.")
281
343
  .option("--cwd <path>", "Target project directory.", ".")
344
+ .option("--env-file <path>", "Load a local env file for this watch without persisting values.")
282
345
  .option("--open", "Open the observer in the default browser.")
283
346
  .option("--no-open", "Render without opening a browser.")
284
347
  .addOption(new Option("--follow", "Deprecated; human output follows by default.").hideHelp())
@@ -289,6 +352,8 @@ function registerWatchCommand(parent, io) {
289
352
  "",
290
353
  "Happy path:",
291
354
  " mimetic watch",
355
+ " mimetic watch first-run",
356
+ " mimetic watch --lab .mimetic/labs/local.yaml",
292
357
  "",
293
358
  "Agent/CI path:",
294
359
  " mimetic watch --json --no-open",
@@ -296,7 +361,70 @@ function registerWatchCommand(parent, io) {
296
361
  "Existing evidence:",
297
362
  " mimetic watch --run latest --detach"
298
363
  ].join("\n"))
299
- .action(async (options, command) => {
364
+ .action(async (labArg, options, command) => {
365
+ const lab = options.lab ?? labArg;
366
+ if (options.lab !== undefined && labArg !== undefined) {
367
+ const result = {
368
+ schema: "mimetic.run-result.v1",
369
+ ok: false,
370
+ cwd: options.cwd,
371
+ warnings: [],
372
+ error: {
373
+ code: "MIMETIC_WATCH_OPTION_CONFLICT",
374
+ message: "Use either positional lab or --lab, not both."
375
+ }
376
+ };
377
+ writeResult(command, io, result, formatRunHuman);
378
+ io.setExitCode(2);
379
+ return;
380
+ }
381
+ if (!await applyEnvFileOption({
382
+ command,
383
+ cwd: options.cwd,
384
+ envFile: options.envFile,
385
+ io
386
+ })) {
387
+ return;
388
+ }
389
+ if (lab) {
390
+ if (options.run !== undefined) {
391
+ const result = {
392
+ schema: "mimetic.run-result.v1",
393
+ ok: false,
394
+ cwd: options.cwd,
395
+ warnings: [],
396
+ error: {
397
+ code: "MIMETIC_WATCH_OPTION_CONFLICT",
398
+ message: "Use either a lab to start evidence or --run to watch existing evidence, not both."
399
+ }
400
+ };
401
+ writeResult(command, io, result, formatRunHuman);
402
+ io.setExitCode(2);
403
+ return;
404
+ }
405
+ await runLabCommand({
406
+ command,
407
+ io,
408
+ lab,
409
+ mode: "watch",
410
+ options: {
411
+ cwd: options.cwd,
412
+ ...(options.count === undefined ? {} : { count: options.count }),
413
+ ...(options.detach === undefined ? {} : { detach: options.detach }),
414
+ ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun }),
415
+ ...(options.keep === undefined ? {} : { keep: options.keep }),
416
+ ...(options.limit === undefined ? {} : { limit: options.limit }),
417
+ ...(options.open === undefined ? {} : { open: options.open }),
418
+ port: options.port,
419
+ ...(options.redactRepos === undefined ? {} : { redactRepos: options.redactRepos }),
420
+ repo: options.repo,
421
+ ...(options.repos === undefined ? {} : { repos: options.repos }),
422
+ ...(options.runId === undefined ? {} : { runId: options.runId }),
423
+ ...(options.sims === undefined ? {} : { sims: options.sims })
424
+ }
425
+ });
426
+ return;
427
+ }
300
428
  const runOptionSource = typeof command.getOptionValueSource === "function"
301
429
  ? command.getOptionValueSource("run")
302
430
  : undefined;
@@ -311,7 +439,7 @@ function registerWatchCommand(parent, io) {
311
439
  warnings: [],
312
440
  error: {
313
441
  code: "MIMETIC_INVALID_SIM_COUNT",
314
- message: "--sims must be an integer between 1 and 64."
442
+ message: "--sims must be a positive integer."
315
443
  }
316
444
  };
317
445
  writeResult(command, io, result, formatRunHuman);
@@ -494,12 +622,83 @@ function registerFeedbackCommands(parent, io) {
494
622
  function registerLabCommands(parent, io) {
495
623
  const lab = parent
496
624
  .command("lab")
497
- .description("Run experimental Mimetic proof loops against disposable public targets.");
625
+ .description("List, inspect, and run Mimetic lab manifests.");
498
626
  lab
499
- .command("oss")
500
- .description("Watch headed Codex/E2B OSS meta-sims setting up Mimetic inside public repos.")
501
- .option("--repos <owner/repo,...>", "Comma-separated public GitHub repo slugs.")
502
- .option("--repo <owner/repo>", "Public GitHub repo slug. Repeatable.", collectRepeated, [])
627
+ .command("list")
628
+ .description("List committed and ignored Mimetic lab manifests.")
629
+ .option("--cwd <path>", "Target project directory.", ".")
630
+ .option("--json", "Print a machine-readable JSON response.")
631
+ .action(async (options, command) => {
632
+ const result = await listLabManifests(options.cwd);
633
+ writeResult(command, io, result, formatLabListHuman);
634
+ io.setExitCode(0);
635
+ });
636
+ lab
637
+ .command("inspect")
638
+ .argument("<lab>", "Lab id or .yaml path.")
639
+ .description("Inspect a Mimetic lab manifest without running it.")
640
+ .option("--cwd <path>", "Target project directory.", ".")
641
+ .option("--json", "Print a machine-readable JSON response.")
642
+ .action(async (labName, options, command) => {
643
+ const result = await inspectLabManifest(options.cwd, labName);
644
+ writeResult(command, io, result, formatLabInspectHuman);
645
+ io.setExitCode(result.ok ? 0 : 2);
646
+ });
647
+ lab
648
+ .command("run")
649
+ .argument("<lab>", "Lab id or .yaml path.")
650
+ .description("Run a Mimetic lab manifest.")
651
+ .option("--env-file <path>", "Load a local env file for this lab without persisting values.")
652
+ .option("--dry-run", "Render contract evidence without live provider spend.")
653
+ .option("--open", "Open the observer in the default browser.")
654
+ .option("--no-open", "Render without opening a browser.")
655
+ .option("--detach", "Render/open once and exit without attached watch server.")
656
+ .option("--port <port>", "Local observer server port when following.", "0")
657
+ .option("--sims <count>", "Override synthetic sims or headed desktop lanes.")
658
+ .option("--count <count>", "Override headed desktop lane count.")
659
+ .option("--limit <count>", "Override smoke lab repo limit.")
660
+ .option("--run-id <id>", "Explicit lab run id.")
661
+ .option("--cwd <path>", "Target project directory.", ".")
662
+ .option("--repo <owner/repo>", "GitHub repo slug. Repeatable.", collectRepeated, [])
663
+ .option("--repos <owner/repo,...>", "Comma-separated GitHub repo slugs.")
664
+ .option("--redact-repos", "Redact repo labels in durable lab artifacts.")
665
+ .option("--no-redact-repos", "Persist repo labels in durable lab artifacts. Use only for public-safe runs.")
666
+ .option("--keep", "Smoke labs only: keep disposable clone sandbox for debugging.")
667
+ .option("--json", "Print a machine-readable JSON response.")
668
+ .addHelpText("after", [
669
+ "",
670
+ "Examples:",
671
+ " mimetic lab run first-run",
672
+ " mimetic lab run oss --dry-run --json --no-open",
673
+ " mimetic lab run .mimetic/labs/private-dogfood.yaml --env-file .mimetic/local/provider.env",
674
+ "",
675
+ "Human watch path:",
676
+ " mimetic watch first-run",
677
+ " mimetic watch --lab .mimetic/labs/local.yaml"
678
+ ].join("\n"))
679
+ .action(async (labName, options, command) => {
680
+ if (!await applyEnvFileOption({
681
+ command,
682
+ cwd: options.cwd,
683
+ envFile: options.envFile,
684
+ io
685
+ })) {
686
+ return;
687
+ }
688
+ await runLabCommand({
689
+ command,
690
+ io,
691
+ lab: labName,
692
+ mode: "run",
693
+ options
694
+ });
695
+ });
696
+ lab
697
+ .command("oss", { hidden: true })
698
+ .description("Alias: run the bundled OSS meta-lab manifest.")
699
+ .option("--env-file <path>", "Load a local env file for this lab without persisting values.")
700
+ .option("--repos <owner/repo,...>", "Comma-separated GitHub repo slugs.")
701
+ .option("--repo <owner/repo>", "GitHub repo slug. Repeatable.", collectRepeated, [])
503
702
  .option("--count <count>", "Number of headed desktop sims to assign.", String(DEFAULT_OSS_REPOS.length))
504
703
  .option("--sims <count>", "Alias for --count.")
505
704
  .option("--run-id <id>", "Explicit lab run id.")
@@ -508,6 +707,8 @@ function registerLabCommands(parent, io) {
508
707
  .option("--open", "Open the observer in the default browser.")
509
708
  .option("--no-open", "Render without opening a browser.")
510
709
  .option("--detach", "Render/open once and exit without attached watch server.")
710
+ .option("--redact-repos", "Redact repo labels in durable lab artifacts.")
711
+ .option("--no-redact-repos", "Persist repo labels in durable lab artifacts. Defaults to redacted when a GitHub token is present.")
511
712
  .option("--port <port>", "Local observer server port when following.", "0")
512
713
  .option("--smoke", "Run the disposable local clone smoke harness instead of headed meta-sims.")
513
714
  .option("--limit <count>", "Smoke mode only: number of selected repos to trial.", String(DEFAULT_OSS_REPOS.length))
@@ -515,30 +716,42 @@ function registerLabCommands(parent, io) {
515
716
  .option("--json", "Print a machine-readable JSON response.")
516
717
  .addHelpText("after", [
517
718
  "",
518
- "Happy path:",
519
- " mimetic lab oss",
719
+ "Preferred paths:",
720
+ " mimetic watch oss",
721
+ " mimetic lab run oss",
520
722
  "",
521
723
  "Repo selection:",
522
- " mimetic lab oss --repos developit/mitt,lukeed/clsx,sindresorhus/is-plain-obj,ai/nanoid",
523
- " mimetic lab oss --repo developit/mitt --repo lukeed/clsx --count 4",
724
+ " mimetic watch --lab .mimetic/labs/local-oss.yaml",
725
+ " mimetic lab run oss --repos CorentinTh/it-tools,drawdb-io/drawdb,maciekt07/TodoApp,lissy93/dashy",
726
+ " mimetic lab run oss --repo CorentinTh/it-tools --repo drawdb-io/drawdb --count 4",
524
727
  "",
525
728
  "Agent/CI path:",
526
- " mimetic lab oss --dry-run --json --no-open",
729
+ " mimetic lab run oss --dry-run --json --no-open",
527
730
  "",
528
731
  "Disposable clone smoke:",
732
+ " mimetic lab run oss-smoke --limit 1 --keep",
529
733
  " mimetic lab oss-smoke --limit 1 --keep",
530
- " mimetic lab oss --smoke --limit 1 --keep",
531
734
  "",
532
735
  "Shape:",
533
- " The top-level Observer shows headed E2B desktop lanes. Each desktop is intended",
534
- " to run Codex TUI, clone its assigned public OSS repo, set up Mimetic, and keep",
535
- " that repo's nested Mimetic Observer visible in the E2B browser.",
736
+ " The top-level Observer shows headed E2B desktop lanes. Each desktop clones",
737
+ " its assigned authorized repo, sets up Mimetic, starts the target app where",
738
+ " feasible, opens desktop/mobile app windows plus the nested Observer, and",
739
+ " starts a nonblocking Codex actor attempt.",
536
740
  "",
537
741
  "Safety:",
538
- " Only public GitHub owner/repo slugs are accepted. No keys or private artifacts",
539
- " are written into committed Mimetic source."
742
+ " Only GitHub owner/repo slugs are accepted. Live stream auth URLs are",
743
+ " runtime-only. Repo labels are redacted by default when a GitHub token",
744
+ " is present; pass --no-redact-repos only for public-safe runs."
540
745
  ].join("\n"))
541
746
  .action(async (options, command) => {
747
+ if (!await applyEnvFileOption({
748
+ command,
749
+ cwd: options.cwd,
750
+ envFile: options.envFile,
751
+ io
752
+ })) {
753
+ return;
754
+ }
542
755
  if (options.smoke) {
543
756
  await runOssSmokeAction({ command, io, options });
544
757
  return;
@@ -569,38 +782,56 @@ function registerLabCommands(parent, io) {
569
782
  const wantsMachine = wantsJson(command);
570
783
  const shouldOpen = options.open === false ? false : wantsMachine ? options.open === true : true;
571
784
  const wantsFollow = !wantsMachine && options.detach !== true && options.dryRun !== true;
572
- const result = await runOssMetaLab({
573
- cwd: options.cwd,
574
- open: wantsFollow ? false : shouldOpen,
575
- repos: [...options.repo, ...(options.repos ? [options.repos] : [])],
576
- ...(count === null ? { count: Number.NaN } : { count }),
577
- ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun }),
578
- ...(options.runId === undefined ? {} : { runId: options.runId })
579
- });
785
+ const repoOverrideRequested = options.repo.length > 0 || options.repos !== undefined;
786
+ const redactRepoNames = options.redactRepos ?? (repoOverrideRequested ? true : undefined);
580
787
  let server = null;
788
+ let liveRefresh = null;
789
+ let result;
790
+ try {
791
+ result = await runOssMetaLab({
792
+ ...(wantsFollow ? { completionTimeoutMs: 0 } : {}),
793
+ cwd: options.cwd,
794
+ ...(wantsFollow
795
+ ? {
796
+ onObserverReady: async (observer) => {
797
+ if (!server) {
798
+ server = await serveObserver(observer, { open: shouldOpen, port });
799
+ }
800
+ }
801
+ }
802
+ : {}),
803
+ open: wantsFollow ? false : shouldOpen,
804
+ ...(redactRepoNames === undefined ? {} : { redactRepoNames }),
805
+ repos: [...options.repo, ...(options.repos ? [options.repos] : [])],
806
+ ...(count === null ? { count: Number.NaN } : { count }),
807
+ ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun }),
808
+ ...(options.runId === undefined ? {} : { runId: options.runId })
809
+ });
810
+ }
811
+ catch (error) {
812
+ const earlyServer = server;
813
+ await earlyServer?.close().catch((cleanupError) => {
814
+ io.writeErr(`watch cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`);
815
+ });
816
+ server = null;
817
+ throw error;
818
+ }
581
819
  let output = result;
582
- if (result.ok && wantsFollow && result.observer?.ok) {
820
+ if (server && shouldServeOssMetaLabObserver(result, { wantsFollow: true })) {
821
+ liveRefresh = startOssMetaLabLiveRefresh(result);
822
+ output = withOssMetaLabServer(result, server);
823
+ }
824
+ else if (shouldServeOssMetaLabObserver(result, { wantsFollow })) {
583
825
  server = await serveObserver(result.observer, { open: shouldOpen, port });
584
- output = {
585
- ...result,
586
- observer: {
587
- ...result.observer,
588
- observerUrl: server.url,
589
- serverUrl: server.url,
590
- opened: server.opened,
591
- ...(server.openCommand ? { openCommand: server.openCommand } : {}),
592
- warnings: [
593
- ...result.observer.warnings,
594
- "Live OSS meta-lab server is polling observer-data.json with no-store caching.",
595
- ...(server.warning ? [server.warning] : [])
596
- ]
597
- },
598
- warnings: [
599
- ...result.warnings,
600
- "Live OSS meta-lab server is polling observer-data.json with no-store caching.",
601
- ...(server.warning ? [server.warning] : [])
602
- ]
603
- };
826
+ liveRefresh = startOssMetaLabLiveRefresh(result);
827
+ output = withOssMetaLabServer(result, server);
828
+ }
829
+ else {
830
+ const earlyServer = server;
831
+ await earlyServer?.close().catch((error) => {
832
+ io.writeErr(`watch cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`);
833
+ });
834
+ server = null;
604
835
  }
605
836
  const exitCode = exitCodeForOssMetaLab(output);
606
837
  writeResult(command, io, output, formatOssMetaLabHuman);
@@ -610,12 +841,23 @@ function registerLabCommands(parent, io) {
610
841
  // return the user's shell, and JSON mode should exit after printing the result.
611
842
  setTimeout(() => process.exit(exitCode), 50);
612
843
  }
613
- if (output.ok && server && output.observer?.ok) {
614
- await followObserver(io, output.observer, server);
844
+ if (server && output.observer?.ok) {
845
+ await followObserver(io, output.observer, server, output.liveRequested
846
+ ? {
847
+ onStop: async () => {
848
+ await liveRefresh?.stop();
849
+ const cleanup = await cleanupOssMetaLabSandboxes(output);
850
+ return [
851
+ `E2B sandbox cleanup killed ${cleanup.killed}, skipped ${cleanup.skipped}.`,
852
+ ...cleanup.errors.map((error) => `E2B sandbox cleanup error: ${error}`)
853
+ ];
854
+ }
855
+ }
856
+ : {});
615
857
  }
616
858
  });
617
859
  lab
618
- .command("oss-smoke")
860
+ .command("oss-smoke", { hidden: true })
619
861
  .description("Clone lightweight public OSS repos, try Mimetic setup/proof, then discard clones.")
620
862
  .option("--repos <owner/repo,...>", "Comma-separated public GitHub repo slugs.")
621
863
  .option("--repo <owner/repo>", "Public GitHub repo slug. Repeatable.", collectRepeated, [])
@@ -628,7 +870,7 @@ function registerLabCommands(parent, io) {
628
870
  "",
629
871
  "Examples:",
630
872
  " mimetic lab oss-smoke",
631
- " mimetic lab oss-smoke --repos developit/mitt,lukeed/clsx",
873
+ " mimetic lab oss-smoke --repos CorentinTh/it-tools,drawdb-io/drawdb",
632
874
  " mimetic lab oss-smoke --limit 1 --keep --json",
633
875
  "",
634
876
  "Safety:",
@@ -652,6 +894,297 @@ async function runOssSmokeAction(args) {
652
894
  writeResult(args.command, args.io, result, formatOssLabHuman);
653
895
  args.io.setExitCode(result.ok ? 0 : 2);
654
896
  }
897
+ async function runLabCommand(args) {
898
+ const resolved = await resolveLabManifest(args.options.cwd, args.lab);
899
+ if (!resolved.ok) {
900
+ writeResult(args.command, args.io, resolved, formatLabResolveFailureHuman);
901
+ args.io.setExitCode(2);
902
+ return;
903
+ }
904
+ switch (resolved.manifest.kind) {
905
+ case "synthetic":
906
+ await runSyntheticLabCommand({ ...args, manifest: resolved.manifest });
907
+ return;
908
+ case "oss-meta":
909
+ await runOssMetaLabCommand({ ...args, manifest: resolved.manifest });
910
+ return;
911
+ case "oss-smoke":
912
+ await runOssSmokeLabCommand({ ...args, manifest: resolved.manifest });
913
+ return;
914
+ }
915
+ }
916
+ async function runSyntheticLabCommand(args) {
917
+ const simCount = parseLabCount(args.options.sims, args.manifest.sims ?? args.manifest.count ?? 4);
918
+ if (simCount === null) {
919
+ const result = {
920
+ schema: "mimetic.run-result.v1",
921
+ ok: false,
922
+ cwd: args.options.cwd,
923
+ warnings: [],
924
+ error: {
925
+ code: "MIMETIC_INVALID_SIM_COUNT",
926
+ message: "--sims must be a positive integer."
927
+ }
928
+ };
929
+ writeResult(args.command, args.io, result, formatRunHuman);
930
+ args.io.setExitCode(2);
931
+ return;
932
+ }
933
+ const runResult = await runDryRun({
934
+ cwd: args.options.cwd,
935
+ dryRun: args.options.dryRun ?? args.manifest.defaults?.dryRun ?? true,
936
+ simCount,
937
+ ...(args.options.runId === undefined ? {} : { runId: args.options.runId })
938
+ });
939
+ if (args.mode === "run") {
940
+ writeResult(args.command, args.io, runResult, formatRunHuman);
941
+ args.io.setExitCode(runResult.ok ? 0 : 2);
942
+ return;
943
+ }
944
+ if (!runResult.ok || !runResult.runId) {
945
+ writeResult(args.command, args.io, runResult, formatRunHuman);
946
+ args.io.setExitCode(2);
947
+ return;
948
+ }
949
+ const openOverride = args.options.open ?? args.manifest.defaults?.open;
950
+ await renderAndMaybeFollowObserver({
951
+ command: args.command,
952
+ cwd: args.options.cwd,
953
+ io: args.io,
954
+ port: args.options.port ?? "0",
955
+ runInput: runResult.runId,
956
+ ...(args.options.detach === undefined ? {} : { detach: args.options.detach }),
957
+ ...(openOverride === undefined ? {} : { open: openOverride })
958
+ });
959
+ }
960
+ async function runOssSmokeLabCommand(args) {
961
+ const limit = parseLabCount(args.options.limit ?? args.options.sims, args.manifest.limit ?? args.manifest.count ?? args.manifest.repos?.length ?? DEFAULT_OSS_REPOS.length);
962
+ if (limit === null) {
963
+ const result = {
964
+ schema: "mimetic.oss-lab-result.v1",
965
+ ok: false,
966
+ cleanup: { kept: Boolean(args.options.keep), sandboxRemoved: false },
967
+ completedAt: new Date().toISOString(),
968
+ cwd: args.options.cwd,
969
+ error: {
970
+ code: "MIMETIC_INVALID_OSS_LIMIT",
971
+ message: "--limit must be a positive integer."
972
+ },
973
+ repos: [],
974
+ runId: args.options.runId ?? "not-created",
975
+ sandboxPath: ".mimetic/tmp/oss-lab/not-created",
976
+ startedAt: new Date().toISOString(),
977
+ warnings: []
978
+ };
979
+ writeResult(args.command, args.io, result, formatOssLabHuman);
980
+ args.io.setExitCode(2);
981
+ return;
982
+ }
983
+ const result = await runOssLab({
984
+ cwd: args.options.cwd,
985
+ limit,
986
+ repos: labRepos(args.options, args.manifest),
987
+ ...(args.options.keep === undefined ? {} : { keep: args.options.keep }),
988
+ ...(args.options.runId === undefined ? {} : { runId: args.options.runId })
989
+ });
990
+ writeResult(args.command, args.io, result, formatOssLabHuman);
991
+ args.io.setExitCode(result.ok ? 0 : 2);
992
+ }
993
+ async function runOssMetaLabCommand(args) {
994
+ const count = parseLabCount(args.options.count ?? args.options.sims, args.manifest.count ?? args.manifest.sims ?? args.manifest.repos?.length ?? DEFAULT_OSS_REPOS.length);
995
+ const port = parseObserverPort(args.options.port ?? "0");
996
+ if (port === null) {
997
+ const result = {
998
+ schema: "mimetic.oss-meta-lab-result.v1",
999
+ ok: false,
1000
+ assignments: [],
1001
+ cwd: args.options.cwd,
1002
+ dryRun: args.options.dryRun === true,
1003
+ error: {
1004
+ code: "MIMETIC_META_RUN_FAILED",
1005
+ message: "--port must be an integer between 0 and 65535."
1006
+ },
1007
+ liveRequested: args.options.dryRun !== true,
1008
+ repos: labRepos(args.options, args.manifest),
1009
+ sandboxes: [],
1010
+ warnings: []
1011
+ };
1012
+ writeResult(args.command, args.io, result, formatOssMetaLabHuman);
1013
+ args.io.setExitCode(2);
1014
+ return;
1015
+ }
1016
+ const wantsMachine = wantsJson(args.command);
1017
+ const dryRun = args.options.dryRun ?? args.manifest.defaults?.dryRun;
1018
+ const shouldOpen = args.options.open === false
1019
+ ? false
1020
+ : wantsMachine
1021
+ ? args.options.open === true
1022
+ : args.options.open ?? args.manifest.defaults?.open ?? args.mode === "watch";
1023
+ const wantsFollow = args.mode === "watch" && !wantsMachine && args.options.detach !== true && dryRun !== true;
1024
+ const repoOverrideRequested = (args.options.repo?.length ?? 0) > 0 || args.options.repos !== undefined;
1025
+ const defaultRedactRepos = repoOverrideRequested ? true : args.manifest.defaults?.redactRepos;
1026
+ const redactRepoNames = args.options.redactRepos ?? defaultRedactRepos;
1027
+ let server = null;
1028
+ let liveRefresh = null;
1029
+ let result;
1030
+ try {
1031
+ result = await runOssMetaLab({
1032
+ ...(wantsFollow ? { completionTimeoutMs: 0 } : {}),
1033
+ cwd: args.options.cwd,
1034
+ ...(wantsFollow
1035
+ ? {
1036
+ onObserverReady: async (observer) => {
1037
+ if (!server) {
1038
+ server = await serveObserver(observer, { open: shouldOpen, port });
1039
+ }
1040
+ }
1041
+ }
1042
+ : {}),
1043
+ open: wantsFollow ? false : shouldOpen,
1044
+ ...(dryRun === undefined ? {} : { dryRun }),
1045
+ ...(redactRepoNames === undefined ? {} : { redactRepoNames }),
1046
+ repos: labRepos(args.options, args.manifest),
1047
+ ...(count === null ? { count: Number.NaN } : { count }),
1048
+ ...(args.options.runId === undefined ? {} : { runId: args.options.runId })
1049
+ });
1050
+ }
1051
+ catch (error) {
1052
+ const earlyServer = server;
1053
+ await earlyServer?.close().catch((cleanupError) => {
1054
+ args.io.writeErr(`watch cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`);
1055
+ });
1056
+ server = null;
1057
+ throw error;
1058
+ }
1059
+ let output = result;
1060
+ if (server && shouldServeOssMetaLabObserver(result, { wantsFollow: true })) {
1061
+ liveRefresh = startOssMetaLabLiveRefresh(result);
1062
+ output = withOssMetaLabServer(result, server);
1063
+ }
1064
+ else if (shouldServeOssMetaLabObserver(result, { wantsFollow })) {
1065
+ server = await serveObserver(result.observer, { open: shouldOpen, port });
1066
+ liveRefresh = startOssMetaLabLiveRefresh(result);
1067
+ output = withOssMetaLabServer(result, server);
1068
+ }
1069
+ else {
1070
+ const earlyServer = server;
1071
+ await earlyServer?.close().catch((error) => {
1072
+ args.io.writeErr(`watch cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`);
1073
+ });
1074
+ server = null;
1075
+ }
1076
+ const exitCode = exitCodeForOssMetaLab(output);
1077
+ writeResult(args.command, args.io, output, formatOssMetaLabHuman);
1078
+ args.io.setExitCode(exitCode);
1079
+ if (shouldForceExitAfterOssMetaLab(output, { detach: args.options.detach === true, wantsMachine })) {
1080
+ setTimeout(() => process.exit(exitCode), 50);
1081
+ }
1082
+ if (server && output.observer?.ok) {
1083
+ await followObserver(args.io, output.observer, server, output.liveRequested
1084
+ ? {
1085
+ onStop: async () => {
1086
+ await liveRefresh?.stop();
1087
+ const cleanup = await cleanupOssMetaLabSandboxes(output);
1088
+ return [
1089
+ `E2B sandbox cleanup killed ${cleanup.killed}, skipped ${cleanup.skipped}.`,
1090
+ ...cleanup.errors.map((error) => `E2B sandbox cleanup error: ${error}`)
1091
+ ];
1092
+ }
1093
+ }
1094
+ : {});
1095
+ }
1096
+ }
1097
+ async function renderAndMaybeFollowObserver(args) {
1098
+ const port = parseObserverPort(args.port);
1099
+ if (port === null) {
1100
+ const result = {
1101
+ schema: "mimetic.run-result.v1",
1102
+ ok: false,
1103
+ cwd: args.cwd,
1104
+ warnings: [],
1105
+ error: {
1106
+ code: "MIMETIC_INVALID_PORT",
1107
+ message: "--port must be an integer between 0 and 65535."
1108
+ }
1109
+ };
1110
+ writeResult(args.command, args.io, result, formatRunHuman);
1111
+ args.io.setExitCode(2);
1112
+ return;
1113
+ }
1114
+ const wantsMachine = wantsJson(args.command);
1115
+ const shouldOpen = args.open === false ? false : wantsMachine ? args.open === true : true;
1116
+ const wantsFollow = !wantsMachine && args.detach !== true;
1117
+ const rendered = await renderObserver(args.cwd, args.runInput, { open: wantsFollow ? false : shouldOpen });
1118
+ let server = null;
1119
+ let result = rendered;
1120
+ if (rendered.ok && wantsFollow) {
1121
+ server = await serveObserver(rendered, { open: shouldOpen, port });
1122
+ result = withObserverServer(rendered, server);
1123
+ }
1124
+ writeResult(args.command, args.io, result, formatObserverHuman);
1125
+ args.io.setExitCode(result.ok ? 0 : 2);
1126
+ if (result.ok && server) {
1127
+ await followObserver(args.io, result, server);
1128
+ }
1129
+ }
1130
+ async function applyEnvFileOption(args) {
1131
+ if (!args.envFile) {
1132
+ return true;
1133
+ }
1134
+ const result = await loadEnvFile(args.cwd, args.envFile);
1135
+ if (result.ok) {
1136
+ return true;
1137
+ }
1138
+ writeResult(args.command, args.io, result, formatEnvFileHuman);
1139
+ args.io.setExitCode(2);
1140
+ return false;
1141
+ }
1142
+ function labRepos(options, manifest) {
1143
+ return [
1144
+ ...(options.repo ?? []),
1145
+ ...(options.repos ? [options.repos] : []),
1146
+ ...(options.repo?.length || options.repos ? [] : manifest.repos ?? [])
1147
+ ];
1148
+ }
1149
+ function parseLabCount(value, fallback) {
1150
+ return value === undefined ? fallback : parsePositiveInteger(value);
1151
+ }
1152
+ function withObserverServer(rendered, server) {
1153
+ return {
1154
+ ...rendered,
1155
+ observerUrl: server.url,
1156
+ serverUrl: server.url,
1157
+ opened: server.opened,
1158
+ ...(server.openCommand ? { openCommand: server.openCommand } : {}),
1159
+ warnings: [
1160
+ ...rendered.warnings,
1161
+ "Live observer server is polling observer-data.json with no-store caching.",
1162
+ ...(server.warning ? [server.warning] : [])
1163
+ ]
1164
+ };
1165
+ }
1166
+ function withOssMetaLabServer(result, server) {
1167
+ return {
1168
+ ...result,
1169
+ observer: {
1170
+ ...result.observer,
1171
+ observerUrl: server.url,
1172
+ serverUrl: server.url,
1173
+ opened: server.opened,
1174
+ ...(server.openCommand ? { openCommand: server.openCommand } : {}),
1175
+ warnings: [
1176
+ ...result.observer.warnings,
1177
+ "Live OSS meta-lab server is polling observer-data.json with no-store caching.",
1178
+ ...(server.warning ? [server.warning] : [])
1179
+ ]
1180
+ },
1181
+ warnings: [
1182
+ ...result.warnings,
1183
+ "Live OSS meta-lab server is polling observer-data.json with no-store caching.",
1184
+ ...(server.warning ? [server.warning] : [])
1185
+ ]
1186
+ };
1187
+ }
655
1188
  function writeResult(command, io, result, formatHuman) {
656
1189
  if (wantsJson(command)) {
657
1190
  io.writeOut(`${JSON.stringify(result, null, 2)}\n`);
@@ -710,12 +1243,62 @@ function formatObserverHuman(result) {
710
1243
  ...result.warnings.map((warning) => `warning: ${warning}`)
711
1244
  ].join("\n") + "\n";
712
1245
  }
1246
+ function formatEnvFileHuman(result) {
1247
+ if (!result.ok) {
1248
+ return `${result.error?.code}: ${result.error?.message}\n`;
1249
+ }
1250
+ return [
1251
+ "mimetic env-file loaded",
1252
+ `env-file: ${result.envFile}`,
1253
+ `loaded: ${result.loaded.length ? result.loaded.join(", ") : "none"}`,
1254
+ `skipped-existing: ${result.skipped.length ? result.skipped.join(", ") : "none"}`
1255
+ ].join("\n") + "\n";
1256
+ }
1257
+ function formatLabListHuman(result) {
1258
+ if (result.labs.length === 0) {
1259
+ return [
1260
+ `No Mimetic labs found in ${result.cwd}`,
1261
+ "Create one under mimetic/labs/*.yaml, .mimetic/labs/*.yaml, or pass a .yaml path.",
1262
+ ...result.warnings.map((warning) => `warning: ${warning}`)
1263
+ ].join("\n") + "\n";
1264
+ }
1265
+ return [
1266
+ "mimetic labs",
1267
+ ...result.labs.map((lab) => `- ${lab.id} ${lab.kind} ${lab.origin} ${lab.path}${lab.title ? ` (${lab.title})` : ""}`),
1268
+ ...result.warnings.map((warning) => `warning: ${warning}`)
1269
+ ].join("\n") + "\n";
1270
+ }
1271
+ function formatLabInspectHuman(result) {
1272
+ if (!result.ok || !result.manifest) {
1273
+ return `${result.error?.code}: ${result.error?.message}\n`;
1274
+ }
1275
+ return [
1276
+ "mimetic lab",
1277
+ `id: ${result.manifest.id}`,
1278
+ `kind: ${result.manifest.kind}`,
1279
+ ...(result.manifest.title ? [`title: ${result.manifest.title}`] : []),
1280
+ ...(result.manifest.description ? [`description: ${result.manifest.description}`] : []),
1281
+ ...(result.path ? [`path: ${result.path}`] : []),
1282
+ ...(result.origin ? [`origin: ${result.origin}`] : []),
1283
+ ...(result.manifest.sims === undefined ? [] : [`sims: ${result.manifest.sims}`]),
1284
+ ...(result.manifest.count === undefined ? [] : [`count: ${result.manifest.count}`]),
1285
+ ...(result.manifest.limit === undefined ? [] : [`limit: ${result.manifest.limit}`]),
1286
+ ...(result.manifest.repos?.length ? [`repos: ${result.manifest.repos.join(", ")}`] : []),
1287
+ ...result.warnings.map((warning) => `warning: ${warning}`)
1288
+ ].join("\n") + "\n";
1289
+ }
1290
+ function formatLabResolveFailureHuman(result) {
1291
+ return [
1292
+ `${result.error.code}: ${result.error.message}`,
1293
+ ...result.warnings.map((warning) => `warning: ${warning}`)
1294
+ ].join("\n") + "\n";
1295
+ }
713
1296
  function parsePositiveInteger(value) {
714
1297
  if (!/^\d+$/.test(value)) {
715
1298
  return null;
716
1299
  }
717
1300
  const parsed = Number.parseInt(value, 10);
718
- return parsed >= 1 && parsed <= 64 ? parsed : null;
1301
+ return Number.isSafeInteger(parsed) && parsed >= 1 ? parsed : null;
719
1302
  }
720
1303
  function parseTimeoutMs(value) {
721
1304
  if (!/^\d+$/.test(value)) {
@@ -731,19 +1314,32 @@ function parseObserverPort(value) {
731
1314
  const parsed = Number.parseInt(value, 10);
732
1315
  return parsed >= 0 && parsed <= 65535 ? parsed : null;
733
1316
  }
734
- async function followObserver(io, result, server) {
1317
+ async function followObserver(io, result, server, options = {}) {
735
1318
  io.writeOut(`watching: ${result.serverUrl ?? result.observerUrl ?? result.observerPath}\n`);
736
1319
  io.writeOut("watching: press Ctrl-C to stop\n");
737
1320
  await new Promise((resolve) => {
738
1321
  process.once("SIGINT", () => {
739
- server.close()
740
- .catch((error) => {
741
- io.writeErr(`watch cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`);
742
- })
743
- .finally(() => {
1322
+ (async () => {
1323
+ try {
1324
+ await server.close();
1325
+ }
1326
+ catch (error) {
1327
+ io.writeErr(`watch cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`);
1328
+ }
1329
+ if (options.onStop) {
1330
+ try {
1331
+ const messages = await options.onStop();
1332
+ for (const message of messages) {
1333
+ io.writeOut(`watch cleanup: ${message}\n`);
1334
+ }
1335
+ }
1336
+ catch (error) {
1337
+ io.writeErr(`watch cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`);
1338
+ }
1339
+ }
744
1340
  io.writeOut("watch stopped\n");
745
1341
  resolve();
746
- });
1342
+ })();
747
1343
  });
748
1344
  });
749
1345
  }
@@ -775,11 +1371,9 @@ function formatOssLabHuman(result) {
775
1371
  ].join("\n") + "\n";
776
1372
  }
777
1373
  function formatOssMetaLabHuman(result) {
778
- if (!result.ok && result.error) {
779
- return `${result.error.code}: ${result.error.message}\n`;
780
- }
781
1374
  return [
782
- `mimetic lab oss ${result.dryRun ? "dry-run" : "watch"}`,
1375
+ `mimetic lab oss ${result.ok ? (result.dryRun ? "dry-run" : "watch") : "failed"}`,
1376
+ ...(result.error ? [`${result.error.code}: ${result.error.message}`] : []),
783
1377
  `run: ${result.runId ?? "not-created"}`,
784
1378
  `repos: ${result.repos.join(", ")}`,
785
1379
  ...(result.count === undefined ? [] : [`desktops: ${result.count}`]),
@@ -876,6 +1470,10 @@ export function shouldForceExitAfterOssMetaLab(output, options) {
876
1470
  && (options.detach || options.wantsMachine)
877
1471
  && output.sandboxes.some((sandbox) => sandbox.urlPresent);
878
1472
  }
1473
+ export function shouldServeOssMetaLabObserver(output, options) {
1474
+ return options.wantsFollow
1475
+ && output.observer?.ok === true;
1476
+ }
879
1477
  export function exitCodeForOssMetaLab(output) {
880
1478
  return output.ok ? 0 : 2;
881
1479
  }