mimetic-cli 0.1.3 → 0.1.5
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.
- package/README.md +67 -12
- package/dist/env-file.d.ts +14 -0
- package/dist/env-file.js +108 -0
- package/dist/env-file.js.map +1 -0
- package/dist/feedback.d.ts +7 -5
- package/dist/feedback.js +61 -4
- package/dist/feedback.js.map +1 -1
- package/dist/init-templates.js +29 -0
- package/dist/init-templates.js.map +1 -1
- package/dist/lab-app-runner.d.ts +78 -0
- package/dist/lab-app-runner.js +403 -0
- package/dist/lab-app-runner.js.map +1 -0
- package/dist/labs.d.ts +67 -0
- package/dist/labs.js +257 -0
- package/dist/labs.js.map +1 -0
- package/dist/observer-assets.js +473 -25
- package/dist/observer-assets.js.map +1 -1
- package/dist/observer.d.ts +6 -0
- package/dist/observer.js +49 -8
- package/dist/observer.js.map +1 -1
- package/dist/oss-lab.d.ts +1 -1
- package/dist/oss-lab.js +6 -6
- package/dist/oss-lab.js.map +1 -1
- package/dist/oss-meta-lab.d.ts +113 -1
- package/dist/oss-meta-lab.js +2753 -200
- package/dist/oss-meta-lab.js.map +1 -1
- package/dist/oss-remote-telemetry.d.ts +77 -0
- package/dist/oss-remote-telemetry.js +393 -0
- package/dist/oss-remote-telemetry.js.map +1 -0
- package/dist/program.d.ts +8 -0
- package/dist/program.js +668 -70
- package/dist/program.js.map +1 -1
- package/dist/run.d.ts +105 -3
- package/dist/run.js +684 -22
- package/dist/run.js.map +1 -1
- package/docs/architecture/local-codex-tui-actor.md +9 -6
- package/docs/architecture/oss-lab-poc.md +119 -47
- package/docs/architecture/project-layout.md +40 -6
- package/docs/assets/mimetic-oss-lab-observer.png +0 -0
- package/docs/contracts/feedback.md +15 -12
- package/docs/contracts/policy.md +9 -2
- package/docs/contracts/run-bundle.md +62 -0
- package/docs/contracts/schemas.md +21 -0
- package/docs/goals/current.md +50 -17
- package/docs/product/open-source-install-experience.md +63 -8
- package/docs/ramp/README.md +26 -8
- package/docs/roadmap/world-class-open-source-v0.md +41 -20
- package/package.json +9 -6
- package/skills/mimetic-cli/SKILL.md +89 -4
- 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
|
|
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
|
|
119
|
-
" mimetic lab
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
625
|
+
.description("List, inspect, and run Mimetic lab manifests.");
|
|
498
626
|
lab
|
|
499
|
-
.command("
|
|
500
|
-
.description("
|
|
501
|
-
.option("--
|
|
502
|
-
.option("--
|
|
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
|
-
"
|
|
519
|
-
" mimetic
|
|
719
|
+
"Preferred paths:",
|
|
720
|
+
" mimetic watch oss",
|
|
721
|
+
" mimetic lab run oss",
|
|
520
722
|
"",
|
|
521
723
|
"Repo selection:",
|
|
522
|
-
" mimetic
|
|
523
|
-
" mimetic lab oss --
|
|
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
|
|
534
|
-
"
|
|
535
|
-
"
|
|
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
|
|
539
|
-
" are
|
|
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
|
|
573
|
-
|
|
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 (
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
}
|