@rigkit/cli 0.0.0-canary-20260518T014918-c5bc0c2

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/src/cli.ts ADDED
@@ -0,0 +1,2496 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import { Command, CommanderError, Option } from "commander";
5
+ import inquirer from "inquirer";
6
+ import * as ui from "./ui.ts";
7
+ import {
8
+ getOrStartRuntime,
9
+ defaultRigkitHome,
10
+ type RuntimeClient,
11
+ } from "@rigkit/runtime-client";
12
+ import {
13
+ type DevMachineEvent,
14
+ type WorkflowPlan,
15
+ type SnapshotRecord,
16
+ type WorkspaceRecord,
17
+ } from "@rigkit/engine";
18
+ import {
19
+ cmuxHostCapabilities,
20
+ type CmuxHostCapabilityHandler,
21
+ } from "@rigkit/provider-cmux/host";
22
+ import { DEFAULT_CONFIG_FILE, discoverProjectConfigs, resolveConfigPaths, PROJECT_PACKAGE_NAME } from "./project.ts";
23
+ import { RIGKIT_CLI_VERSION } from "./version.ts";
24
+ import { initProject, normalizeMachineName, type InitProjectResult } from "./init.ts";
25
+ import { openExternalTarget } from "./interaction.ts";
26
+ import { createRunPresenter, type RunPresenter } from "./run-presenter.ts";
27
+ import { createRunLogger, type RunLogger } from "./run-logger.ts";
28
+ import {
29
+ completeRig,
30
+ formatCompletionItems,
31
+ renderCompletionScript,
32
+ resolveCompletionShell,
33
+ type CompletionShell,
34
+ } from "./completion.ts";
35
+ import { generateWorkspaceName } from "./workspace-name.ts";
36
+
37
+ type GlobalOptions = {
38
+ chdir?: string;
39
+ config?: string;
40
+ state?: string;
41
+ json: boolean;
42
+ };
43
+
44
+ type CliInvocation = {
45
+ global: GlobalOptions;
46
+ json: boolean;
47
+ };
48
+
49
+ type InitOptions = {
50
+ force: boolean;
51
+ name?: string;
52
+ apiKey?: string;
53
+ packageManager?: PackageManager;
54
+ };
55
+
56
+ type CompletionOptions = {
57
+ shell?: CompletionShell;
58
+ index?: string;
59
+ };
60
+
61
+ type DoctorOptions = {
62
+ cli: boolean;
63
+ };
64
+
65
+ type RunOptions = {
66
+ all: boolean;
67
+ discover: boolean;
68
+ };
69
+
70
+ type ListOptions = {
71
+ target?: string;
72
+ };
73
+
74
+ type CacheClearOptions = {
75
+ local: boolean;
76
+ global: boolean;
77
+ all: boolean;
78
+ };
79
+
80
+ type PackageManager = "npm" | "bun" | "pnpm" | "skip";
81
+
82
+ type InitInstallResult = {
83
+ packageManager: PackageManager;
84
+ command?: string;
85
+ skipped: boolean;
86
+ reported?: boolean;
87
+ };
88
+
89
+ type EngineProjectInfo = {
90
+ projectDir: string;
91
+ configPath: string;
92
+ statePath: string;
93
+ workflow?: {
94
+ name: string;
95
+ providers: string[];
96
+ };
97
+ };
98
+
99
+ type RuntimeOperationManifest = {
100
+ operations: RuntimeOperationDefinition[];
101
+ workspaceOperations?: RuntimeOperationDefinition[];
102
+ };
103
+
104
+ type RuntimeOperationDefinition = {
105
+ id: string;
106
+ aliases?: string[];
107
+ title?: string;
108
+ description?: string;
109
+ createsWorkspace?: boolean;
110
+ cli?: {
111
+ positionals?: Array<{ name: string; index: number }>;
112
+ options?: Array<{
113
+ name: string;
114
+ flag: string;
115
+ aliases?: string[];
116
+ required?: boolean;
117
+ runtime?: boolean;
118
+ type?: "string" | "boolean" | "number";
119
+ }>;
120
+ };
121
+ inputSchema?: {
122
+ properties?: Record<string, { type?: string; default?: unknown }>;
123
+ required?: string[];
124
+ };
125
+ };
126
+
127
+ type RuntimeOperationCliOption = NonNullable<NonNullable<RuntimeOperationDefinition["cli"]>["options"]>[number];
128
+
129
+ type ParsedOperationInput = {
130
+ input: Record<string, unknown>;
131
+ hostOptions: Record<string, unknown>;
132
+ };
133
+
134
+ const CLI_HOST_METHODS = [
135
+ { id: "message.show" },
136
+ { id: "prompt.text" },
137
+ { id: "prompt.confirm" },
138
+ { id: "prompt.select" },
139
+ { id: "open.external" },
140
+ { id: "host.command.run", modes: ["capture", "interactive"] },
141
+ ];
142
+
143
+ const CLI_HOST_CAPABILITY_HANDLERS = new Map<string, CmuxHostCapabilityHandler>(
144
+ cmuxHostCapabilities.map((capability) => [capability.id, capability]),
145
+ );
146
+
147
+ const CLI_HOST_CAPABILITIES: Array<{ id: string; schemaHash?: string }> = [
148
+ ...CLI_HOST_CAPABILITY_HANDLERS.values(),
149
+ ].map((capability) => ({
150
+ id: capability.id,
151
+ ...(capability.schemaHash ? { schemaHash: capability.schemaHash } : {}),
152
+ }));
153
+
154
+ const TERRAFORM_STYLE_GLOBAL_OPTIONS = new Set(["chdir", "config", "state", "json", "help", "version"]);
155
+ const STATIC_COMMANDS = new Set([
156
+ "init",
157
+ "plan",
158
+ "apply",
159
+ "create",
160
+ "rm",
161
+ "run",
162
+ "ls",
163
+ "cache",
164
+ "projects",
165
+ "doctor",
166
+ "version",
167
+ "help",
168
+ "completion",
169
+ ]);
170
+
171
+ if (process.argv[2] === "__complete") {
172
+ runCompletionEndpoint(process.argv.slice(3)).catch(handleCliError);
173
+ } else {
174
+ runCli(normalizeCliArgv(process.argv)).catch(handleCliError);
175
+ }
176
+
177
+ function normalizeCliArgv(argv: string[]): string[] {
178
+ const normalized = argv.slice();
179
+ for (let index = 2; index < normalized.length; index += 1) {
180
+ const arg = normalized[index]!;
181
+ if (arg === "--") break;
182
+ if (!arg.startsWith("-")) {
183
+ if (STATIC_COMMANDS.has(arg)) break;
184
+ continue;
185
+ }
186
+
187
+ const match = /^-([A-Za-z][A-Za-z0-9-]*)(=.*)?$/.exec(arg);
188
+ if (!match) continue;
189
+ const name = match[1]!;
190
+ if (!TERRAFORM_STYLE_GLOBAL_OPTIONS.has(name)) continue;
191
+ normalized[index] = `--${name}${match[2] ?? ""}`;
192
+ }
193
+ return normalized;
194
+ }
195
+
196
+ async function runCli(argv: string[]): Promise<void> {
197
+ const program = new Command();
198
+ program
199
+ .name("rig")
200
+ .description("Rigkit workflow CLI")
201
+ .usage("[global options] <command> [args]")
202
+ .version(RIGKIT_CLI_VERSION, "-v, --version", "Show Rigkit CLI version")
203
+ .showHelpAfterError()
204
+ .exitOverride()
205
+ .argument("[command]")
206
+ .addOption(new Option("--chdir <dir>", `Switch to a directory containing ${DEFAULT_CONFIG_FILE} before running the command`).hideHelp())
207
+ .addOption(new Option("--config <file>", "Config file to load, relative to -chdir when set").hideHelp())
208
+ .addOption(new Option("--state <file>", "Local runtime state database path").hideHelp())
209
+ .addOption(new Option("--json", "Print machine-readable JSON where supported").hideHelp())
210
+ .addHelpText("after", [
211
+ "",
212
+ "Global Options:",
213
+ " -chdir=DIR Switch to a directory containing rig.config.ts before running the command",
214
+ " -config=FILE Config file to load, relative to -chdir when set",
215
+ " -state=FILE Local runtime state database path",
216
+ " -json Print machine-readable JSON where supported",
217
+ ].join("\n"))
218
+ .action(async (command?: string) => {
219
+ if (command) program.error(`unknown command '${command}'`);
220
+ await runHelp(makeInvocation(rootOptions(program)));
221
+ });
222
+
223
+ program
224
+ .command("init")
225
+ .description("Initialize a Rigkit project")
226
+ .option("--name <name>", "Project and workflow name")
227
+ .option("--api-key <apiKey>", "Freestyle API key")
228
+ .option("--package-manager <packageManager>", "Install with npm, bun, pnpm, or skip")
229
+ .option("--force", "Overwrite an existing config file")
230
+ .option("--json", "Print machine-readable JSON")
231
+ .action(async (options: {
232
+ name?: string;
233
+ apiKey?: string;
234
+ packageManager?: string;
235
+ force?: boolean;
236
+ json?: boolean;
237
+ }) => {
238
+ await runInit(makeInvocation(rootOptions(program), options.json), {
239
+ name: options.name,
240
+ apiKey: options.apiKey,
241
+ packageManager: parsePackageManagerOption(options.packageManager),
242
+ force: Boolean(options.force),
243
+ });
244
+ });
245
+
246
+ for (const operation of ["plan", "apply"] as const) {
247
+ program
248
+ .command(`${operation} [args...]`)
249
+ .description(operation === "plan" ? "Plan project workflow changes" : "Apply project workflow changes")
250
+ .allowUnknownOption(true)
251
+ .option("--all", "Run against every discovered project")
252
+ .option("--discover", "Discover projects below the selected directory")
253
+ .option("--json", "Print machine-readable JSON")
254
+ .action(async (args: string[], options: { all?: boolean; discover?: boolean; json?: boolean }) => {
255
+ await runProjectOperation(makeInvocation(rootOptions(program), options.json), operation, args ?? [], {
256
+ all: Boolean(options.all),
257
+ discover: Boolean(options.discover),
258
+ });
259
+ });
260
+ }
261
+
262
+ program
263
+ .command("create [args...]")
264
+ .description("Create a workspace")
265
+ .allowUnknownOption(true)
266
+ .option("--json", "Print machine-readable JSON")
267
+ .action(async (args: string[], options: { json?: boolean }) => {
268
+ await runProjectOperation(makeInvocation(rootOptions(program), options.json), "create", args ?? [], {
269
+ all: false,
270
+ discover: false,
271
+ });
272
+ });
273
+
274
+ program
275
+ .command("rm [workspace]")
276
+ .description("Remove one workspace, several via multi-select, or every workspace")
277
+ .option("-y, --yes", "Remove without confirmation")
278
+ .option("--all", "Remove every workspace in this project")
279
+ .option("--json", "Print machine-readable JSON")
280
+ .action(async (workspace: string | undefined, options: { yes?: boolean; all?: boolean; json?: boolean }) => {
281
+ await runRemove(
282
+ makeInvocation(rootOptions(program), options.json),
283
+ {
284
+ workspace,
285
+ yes: Boolean(options.yes),
286
+ all: Boolean(options.all),
287
+ },
288
+ );
289
+ });
290
+
291
+ program
292
+ .command("run <workspace> <operation> [args...]")
293
+ .description("Run a workspace operation")
294
+ .allowUnknownOption(true)
295
+ .option("--json", "Print machine-readable JSON")
296
+ .action(async (
297
+ workspace: string,
298
+ operation: string,
299
+ args: string[],
300
+ options: { json?: boolean },
301
+ ) => {
302
+ await runWorkspaceOperation(makeInvocation(rootOptions(program), options.json), workspace, operation, args ?? []);
303
+ });
304
+
305
+ program
306
+ .command("ls [target]")
307
+ .description("List project workspaces")
308
+ .option("--json", "Print machine-readable JSON")
309
+ .action(async (target: string | undefined, options: { json?: boolean }) => {
310
+ await runList(makeInvocation(rootOptions(program), options.json), { target });
311
+ });
312
+
313
+ const cache = program
314
+ .command("cache")
315
+ .description("Inspect and clear Rigkit cache");
316
+
317
+ cache
318
+ .command("ls")
319
+ .description("List cache entries for the selected project config")
320
+ .option("--json", "Print machine-readable JSON")
321
+ .action(async (options: { json?: boolean }) => {
322
+ await runCacheList(makeInvocation(rootOptions(program), options.json));
323
+ });
324
+
325
+ cache
326
+ .command("clear")
327
+ .description("Clear cache entries")
328
+ .option("--local", "Clear local cache entries for the selected config")
329
+ .option("--global", "Clear global cache fragments")
330
+ .option("--all", "With --global, clear every global fragment without loading a config")
331
+ .option("--json", "Print machine-readable JSON")
332
+ .action(async (options: { local?: boolean; global?: boolean; all?: boolean; json?: boolean }) => {
333
+ await runCacheClear(makeInvocation(rootOptions(program), options.json), {
334
+ local: Boolean(options.local),
335
+ global: Boolean(options.global),
336
+ all: Boolean(options.all),
337
+ });
338
+ });
339
+
340
+ cache
341
+ .command("invalidate [step]")
342
+ .description("Invalidate cached task outputs so they re-run on next plan/apply")
343
+ .option("--all", "Invalidate every cached task in this project's workflow")
344
+ .option("-y, --yes", "Skip confirmation when invalidating --all")
345
+ .option("--json", "Print machine-readable JSON")
346
+ .action(async (step: string | undefined, options: { all?: boolean; yes?: boolean; json?: boolean }) => {
347
+ await runCacheInvalidate(makeInvocation(rootOptions(program), options.json), {
348
+ step,
349
+ all: Boolean(options.all),
350
+ yes: Boolean(options.yes),
351
+ });
352
+ });
353
+
354
+ program
355
+ .command("projects")
356
+ .description("Discover Rigkit projects below the current directory")
357
+ .option("--json", "Print machine-readable JSON")
358
+ .action(async (options: { json?: boolean }) => {
359
+ await runProjects(makeInvocation(rootOptions(program), options.json));
360
+ });
361
+
362
+ program
363
+ .command("doctor")
364
+ .description("Show Rigkit runtime diagnostics")
365
+ .option("--cli", "Show CLI diagnostics without connecting to a project runtime")
366
+ .option("--json", "Print machine-readable JSON")
367
+ .action(async (options: { cli?: boolean; json?: boolean }) => {
368
+ await runDoctor(makeInvocation(rootOptions(program), options.json), { cli: Boolean(options.cli) });
369
+ });
370
+
371
+ program
372
+ .command("version")
373
+ .description("Show Rigkit CLI version")
374
+ .option("--json", "Print machine-readable JSON")
375
+ .action(async (options: { json?: boolean }) => {
376
+ await runVersion(makeInvocation(rootOptions(program), options.json));
377
+ });
378
+
379
+ program
380
+ .command("help")
381
+ .description("Show Rigkit CLI help")
382
+ .option("--json", "Print machine-readable JSON")
383
+ .action(async (options: { json?: boolean }) => {
384
+ await runHelp(makeInvocation(rootOptions(program), options.json));
385
+ });
386
+
387
+ program
388
+ .command("completion [shell]")
389
+ .description("Generate shell completion script")
390
+ .action((shell?: string) => {
391
+ console.log(renderCompletionScript(resolveCompletionShell(shell)));
392
+ });
393
+ await program.parseAsync(argv);
394
+ }
395
+
396
+ function rootOptions(program: Command): GlobalOptions {
397
+ const options = program.opts<{
398
+ chdir?: string;
399
+ config?: string;
400
+ state?: string;
401
+ json?: boolean;
402
+ }>();
403
+ return {
404
+ chdir: options.chdir,
405
+ config: options.config,
406
+ state: options.state,
407
+ json: Boolean(options.json),
408
+ };
409
+ }
410
+
411
+ function parsePackageManagerOption(value: string | undefined): PackageManager | undefined {
412
+ if (value === undefined) return undefined;
413
+ if (isPackageManager(value)) return value;
414
+ throw new Error(`Unknown package manager ${value}. Expected npm, bun, pnpm, or skip.`);
415
+ }
416
+
417
+ function makeInvocation(global: GlobalOptions, commandJson = false): CliInvocation {
418
+ return {
419
+ global,
420
+ json: Boolean(global.json || commandJson),
421
+ };
422
+ }
423
+
424
+ async function runCompletionEndpoint(args: string[]): Promise<void> {
425
+ const options = parseCompletionEndpointArgs(args);
426
+ const shell = resolveCompletionShell(options.shell);
427
+ const currentIndex = options.index === undefined ? undefined : Number(options.index);
428
+ const items = await completeRig({
429
+ words: options.words,
430
+ currentIndex: Number.isFinite(currentIndex) ? currentIndex : undefined,
431
+ cwd: process.cwd(),
432
+ });
433
+ const output = formatCompletionItems(items, shell);
434
+ if (output) console.log(output);
435
+ }
436
+
437
+ function parseCompletionEndpointArgs(args: string[]): CompletionOptions & { words: string[] } {
438
+ const words: string[] = [];
439
+ const options: CompletionOptions = {};
440
+
441
+ for (let index = 0; index < args.length; index += 1) {
442
+ const arg = args[index]!;
443
+ if (arg === "--") {
444
+ words.push(...args.slice(index + 1));
445
+ break;
446
+ }
447
+ if (arg === "--shell") {
448
+ options.shell = args[++index] as CompletionShell | undefined;
449
+ continue;
450
+ }
451
+ if (arg.startsWith("--shell=")) {
452
+ options.shell = arg.slice("--shell=".length) as CompletionShell;
453
+ continue;
454
+ }
455
+ if (arg === "--index") {
456
+ options.index = args[++index];
457
+ continue;
458
+ }
459
+ if (arg.startsWith("--index=")) {
460
+ options.index = arg.slice("--index=".length);
461
+ continue;
462
+ }
463
+ words.push(arg);
464
+ }
465
+
466
+ return { ...options, words };
467
+ }
468
+
469
+ // Errors already rendered to the user (via printRunFailure or similar) carry
470
+ // this sentinel so handleCliError doesn't re-print the message a second time.
471
+ class DisplayedCliError extends Error {
472
+ readonly displayed = true as const;
473
+ constructor(message: string) {
474
+ super(message);
475
+ this.name = "DisplayedCliError";
476
+ }
477
+ }
478
+
479
+ function handleCliError(error: unknown): void {
480
+ if (error instanceof CommanderError) {
481
+ process.exitCode = error.exitCode;
482
+ return;
483
+ }
484
+ if (error instanceof DisplayedCliError) {
485
+ process.exitCode = 1;
486
+ return;
487
+ }
488
+ console.error(error instanceof Error ? error.message : String(error));
489
+ process.exitCode = 1;
490
+ }
491
+
492
+ async function runInit(invocation: CliInvocation, options: InitOptions): Promise<void> {
493
+ const answers = await resolveInitAnswers(options, wantsJson(invocation));
494
+ const paths = resolveInitProjectPaths(invocation, answers.name);
495
+
496
+ if (existsSync(paths.configPath) && !options.force) {
497
+ throw new Error(`${paths.configPath} already exists. Pass --force to overwrite it.`);
498
+ }
499
+
500
+ const result = initProject({
501
+ projectDir: paths.projectDir,
502
+ configPath: paths.configPath,
503
+ name: answers.name,
504
+ apiKey: answers.apiKey,
505
+ force: options.force,
506
+ });
507
+ const install = await runPackageManagerInstall(paths.projectDir, answers.packageManager, wantsJson(invocation));
508
+
509
+ if (wantsJson(invocation)) {
510
+ printJson({ ...result, install });
511
+ return;
512
+ }
513
+
514
+ printInitResult(result, install);
515
+ }
516
+
517
+ async function resolveInitAnswers(
518
+ options: InitOptions,
519
+ jsonMode: boolean,
520
+ ): Promise<{ name: string; apiKey?: string; packageManager: PackageManager }> {
521
+ if (jsonMode && options.name === undefined) {
522
+ throw new Error(`rig init --json requires --name`);
523
+ }
524
+
525
+ if (jsonMode && options.packageManager && options.packageManager !== "skip") {
526
+ throw new Error(`rig init --json only supports --package-manager skip`);
527
+ }
528
+
529
+ if (options.name === undefined) {
530
+ assertInteractiveInit();
531
+ }
532
+
533
+ if (!jsonMode) {
534
+ console.log(`${ui.bold("rig")} ${ui.dim("· initialize")}`);
535
+ console.log("");
536
+ }
537
+
538
+ const name = options.name !== undefined
539
+ ? normalizeMachineName(options.name)
540
+ : await promptName();
541
+ const apiKey = options.apiKey?.trim();
542
+ const packageManager = options.packageManager ?? (jsonMode || !canPrompt() ? "skip" : await promptPackageManager("skip"));
543
+
544
+ return {
545
+ name,
546
+ apiKey,
547
+ packageManager,
548
+ };
549
+ }
550
+
551
+ function assertInteractiveInit(): void {
552
+ if (canPrompt()) return;
553
+ throw new Error(`rig init needs --name when not running in an interactive terminal`);
554
+ }
555
+
556
+ function canPrompt(): boolean {
557
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
558
+ }
559
+
560
+ function resolveInitProjectPaths(invocation: CliInvocation, name: string): { projectDir: string; configPath: string } {
561
+ const options = invocation.global;
562
+ if (options.config) {
563
+ throw new Error(`rig init does not support -config. Use -chdir to choose the parent directory.`);
564
+ }
565
+
566
+ const parentDir = resolve(process.cwd(), options.chdir ?? ".");
567
+ const projectDir = resolve(parentDir, name);
568
+ return {
569
+ projectDir,
570
+ configPath: join(projectDir, DEFAULT_CONFIG_FILE),
571
+ };
572
+ }
573
+
574
+ async function promptName(): Promise<string> {
575
+ const answers = await inquirer.prompt<{ name: string }>([{
576
+ type: "input",
577
+ name: "name",
578
+ message: "Project name:",
579
+ validate(value: string) {
580
+ try {
581
+ normalizeMachineName(value);
582
+ return true;
583
+ } catch (error) {
584
+ return error instanceof Error ? error.message : String(error);
585
+ }
586
+ },
587
+ filter: (value: string) => normalizeMachineName(value),
588
+ }]);
589
+ return answers.name;
590
+ }
591
+
592
+ async function promptPackageManager(defaultValue: PackageManager): Promise<PackageManager> {
593
+ const choices: Array<{ value: PackageManager; label: string; hint: string }> = [
594
+ { value: "npm", label: "npm", hint: "npm install" },
595
+ { value: "bun", label: "bun", hint: "bun install" },
596
+ { value: "pnpm", label: "pnpm", hint: "pnpm install" },
597
+ { value: "skip", label: "skip", hint: "do not install now" },
598
+ ];
599
+ const answers = await inquirer.prompt<{ packageManager: PackageManager }>([{
600
+ type: "select",
601
+ name: "packageManager",
602
+ message: "Install dependencies?",
603
+ default: defaultValue,
604
+ choices: choices.map((choice) => ({
605
+ name: choice.label,
606
+ value: choice.value,
607
+ description: choice.hint,
608
+ })),
609
+ }]);
610
+ return answers.packageManager;
611
+ }
612
+
613
+ async function promptWorkspaceName(defaultValue: string): Promise<string> {
614
+ const answers = await inquirer.prompt<{ name: string }>([{
615
+ type: "input",
616
+ name: "name",
617
+ message: "Workspace name:",
618
+ default: defaultValue,
619
+ validate(value: string) {
620
+ return validateWorkspaceName(value.trim());
621
+ },
622
+ filter: (value: string) => value.trim(),
623
+ }]);
624
+ return answers.name;
625
+ }
626
+
627
+ const workspaceNamePattern = /^(?!-)[A-Za-z0-9._-]+$/;
628
+
629
+ function validateWorkspaceName(value: string): true | string {
630
+ if (!value) return "Workspace name is required.";
631
+ if (!workspaceNamePattern.test(value)) {
632
+ return 'Use only letters, numbers, ".", "_", and "-", and do not start with "-".';
633
+ }
634
+ return true;
635
+ }
636
+
637
+ function assertValidWorkspaceName(value: unknown): void {
638
+ if (typeof value !== "string") throw new Error(`Workspace name must be a string`);
639
+ const valid = validateWorkspaceName(value);
640
+ if (valid !== true) throw new Error(`Invalid workspace name "${value}". ${valid}`);
641
+ }
642
+
643
+ async function defaultWorkspaceName(runtime: RuntimeClient): Promise<string> {
644
+ const existingNames = await runtime.control.workspaces()
645
+ .then((response) => response.workspaces.map((workspace) => workspace.name))
646
+ .catch(() => []);
647
+ return generateWorkspaceName(existingNames);
648
+ }
649
+
650
+ async function runPackageManagerInstall(
651
+ projectDir: string,
652
+ packageManager: PackageManager,
653
+ jsonMode: boolean,
654
+ ): Promise<InitInstallResult> {
655
+ if (packageManager === "skip") {
656
+ return { packageManager, skipped: true };
657
+ }
658
+
659
+ const command = packageManagerInstallCommand(packageManager);
660
+ if (!jsonMode) {
661
+ process.stderr.write(`${ui.accent(ui.sym.active)} ${ui.dim(`$ ${command.join(" ")}`)}\n`);
662
+ }
663
+
664
+ const proc = Bun.spawn(command, {
665
+ cwd: projectDir,
666
+ stdin: "inherit",
667
+ stdout: jsonMode ? "pipe" : "inherit",
668
+ stderr: jsonMode ? "pipe" : "inherit",
669
+ });
670
+ const exitCode = await proc.exited;
671
+
672
+ if (exitCode !== 0) {
673
+ throw new Error(`${command.join(" ")} failed with exit code ${exitCode}`);
674
+ }
675
+
676
+ return { packageManager, command: command.join(" "), skipped: false };
677
+ }
678
+
679
+ function printInitResult(result: InitProjectResult, install: InitInstallResult): void {
680
+ console.log(`${ui.ok(ui.sym.ok)} ${ui.bold(result.name)} ${ui.dim("ready")}`);
681
+ console.log("");
682
+
683
+ const fileLines = [
684
+ ui.fileStatus(initStatus(result.created.config, false), shortPath(result.configPath)),
685
+ ui.fileStatus(initStatus(result.created.env, result.updated.envApiKey), shortPath(result.envPath)),
686
+ ui.fileStatus(initStatus(result.created.envExample, false), shortPath(result.envExamplePath)),
687
+ ui.fileStatus(initStatus(result.created.packageJson, result.updated.packageJson), shortPath(result.packageJsonPath)),
688
+ ui.fileStatus(initStatus(result.created.gitignore, result.updated.gitignore), shortPath(result.gitignorePath)),
689
+ ];
690
+ if (result.updated.sdkDependency) {
691
+ fileLines.push(ui.fileStatus("pinned", `${PROJECT_PACKAGE_NAME}@${RIGKIT_CLI_VERSION}`));
692
+ }
693
+ for (const line of fileLines) console.log(line);
694
+
695
+ if (!install.skipped && install.command && !install.reported) {
696
+ console.log(ui.fileStatus("created", install.command));
697
+ }
698
+
699
+ console.log("");
700
+ console.log(ui.bold("Next"));
701
+ console.log(ui.hint(`cd ${displayProjectDir(result.projectDir)}`));
702
+ if (install.skipped) {
703
+ console.log(ui.hint(detectInstallCommand(result.packageJsonPath)));
704
+ }
705
+ console.log(ui.hint("rig plan"));
706
+ }
707
+
708
+ function initStatus(created: boolean, updated: boolean): ui.FileStatus {
709
+ if (created) return "created";
710
+ if (updated) return "updated";
711
+ return "kept";
712
+ }
713
+
714
+ function shortPath(path: string): string {
715
+ const rel = relative(process.cwd(), path);
716
+ return rel && !rel.startsWith("..") ? rel : path;
717
+ }
718
+
719
+ function displayProjectDir(projectDir: string): string {
720
+ const path = relative(process.cwd(), projectDir);
721
+ return path && !path.startsWith("..") ? path : projectDir;
722
+ }
723
+
724
+ function detectInstallCommand(packageJsonPath: string): string {
725
+ const projectDir = dirname(packageJsonPath);
726
+ if (existsSync(join(projectDir, "bun.lock")) || existsSync(join(projectDir, "bun.lockb"))) return "bun install";
727
+ if (existsSync(join(projectDir, "pnpm-lock.yaml"))) return "pnpm install";
728
+ if (existsSync(join(projectDir, "yarn.lock"))) return "yarn install";
729
+ if (existsSync(join(projectDir, "package-lock.json"))) return "npm install";
730
+ return "npm install";
731
+ }
732
+
733
+ function isPackageManager(value: string): value is PackageManager {
734
+ return value === "npm" || value === "bun" || value === "pnpm" || value === "skip";
735
+ }
736
+
737
+ function packageManagerInstallCommand(packageManager: Exclude<PackageManager, "skip">): string[] {
738
+ switch (packageManager) {
739
+ case "bun":
740
+ return ["bun", "install"];
741
+ case "pnpm":
742
+ return ["pnpm", "install"];
743
+ case "npm":
744
+ return ["npm", "install"];
745
+ }
746
+ }
747
+
748
+ async function runProjectOperation(
749
+ invocation: CliInvocation,
750
+ requestedOperation: string,
751
+ args: string[],
752
+ options: RunOptions,
753
+ ): Promise<void> {
754
+ if (options.all || options.discover) {
755
+ await runDiscoveredProjectOperation(invocation, requestedOperation, args, { all: options.all });
756
+ return;
757
+ }
758
+
759
+ const runtime = await loadRuntime(invocation);
760
+ const { operation, parsed, result } = await executeRuntimeOperation(
761
+ invocation,
762
+ runtime,
763
+ requestedOperation,
764
+ args,
765
+ );
766
+
767
+ if (wantsJson(invocation)) {
768
+ printJson(result);
769
+ return;
770
+ }
771
+
772
+ await renderOperationResult(operation, result, parsed.hostOptions);
773
+ if (operation.createsWorkspace && isWorkspaceRecord(result)) {
774
+ await printWorkspaceNextSteps(runtime, result.name);
775
+ }
776
+ printInteractiveOutputGap(invocation);
777
+ }
778
+
779
+ async function runWorkspaceOperation(
780
+ invocation: CliInvocation,
781
+ workspaceName: string,
782
+ requestedOperation: string,
783
+ args: string[],
784
+ ): Promise<void> {
785
+ const runtime = await loadRuntime(invocation);
786
+ const manifest = await readRuntimeOperations(runtime);
787
+ const operation = (manifest.workspaceOperations ?? []).find((item) =>
788
+ item.id === requestedOperation || item.aliases?.includes(requestedOperation)
789
+ );
790
+ if (!operation) {
791
+ throw new Error(`This project does not define a workspace operation named "${requestedOperation}".`);
792
+ }
793
+
794
+ const workspaces = await runtime.control.workspaces()
795
+ .then((response) => response.workspaces as WorkspaceRecord[])
796
+ .catch(() => []);
797
+ if (!workspaces.some((workspace) => workspace.name === workspaceName)) {
798
+ throw new Error(`This project does not have a workspace named "${workspaceName}".`);
799
+ }
800
+
801
+ const parsed = parseOperationArgs(operation, args);
802
+ enforceHostOnlyBooleanGuards(operation, parsed);
803
+
804
+ const result = await runRuntimeOperation<unknown>(
805
+ runtime,
806
+ `${workspaceName}/${operation.id}`,
807
+ parsed.input,
808
+ { renderEvents: !wantsJson(invocation) },
809
+ );
810
+
811
+ if (wantsJson(invocation)) {
812
+ printJson(result);
813
+ return;
814
+ }
815
+
816
+ await renderOperationResult(operation, result, parsed.hostOptions);
817
+ printInteractiveOutputGap(invocation);
818
+ }
819
+
820
+ async function runRemove(
821
+ invocation: CliInvocation,
822
+ options: { workspace?: string; yes: boolean; all: boolean },
823
+ ): Promise<void> {
824
+ if (options.workspace && options.all) {
825
+ throw new Error(`rig rm accepts either a workspace name or --all, not both`);
826
+ }
827
+
828
+ if (options.workspace) {
829
+ await runRemoveWorkspaceOperation(invocation, options.workspace, { yes: options.yes });
830
+ return;
831
+ }
832
+
833
+ const runtime = await loadRuntime(invocation);
834
+ const workspaces = await runtime.control.workspaces()
835
+ .then((response) => response.workspaces as WorkspaceRecord[])
836
+ .catch(() => []);
837
+ if (workspaces.length === 0) {
838
+ if (wantsJson(invocation)) {
839
+ printJson({ removed: [] });
840
+ return;
841
+ }
842
+ console.log(ui.dim("no workspaces"));
843
+ return;
844
+ }
845
+
846
+ let targets: string[];
847
+ if (options.all) {
848
+ if (!options.yes && !wantsJson(invocation) && canPrompt()) {
849
+ const confirmed = await promptHostConfirm({
850
+ message: `Remove all ${workspaces.length} workspaces?`,
851
+ defaultValue: false,
852
+ });
853
+ if (!confirmed) throw new Error("Remove cancelled");
854
+ } else if (!options.yes && !canPrompt()) {
855
+ throw new Error(`rig rm --all needs --yes when not running in an interactive terminal`);
856
+ }
857
+ targets = workspaces.map((workspace) => workspace.name);
858
+ } else {
859
+ if (wantsJson(invocation) || !canPrompt()) {
860
+ throw new Error(`rig rm needs a workspace name or --all when not running in an interactive terminal`);
861
+ }
862
+ targets = await promptWorkspaceRemoveSelection(workspaces);
863
+ if (targets.length === 0) throw new Error("Nothing selected");
864
+ }
865
+
866
+ const removed: string[] = [];
867
+ for (const name of targets) {
868
+ await runRemoveWorkspaceOperation(invocation, name, { yes: true });
869
+ removed.push(name);
870
+ }
871
+ if (wantsJson(invocation)) printJson({ removed });
872
+ }
873
+
874
+ async function promptWorkspaceRemoveSelection(
875
+ workspaces: ReadonlyArray<Pick<WorkspaceRecord, "name" | "workflow">>,
876
+ ): Promise<string[]> {
877
+ const answers = await inquirer.prompt<{ names: string[] }>([{
878
+ type: "checkbox",
879
+ name: "names",
880
+ message: "Select workspaces to remove",
881
+ choices: workspaces.map((workspace) => ({
882
+ name: workspace.name,
883
+ value: workspace.name,
884
+ description: workspace.workflow ? `workflow ${workspace.workflow}` : undefined,
885
+ })),
886
+ }]);
887
+ return answers.names;
888
+ }
889
+
890
+ async function runRemoveWorkspaceOperation(
891
+ invocation: CliInvocation,
892
+ workspaceName: string,
893
+ options: { yes: boolean },
894
+ ): Promise<void> {
895
+ const runtime = await loadRuntime(invocation);
896
+ const manifest = await readRuntimeOperations(runtime);
897
+ const operation = (manifest.workspaceOperations ?? []).find((item) => item.id === "remove");
898
+ if (!operation) {
899
+ throw new Error(`This project does not define a removable workspace.`);
900
+ }
901
+
902
+ const workspaces = await runtime.control.workspaces()
903
+ .then((response) => response.workspaces as WorkspaceRecord[])
904
+ .catch(() => []);
905
+ if (!workspaces.some((workspace) => workspace.name === workspaceName)) {
906
+ throw new Error(`This project does not have a workspace named "${workspaceName}".`);
907
+ }
908
+
909
+ if (!options.yes && !wantsJson(invocation) && canPrompt()) {
910
+ const confirmed = await promptHostConfirm({
911
+ message: `Remove workspace ${workspaceName}?`,
912
+ defaultValue: false,
913
+ });
914
+ if (!confirmed) throw new Error("Remove cancelled");
915
+ options = { yes: true };
916
+ }
917
+
918
+ const parsed = parseOperationArgs(operation, options.yes ? ["--yes"] : []);
919
+ enforceHostOnlyBooleanGuards(operation, parsed);
920
+
921
+ const result = await runRuntimeOperation<unknown>(
922
+ runtime,
923
+ `${workspaceName}/remove`,
924
+ parsed.input,
925
+ { renderEvents: !wantsJson(invocation) },
926
+ );
927
+
928
+ if (wantsJson(invocation)) {
929
+ printJson(result);
930
+ return;
931
+ }
932
+
933
+ await renderOperationResult(operation, result, parsed.hostOptions);
934
+ printInteractiveOutputGap(invocation);
935
+ }
936
+
937
+ async function runDiscoveredProjectOperation(
938
+ invocation: CliInvocation,
939
+ requestedOperation: string,
940
+ args: string[],
941
+ options: { all: boolean },
942
+ ): Promise<void> {
943
+ const projects = discoverProjectConfigs({
944
+ chdir: invocation.global.chdir,
945
+ config: invocation.global.config,
946
+ });
947
+ if (projects.length === 0) {
948
+ throw new Error("No Rigkit projects found.");
949
+ }
950
+ if (!options.all && projects.length > 1) {
951
+ throw new Error([
952
+ "Multiple Rigkit projects found.",
953
+ "Use `rig projects` to list candidates, pass -chdir or -config to select one, or pass --all to run every discovered project.",
954
+ ...projects.map((project) => `- ${project.configPath}`),
955
+ ].join("\n"));
956
+ }
957
+ if (invocation.global.state && projects.length > 1) {
958
+ throw new Error(`--state cannot be used with multiple discovered projects`);
959
+ }
960
+
961
+ const results: Array<{
962
+ project: { projectDir: string; configPath: string };
963
+ operation: string;
964
+ result: unknown;
965
+ }> = [];
966
+
967
+ for (const project of projects) {
968
+ const runtime = await getOrStartRuntime({
969
+ projectDir: project.projectDir,
970
+ configPath: project.configPath,
971
+ statePath: invocation.global.state ? resolveGlobalPath(invocation, invocation.global.state) : undefined,
972
+ });
973
+ const { operation, parsed, result } = await executeRuntimeOperation(
974
+ invocation,
975
+ runtime,
976
+ requestedOperation,
977
+ args,
978
+ );
979
+ results.push({
980
+ project,
981
+ operation: operation.id,
982
+ result,
983
+ });
984
+
985
+ if (!wantsJson(invocation)) {
986
+ if (projects.length > 1) {
987
+ console.log(ui.bold(displayProjectDir(project.projectDir)));
988
+ }
989
+ await renderOperationResult(operation, result, parsed.hostOptions);
990
+ printInteractiveOutputGap(invocation);
991
+ }
992
+ }
993
+
994
+ if (wantsJson(invocation)) {
995
+ printJson({ projects: results });
996
+ }
997
+ }
998
+
999
+ async function runProjects(invocation: CliInvocation): Promise<void> {
1000
+ const projects = discoverProjectConfigs({
1001
+ chdir: invocation.global.chdir,
1002
+ config: invocation.global.config,
1003
+ });
1004
+ if (wantsJson(invocation)) {
1005
+ printJson({ projects });
1006
+ return;
1007
+ }
1008
+ if (projects.length === 0) {
1009
+ console.log(ui.dim("no Rigkit projects found"));
1010
+ return;
1011
+ }
1012
+ const rows = projects.map((project) => [
1013
+ { text: project.projectDir, style: ui.bold },
1014
+ { text: project.configPath, style: ui.dim },
1015
+ ]);
1016
+ console.log(ui.columns(["project", "config"], rows));
1017
+ }
1018
+
1019
+ async function runList(invocation: CliInvocation, options: ListOptions): Promise<void> {
1020
+ const target = normalizeListTarget(options.target);
1021
+ const runtime = await loadRuntime(invocation);
1022
+
1023
+ if (target === "workspaces") {
1024
+ const { workspaces } = await runtime.control.workspaces();
1025
+ if (wantsJson(invocation)) {
1026
+ printJson({ workspaces });
1027
+ return;
1028
+ }
1029
+ printWorkspaces(workspaces);
1030
+ return;
1031
+ }
1032
+
1033
+ if (target === "snapshots") {
1034
+ const { snapshots } = await runtime.control.snapshots();
1035
+ if (wantsJson(invocation)) {
1036
+ printJson({ snapshots });
1037
+ return;
1038
+ }
1039
+ printSnapshots(snapshots as SnapshotRecord[]);
1040
+ return;
1041
+ }
1042
+
1043
+ const project = await readRuntimeProject(runtime);
1044
+ if (wantsJson(invocation)) {
1045
+ printJson(project);
1046
+ return;
1047
+ }
1048
+ printConfig(project);
1049
+ }
1050
+
1051
+ async function runCacheList(invocation: CliInvocation): Promise<void> {
1052
+ const runtime = await loadRuntime(invocation);
1053
+ const cache = await runtime.control.cache();
1054
+ if (wantsJson(invocation)) {
1055
+ printJson(cache);
1056
+ return;
1057
+ }
1058
+ printCacheEntries(cache.entries);
1059
+ }
1060
+
1061
+ async function runCacheClear(invocation: CliInvocation, options: CacheClearOptions): Promise<void> {
1062
+ if (options.all && !options.global) {
1063
+ throw new Error(`rig cache clear --all must be combined with --global`);
1064
+ }
1065
+ if (options.local && options.global && !options.all) {
1066
+ throw new Error(`Choose --local or --global, not both`);
1067
+ }
1068
+
1069
+ if (options.global && options.all) {
1070
+ if (invocation.global.chdir || invocation.global.config || invocation.global.state) {
1071
+ throw new Error(`rig cache clear --global --all cannot be combined with -chdir, -config, or -state`);
1072
+ }
1073
+ const fragmentRoot = join(defaultRigkitHome(), "fragments");
1074
+ rmSync(fragmentRoot, { recursive: true, force: true });
1075
+ if (wantsJson(invocation)) {
1076
+ printJson({ ok: true, deleted: null, scope: "global-all", fragmentRoot });
1077
+ return;
1078
+ }
1079
+ console.log(`Cleared global fragment cache at ${fragmentRoot}`);
1080
+ return;
1081
+ }
1082
+
1083
+ const scope = options.local ? "local" : options.global ? "global" : "all";
1084
+ const runtime = await loadRuntime(invocation);
1085
+ const result = await runtime.control.clearCache({ scope });
1086
+ if (wantsJson(invocation)) {
1087
+ printJson(result);
1088
+ return;
1089
+ }
1090
+ console.log(`Cleared ${result.deleted} cache ${result.deleted === 1 ? "entry" : "entries"}.`);
1091
+ }
1092
+
1093
+ type CacheInvalidateOptions = {
1094
+ step?: string;
1095
+ all: boolean;
1096
+ yes: boolean;
1097
+ };
1098
+
1099
+ async function runCacheInvalidate(invocation: CliInvocation, options: CacheInvalidateOptions): Promise<void> {
1100
+ if (options.step && options.all) {
1101
+ throw new Error(`rig cache invalidate accepts either a step or --all, not both`);
1102
+ }
1103
+
1104
+ const runtime = await loadRuntime(invocation);
1105
+ let targets: string[] = [];
1106
+
1107
+ if (options.step) {
1108
+ targets = [options.step];
1109
+ } else if (options.all) {
1110
+ if (!options.yes && !wantsJson(invocation) && canPrompt()) {
1111
+ const confirmed = await promptHostConfirm({
1112
+ message: "Invalidate every cached task in this project?",
1113
+ defaultValue: false,
1114
+ });
1115
+ if (!confirmed) throw new Error("Invalidate cancelled");
1116
+ } else if (!options.yes && !canPrompt()) {
1117
+ throw new Error(`rig cache invalidate --all needs --yes when not running in an interactive terminal`);
1118
+ }
1119
+ targets = []; // empty = engine invalidates everything for the workflow
1120
+ } else {
1121
+ // Interactive: multi-select among currently-valid cache entries.
1122
+ const cache = await runtime.control.cache();
1123
+ const candidates = cache.entries
1124
+ .filter((entry) => !entry.invalidated && entry.scope === "local")
1125
+ .map((entry) => ({ path: entry.nodePath || entry.nodeName, workflow: entry.workflow }));
1126
+ if (candidates.length === 0) {
1127
+ if (wantsJson(invocation)) {
1128
+ printJson({ ok: true, invalidated: 0 });
1129
+ return;
1130
+ }
1131
+ console.log(ui.dim("no valid cache entries to invalidate"));
1132
+ return;
1133
+ }
1134
+ if (wantsJson(invocation) || !canPrompt()) {
1135
+ throw new Error(`rig cache invalidate needs a step name or --all when not running in an interactive terminal`);
1136
+ }
1137
+ const picked = await promptCacheInvalidateSelection(candidates);
1138
+ if (picked.length === 0) throw new Error("Nothing selected");
1139
+ targets = picked;
1140
+ }
1141
+
1142
+ const result = await runtime.control.invalidateCache({ nodePaths: targets });
1143
+ if (wantsJson(invocation)) {
1144
+ printJson(result);
1145
+ return;
1146
+ }
1147
+ console.log(
1148
+ `${ui.ok(ui.sym.ok)} invalidated ${result.invalidated} cache ${result.invalidated === 1 ? "entry" : "entries"}`,
1149
+ );
1150
+ }
1151
+
1152
+ async function promptCacheInvalidateSelection(
1153
+ candidates: Array<{ path: string; workflow: string }>,
1154
+ ): Promise<string[]> {
1155
+ const answers = await inquirer.prompt<{ paths: string[] }>([{
1156
+ type: "checkbox",
1157
+ name: "paths",
1158
+ message: "Select tasks to invalidate",
1159
+ choices: candidates.map((c) => ({
1160
+ name: c.path,
1161
+ value: c.path,
1162
+ description: c.workflow ? `workflow ${c.workflow}` : undefined,
1163
+ })),
1164
+ }]);
1165
+ return answers.paths;
1166
+ }
1167
+
1168
+ async function executeRuntimeOperation(
1169
+ invocation: CliInvocation,
1170
+ runtime: RuntimeClient,
1171
+ requestedOperation: string,
1172
+ args: string[],
1173
+ ): Promise<{
1174
+ operation: RuntimeOperationDefinition;
1175
+ parsed: ParsedOperationInput;
1176
+ result: unknown;
1177
+ }> {
1178
+ const manifest = await readRuntimeOperations(runtime);
1179
+ const resolved = findRuntimeOperation(manifest, requestedOperation);
1180
+ if (!resolved) {
1181
+ throw new Error(`This project does not define a Rigkit operation named "${requestedOperation}".`);
1182
+ }
1183
+ const { operation, runOperation } = resolved;
1184
+
1185
+ const parsed = await parseOperationArgsWithPrompts(invocation, runtime, operation, args);
1186
+ enforceHostOnlyBooleanGuards(operation, parsed);
1187
+
1188
+ const result = await runRuntimeOperation<unknown>(
1189
+ runtime,
1190
+ runOperation,
1191
+ parsed.input,
1192
+ { renderEvents: !wantsJson(invocation) },
1193
+ );
1194
+
1195
+ return { operation, parsed, result };
1196
+ }
1197
+
1198
+ function findRuntimeOperation(
1199
+ manifest: RuntimeOperationManifest,
1200
+ requestedOperation: string,
1201
+ ): { operation: RuntimeOperationDefinition; runOperation: string } | undefined {
1202
+ const operation = manifest.operations.find((operation) =>
1203
+ operation.id === requestedOperation || operation.aliases?.includes(requestedOperation)
1204
+ );
1205
+ return operation ? { operation, runOperation: operation.id } : undefined;
1206
+ }
1207
+
1208
+ async function parseOperationArgsWithPrompts(
1209
+ invocation: CliInvocation,
1210
+ runtime: RuntimeClient,
1211
+ operation: RuntimeOperationDefinition,
1212
+ args: string[],
1213
+ ): Promise<ParsedOperationInput> {
1214
+ const parsed = parseOperationArgs(operation, args, {
1215
+ allowMissingRequired: !wantsJson(invocation) && canPrompt(),
1216
+ });
1217
+
1218
+ if (
1219
+ operation.createsWorkspace &&
1220
+ parsed.input.name === undefined &&
1221
+ !wantsJson(invocation) &&
1222
+ canPrompt()
1223
+ ) {
1224
+ parsed.input.name = await promptWorkspaceName(await defaultWorkspaceName(runtime));
1225
+ }
1226
+ if (operation.createsWorkspace && parsed.input.name !== undefined) {
1227
+ assertValidWorkspaceName(parsed.input.name);
1228
+ }
1229
+
1230
+ enforceRequiredOperationInputs(operation, parsed);
1231
+ return parsed;
1232
+ }
1233
+
1234
+ function parseOperationArgs(
1235
+ operation: RuntimeOperationDefinition,
1236
+ args: string[],
1237
+ options: { allowMissingRequired?: boolean } = {},
1238
+ ): ParsedOperationInput {
1239
+ const cli = inferCliMetadata(operation);
1240
+ const input: Record<string, unknown> = {};
1241
+ const hostOptions: Record<string, unknown> = {};
1242
+ const positionals = cli.positionals ?? [];
1243
+ let positionalIndex = 0;
1244
+
1245
+ for (let index = 0; index < args.length; index += 1) {
1246
+ const arg = args[index]!;
1247
+
1248
+ if (arg === "--") {
1249
+ for (const value of args.slice(index + 1)) {
1250
+ assignPositional(positionals, positionalIndex++, value, input);
1251
+ }
1252
+ break;
1253
+ }
1254
+
1255
+ if (arg.startsWith("--")) {
1256
+ const [flag, inlineValue] = splitFlag(arg);
1257
+ const option = cli.options?.find((item) => item.flag === flag || item.aliases?.includes(flag));
1258
+ if (!option) throw new Error(`Unknown option ${flag} for operation ${operation.id}`);
1259
+ const rawValue = option.type === "boolean"
1260
+ ? inlineValue ?? true
1261
+ : inlineValue ?? readOptionValue(args, ++index, flag);
1262
+ assignCliValue(option, rawValue, input, hostOptions);
1263
+ continue;
1264
+ }
1265
+
1266
+ if (arg.startsWith("-") && arg !== "-") {
1267
+ const option = cli.options?.find((item) => item.flag === arg || item.aliases?.includes(arg));
1268
+ if (!option) throw new Error(`Unknown option ${arg} for operation ${operation.id}`);
1269
+ const rawValue = option.type === "boolean" ? true : readOptionValue(args, ++index, arg);
1270
+ assignCliValue(option, rawValue, input, hostOptions);
1271
+ continue;
1272
+ }
1273
+
1274
+ assignPositional(positionals, positionalIndex++, arg, input);
1275
+ }
1276
+
1277
+ if (!options.allowMissingRequired) enforceRequiredOperationInputs(operation, { input, hostOptions });
1278
+
1279
+ return { input, hostOptions };
1280
+ }
1281
+
1282
+ function enforceRequiredOperationInputs(
1283
+ operation: RuntimeOperationDefinition,
1284
+ parsed: ParsedOperationInput,
1285
+ ): void {
1286
+ const cli = inferCliMetadata(operation);
1287
+ for (const option of cli.options ?? []) {
1288
+ if (option.required && parsed.input[option.name] === undefined && parsed.hostOptions[option.name] === undefined) {
1289
+ throw new Error(`Operation ${operation.id} requires ${option.flag}`);
1290
+ }
1291
+ }
1292
+
1293
+ for (const name of operation.inputSchema?.required ?? []) {
1294
+ if (parsed.input[name] === undefined) {
1295
+ throw new Error(`Operation ${operation.id} requires ${name}`);
1296
+ }
1297
+ }
1298
+ }
1299
+
1300
+ function enforceHostOnlyBooleanGuards(
1301
+ operation: RuntimeOperationDefinition,
1302
+ parsed: ParsedOperationInput,
1303
+ ): void {
1304
+ const guard = operation.cli?.options?.find((option) =>
1305
+ option.runtime === false &&
1306
+ option.type === "boolean" &&
1307
+ (option.name === "yes" || option.name === "confirm") &&
1308
+ parsed.hostOptions[option.name] !== true
1309
+ );
1310
+ if (!guard) return;
1311
+ throw new Error(`Operation ${operation.id} requires ${guard.flag}`);
1312
+ }
1313
+
1314
+ function inferCliMetadata(operation: RuntimeOperationDefinition): Required<NonNullable<RuntimeOperationDefinition["cli"]>> {
1315
+ const properties = operation.inputSchema?.properties ?? {};
1316
+ return {
1317
+ positionals: operation.cli?.positionals ?? [],
1318
+ options: operation.cli?.options ?? Object.entries(properties).map(([name, schema]) => ({
1319
+ name,
1320
+ flag: `--${dashCase(name)}`,
1321
+ required: operation.inputSchema?.required?.includes(name),
1322
+ type: schema.type === "boolean" ? "boolean" : schema.type === "number" ? "number" : "string",
1323
+ })),
1324
+ };
1325
+ }
1326
+
1327
+ function dashCase(value: string): string {
1328
+ return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
1329
+ }
1330
+
1331
+ function splitFlag(arg: string): [string, string | undefined] {
1332
+ const index = arg.indexOf("=");
1333
+ return index < 0 ? [arg, undefined] : [arg.slice(0, index), arg.slice(index + 1)];
1334
+ }
1335
+
1336
+ function readOptionValue(args: string[], index: number, flag: string): string {
1337
+ const value = args[index];
1338
+ if (value === undefined || value.startsWith("-")) throw new Error(`${flag} requires a value`);
1339
+ return value;
1340
+ }
1341
+
1342
+ function assignCliValue(
1343
+ option: RuntimeOperationCliOption,
1344
+ rawValue: unknown,
1345
+ input: Record<string, unknown>,
1346
+ hostOptions: Record<string, unknown>,
1347
+ ): void {
1348
+ const value = coerceCliValue(rawValue, option.type ?? "string", option.flag);
1349
+ const target = option.runtime === false ? hostOptions : input;
1350
+ target[option.name] = value;
1351
+ }
1352
+
1353
+ function assignPositional(
1354
+ positionals: Array<{ name: string; index: number }>,
1355
+ index: number,
1356
+ value: string,
1357
+ input: Record<string, unknown>,
1358
+ ): void {
1359
+ const positional = positionals.find((item) => item.index === index);
1360
+ if (!positional) throw new Error(`Unexpected positional argument ${value}`);
1361
+ input[positional.name] = value;
1362
+ }
1363
+
1364
+ function coerceCliValue(value: unknown, type: "string" | "boolean" | "number", flag: string): unknown {
1365
+ if (type === "boolean") {
1366
+ if (typeof value === "boolean") return value;
1367
+ if (value === "true") return true;
1368
+ if (value === "false") return false;
1369
+ throw new Error(`${flag} expects true or false`);
1370
+ }
1371
+ if (type === "number") {
1372
+ const number = Number(value);
1373
+ if (!Number.isFinite(number)) throw new Error(`${flag} expects a number`);
1374
+ return number;
1375
+ }
1376
+ return String(value);
1377
+ }
1378
+
1379
+ async function renderOperationResult(
1380
+ operation: RuntimeOperationDefinition,
1381
+ result: unknown,
1382
+ hostOptions: Record<string, unknown>,
1383
+ ): Promise<void> {
1384
+ if (isWorkflowPlan(result)) {
1385
+ printPlan(result);
1386
+ return;
1387
+ }
1388
+
1389
+ if (isRecord(result) && isWorkflowPlan(result.plan)) {
1390
+ if (result.dryRun === true) {
1391
+ printPlan(result.plan);
1392
+ console.log("No changes applied.");
1393
+ return;
1394
+ }
1395
+ console.log(`resolved ${result.plan.workflow}`);
1396
+ return;
1397
+ }
1398
+
1399
+ if (operation.createsWorkspace && isWorkspaceRecord(result)) {
1400
+ // Only emit the bareword name when stdout is piped, so scripts can do
1401
+ // `name=$(rig create)` while TTY users aren't shown a redundant line.
1402
+ if (!process.stdout.isTTY) console.log(result.name);
1403
+ return;
1404
+ }
1405
+
1406
+ if (isRecord(result) && typeof result.command === "string") {
1407
+ const commandResult = isRecord(result.commandResult) ? result.commandResult : undefined;
1408
+ if (!commandResult || hostOptions.print === true) {
1409
+ console.log(result.command);
1410
+ return;
1411
+ }
1412
+ const exitCode = typeof commandResult.exitCode === "number" ? commandResult.exitCode : 0;
1413
+ if (exitCode !== 0) throw new Error(`Host command failed with exit code ${exitCode}`);
1414
+ return;
1415
+ }
1416
+
1417
+ if (isWorkspaceRecord(result)) {
1418
+ if (!process.stdout.isTTY) console.log(result.name);
1419
+ return;
1420
+ }
1421
+
1422
+ printJson(result);
1423
+ }
1424
+
1425
+ async function runDoctor(invocation: CliInvocation, options: DoctorOptions): Promise<void> {
1426
+ if (options.cli) {
1427
+ const diagnostics = {
1428
+ cliVersion: RIGKIT_CLI_VERSION,
1429
+ binary: process.argv[1] ? resolve(process.argv[1]) : undefined,
1430
+ node: process.version,
1431
+ bun: typeof Bun !== "undefined" ? Bun.version : undefined,
1432
+ };
1433
+ if (wantsJson(invocation)) {
1434
+ printJson(diagnostics);
1435
+ return;
1436
+ }
1437
+ console.log(ui.kvList([
1438
+ ["cli", diagnostics.cliVersion],
1439
+ ["binary", diagnostics.binary ?? ""],
1440
+ ["node", diagnostics.node],
1441
+ ["bun", diagnostics.bun ?? ""],
1442
+ ]));
1443
+ return;
1444
+ }
1445
+
1446
+ const runtime = await loadRuntime(invocation);
1447
+ const [health, runtimeInfo, project] = await Promise.all([
1448
+ runtime.control.health(),
1449
+ runtime.control.runtime(),
1450
+ readRuntimeProject(runtime),
1451
+ ]);
1452
+ const diagnostics = {
1453
+ cliVersion: RIGKIT_CLI_VERSION,
1454
+ project,
1455
+ daemon: {
1456
+ url: runtime.handle.url,
1457
+ pid: runtime.handle.pid,
1458
+ handlePath: runtime.paths.handlePath,
1459
+ tokenPath: runtime.handle.tokenPath,
1460
+ expiresAt: health.expiresAt ?? runtime.handle.expiresAt,
1461
+ },
1462
+ runtime: runtimeInfo,
1463
+ };
1464
+
1465
+ if (wantsJson(invocation)) {
1466
+ printJson(diagnostics);
1467
+ return;
1468
+ }
1469
+
1470
+ console.log(ui.kvList([
1471
+ ["cli", RIGKIT_CLI_VERSION],
1472
+ ["project", project.projectDir],
1473
+ ["config", project.configPath],
1474
+ ["runtime handle", runtime.paths.handlePath],
1475
+ ["daemon", runtime.handle.url],
1476
+ ["daemon pid", String(runtime.handle.pid)],
1477
+ ["engine", runtimeInfo.engineVersion],
1478
+ ["runtime", runtimeInfo.runtimeVersion],
1479
+ ["api version", String(runtimeInfo.apiVersion)],
1480
+ ["protocol", runtimeInfo.protocolHash],
1481
+ ["state", project.statePath ?? ""],
1482
+ ["expires", health.expiresAt ?? runtime.handle.expiresAt ?? ""],
1483
+ ]));
1484
+ }
1485
+
1486
+ async function runVersion(invocation: CliInvocation): Promise<void> {
1487
+ if (wantsJson(invocation)) {
1488
+ printJson({ cliVersion: RIGKIT_CLI_VERSION });
1489
+ return;
1490
+ }
1491
+ console.log(RIGKIT_CLI_VERSION);
1492
+ }
1493
+
1494
+ async function runHelp(invocation: CliInvocation): Promise<void> {
1495
+ if (wantsJson(invocation)) {
1496
+ printJson({
1497
+ name: "rig",
1498
+ version: RIGKIT_CLI_VERSION,
1499
+ commands: [
1500
+ { name: "help", description: "Show Rigkit CLI help" },
1501
+ { name: "init", description: "Initialize a Rigkit project" },
1502
+ { name: "plan", description: "Plan project workflow changes" },
1503
+ { name: "apply", description: "Apply project workflow changes" },
1504
+ { name: "create", description: "Create a workspace" },
1505
+ { name: "rm", description: "Remove a workspace" },
1506
+ { name: "run", description: "Run a workspace operation" },
1507
+ { name: "ls", description: "List project workspaces" },
1508
+ { name: "cache", description: "Inspect and clear Rigkit cache" },
1509
+ { name: "projects", description: "Discover Rigkit projects below the current directory" },
1510
+ { name: "doctor", description: "Show Rigkit runtime diagnostics" },
1511
+ { name: "version", description: "Show Rigkit CLI version" },
1512
+ { name: "completion", description: "Generate shell completion script" },
1513
+ ],
1514
+ });
1515
+ return;
1516
+ }
1517
+ const cmd = (name: string, description: string): string =>
1518
+ ` ${ui.bold(name.padEnd(10))} ${description}`;
1519
+ const opt = (flag: string, description: string): string =>
1520
+ ` ${ui.bold(flag.padEnd(12))} ${description}`;
1521
+
1522
+ console.log([
1523
+ `${ui.bold("rig")} ${ui.dim(RIGKIT_CLI_VERSION)}`,
1524
+ "",
1525
+ ui.dim("Usage:"),
1526
+ ` ${ui.accent(ui.sym.prompt)} rig [global options] <command> [args]`,
1527
+ "",
1528
+ ui.dim("Commands:"),
1529
+ cmd("help", "Show Rigkit CLI help"),
1530
+ cmd("init", "Initialize a Rigkit project"),
1531
+ cmd("plan", "Plan project workflow changes"),
1532
+ cmd("apply", "Apply project workflow changes"),
1533
+ cmd("create", "Create a workspace"),
1534
+ cmd("rm", "Remove a workspace"),
1535
+ cmd("run", "Run a workspace operation"),
1536
+ cmd("ls", "List project workspaces"),
1537
+ cmd("cache", "Inspect and clear Rigkit cache"),
1538
+ cmd("projects", "Discover Rigkit projects below the current directory"),
1539
+ cmd("doctor", "Show Rigkit runtime diagnostics"),
1540
+ cmd("version", "Show Rigkit CLI version"),
1541
+ cmd("completion", "Generate shell completion script"),
1542
+ "",
1543
+ ui.dim("Options:"),
1544
+ opt("-chdir=DIR", "Switch to a directory containing rig.config.ts before running the command"),
1545
+ opt("-config=FILE", "Config file to load, relative to -chdir when set"),
1546
+ opt("-state=FILE", "Local runtime state database path"),
1547
+ opt("-json", "Print machine-readable JSON where supported"),
1548
+ ].join("\n"));
1549
+ }
1550
+
1551
+ async function loadRuntime(invocation: CliInvocation): Promise<RuntimeClient> {
1552
+ const engineOptions = resolveEngineOptions(invocation);
1553
+ return await getOrStartRuntime(engineOptions);
1554
+ }
1555
+
1556
+ async function readRuntimeProject(runtime: RuntimeClient): Promise<EngineProjectInfo> {
1557
+ return await runtime.control.project() as EngineProjectInfo;
1558
+ }
1559
+
1560
+ async function readRuntimeOperations(runtime: RuntimeClient): Promise<RuntimeOperationManifest> {
1561
+ return await runtime.control.operations() as unknown as RuntimeOperationManifest;
1562
+ }
1563
+
1564
+ async function runRuntimeOperation<T>(
1565
+ runtime: RuntimeClient,
1566
+ operation: string,
1567
+ input: Record<string, unknown>,
1568
+ options: { renderEvents: boolean },
1569
+ ): Promise<T> {
1570
+ const started = await runtime.control.startRun({ operation, input });
1571
+ let presenter: RunPresenter | undefined = options.renderEvents
1572
+ ? createRunPresenter(operation)
1573
+ : undefined;
1574
+ const logger: RunLogger | undefined = createRunLogger({
1575
+ projectDir: runtime.handle.projectDir,
1576
+ operation,
1577
+ daemonStderrPath: runtime.paths.runtimeLogPath,
1578
+ });
1579
+ if (logger) {
1580
+ logger.append({ type: "run.started", runId: started.runId, operation, input });
1581
+ }
1582
+ let result: T | undefined;
1583
+ let failure: Error | undefined;
1584
+ let failureCode: string | undefined;
1585
+ let activeNodePath: string | undefined;
1586
+
1587
+ const handleEvent = async (
1588
+ event: unknown,
1589
+ respond?: (id: string, response: unknown) => void | Promise<void>,
1590
+ sendSession?: (message: unknown) => void | Promise<void>,
1591
+ ) => {
1592
+ logger?.append(event);
1593
+ if (isRecord(event)) {
1594
+ if (event.type === "node.started" && typeof event.nodePath === "string") {
1595
+ activeNodePath = event.nodePath;
1596
+ }
1597
+ if (event.type === "node.completed" && event.nodePath === activeNodePath) {
1598
+ activeNodePath = undefined;
1599
+ }
1600
+ }
1601
+ if (isHostRequestEvent(event)) {
1602
+ const suspendPresenter = hostRequestNeedsTerminal(event);
1603
+ if (suspendPresenter) presenter?.pause();
1604
+ try {
1605
+ if (respond) {
1606
+ await answerHostRequestOverSession(respond, event, { quietOpen: Boolean(presenter) });
1607
+ } else {
1608
+ await answerHostRequest(runtime, event, { quietOpen: Boolean(presenter) });
1609
+ }
1610
+ } finally {
1611
+ if (suspendPresenter) presenter?.resume();
1612
+ }
1613
+ return;
1614
+ }
1615
+ if (isHostCapabilityRequestEvent(event)) {
1616
+ const suspendPresenter = hostCapabilityNeedsTerminal(event);
1617
+ const logger = createHostCapabilityLogger(event, presenter);
1618
+ if (suspendPresenter) presenter?.pause();
1619
+ try {
1620
+ if (sendSession) {
1621
+ await answerHostCapabilityRequestOverSession(sendSession, event, { logger });
1622
+ } else if (respond) {
1623
+ await answerHostCapabilityRequestOverSession((message) => {
1624
+ if (isRecord(message) && message.type === "response") {
1625
+ const id = typeof message.id === "string" ? message.id : undefined;
1626
+ if (id) return respond(id, "error" in message ? { error: message.error } : { result: message.result });
1627
+ }
1628
+ throw new Error(`Session response channel cannot send ${String(isRecord(message) ? message.type : typeof message)}`);
1629
+ }, event, { logger });
1630
+ } else {
1631
+ await answerHostCapabilityRequest(runtime, event, { logger });
1632
+ }
1633
+ } finally {
1634
+ if (suspendPresenter) presenter?.resume();
1635
+ }
1636
+ return;
1637
+ }
1638
+ if (isRecord(event) && event.type === "run.completed") {
1639
+ presenter?.render({ ...event, type: "run.completed" });
1640
+ result = event.result as T;
1641
+ return;
1642
+ }
1643
+ if (isRecord(event) && event.type === "run.failed") {
1644
+ const message = isRecord(event.error) && typeof event.error.message === "string"
1645
+ ? event.error.message
1646
+ : "Runtime operation failed";
1647
+ failure = new Error(message);
1648
+ failureCode = isRecord(event.error) && typeof event.error.code === "string"
1649
+ ? event.error.code
1650
+ : undefined;
1651
+ presenter?.render({ ...event, type: "run.failed" });
1652
+ return;
1653
+ }
1654
+ if (options.renderEvents && isDevMachineEvent(event)) {
1655
+ if (presenter) presenter.render(event);
1656
+ else renderEvent(event);
1657
+ }
1658
+ };
1659
+
1660
+ try {
1661
+ if (started.sessionUrl) {
1662
+ await runtime.runSession(started.runId, {
1663
+ hello: {
1664
+ type: "hello",
1665
+ transportVersion: 1,
1666
+ host: {
1667
+ name: "rigkit-cli",
1668
+ version: RIGKIT_CLI_VERSION,
1669
+ },
1670
+ hostMethods: CLI_HOST_METHODS,
1671
+ hostCapabilities: CLI_HOST_CAPABILITIES,
1672
+ },
1673
+ onOpen(session) {
1674
+ return installRunCancelHandler(session);
1675
+ },
1676
+ onClose() {
1677
+ uninstallRunCancelHandler();
1678
+ },
1679
+ async onMessage(message, session) {
1680
+ if (isRecord(message) && message.type === "hello.ack") return;
1681
+ if (isRecord(message) && message.type === "run.event") {
1682
+ await handleEvent(message.event);
1683
+ return;
1684
+ }
1685
+ await handleEvent(
1686
+ message,
1687
+ (id, response) => session.send({ type: "response", id, ...(response as object) }),
1688
+ (sessionMessage) => session.send(sessionMessage),
1689
+ );
1690
+ if (result !== undefined || failure) session.close();
1691
+ },
1692
+ });
1693
+ uninstallRunCancelHandler();
1694
+ } else {
1695
+ await runtime.runEvents(started.runId, handleEvent);
1696
+ }
1697
+ } finally {
1698
+ presenter?.close();
1699
+ if (logger) {
1700
+ logger.finish({
1701
+ status: failure ? "failed" : "completed",
1702
+ error: failure,
1703
+ result,
1704
+ });
1705
+ logger.close();
1706
+ }
1707
+ uninstallRunCancelHandler();
1708
+ }
1709
+
1710
+ if (failure) {
1711
+ printRunFailure({
1712
+ operation,
1713
+ node: activeNodePath,
1714
+ code: failureCode,
1715
+ message: failure.message,
1716
+ logPath: logger?.path,
1717
+ });
1718
+ throw new DisplayedCliError(failure.message);
1719
+ }
1720
+ if (result === undefined) throw new Error(`Runtime operation ${operation} finished without a result`);
1721
+ return result;
1722
+ }
1723
+
1724
+ function printRunFailure(input: {
1725
+ operation: string;
1726
+ node: string | undefined;
1727
+ code: string | undefined;
1728
+ message: string;
1729
+ logPath: string | undefined;
1730
+ }): void {
1731
+ const pairs: Array<[string, string]> = [];
1732
+ if (input.node) pairs.push(["node", input.node]);
1733
+ if (input.code) pairs.push(["code", ui.bold(input.code)]);
1734
+ pairs.push(["reason", input.message]);
1735
+
1736
+ process.stderr.write("\n");
1737
+ process.stderr.write(`${ui.err(ui.sym.err)} ${ui.bold(`${input.operation} failed`)}\n`);
1738
+ process.stderr.write(`${ui.kvList(pairs)}\n`);
1739
+ if (input.logPath) {
1740
+ process.stderr.write("\n");
1741
+ process.stderr.write(`${ui.dim("full log")} ${shortPath(input.logPath)}\n`);
1742
+ process.stderr.write(`${ui.dim(" ")} ${ui.dim("daemon stderr appended on failure")}\n`);
1743
+ }
1744
+ }
1745
+
1746
+ // After a successful `rig create`, list the workspace's available operations so
1747
+ // the user doesn't have to guess what to do next. TTY-only — JSON consumers and
1748
+ // pipes get clean output.
1749
+ async function printWorkspaceNextSteps(runtime: RuntimeClient, workspaceName: string): Promise<void> {
1750
+ if (!process.stderr.isTTY) return;
1751
+
1752
+ let manifest: RuntimeOperationManifest;
1753
+ try {
1754
+ manifest = await readRuntimeOperations(runtime);
1755
+ } catch {
1756
+ return;
1757
+ }
1758
+
1759
+ const ops = (manifest.workspaceOperations ?? []).filter((op) => op.id !== "remove");
1760
+ const invocations: Array<{ command: string; description: string }> = ops.map((op) => ({
1761
+ command: `rig run ${workspaceName} ${op.id}`,
1762
+ description: op.description ?? op.title ?? "",
1763
+ }));
1764
+ invocations.push({ command: `rig rm ${workspaceName}`, description: "Remove this workspace" });
1765
+
1766
+ const commandWidth = invocations.reduce((max, item) => Math.max(max, item.command.length), 0);
1767
+
1768
+ process.stderr.write("\n");
1769
+ process.stderr.write(`${ui.bold("Next")}\n`);
1770
+ for (const item of invocations) {
1771
+ const command = `${ui.bold("rig")}${item.command.slice("rig".length)}`;
1772
+ const padding = " ".repeat(Math.max(0, commandWidth - item.command.length));
1773
+ const tail = item.description ? ` ${ui.dim(item.description)}` : "";
1774
+ process.stderr.write(`${ui.dim(ui.sym.arrow)} ${command}${padding}${tail}\n`);
1775
+ }
1776
+ }
1777
+
1778
+ let uninstallActiveRunCancelHandler: (() => void) | undefined;
1779
+
1780
+ function installRunCancelHandler(session: { send(message: unknown): void; close(code?: number, reason?: string): void }): void {
1781
+ uninstallRunCancelHandler();
1782
+ let cancelRequested = false;
1783
+ const onSigint = () => {
1784
+ if (cancelRequested) {
1785
+ session.close(1000, "Run cancelled by host");
1786
+ return;
1787
+ }
1788
+ cancelRequested = true;
1789
+ session.send({ type: "run.cancel", reason: "user" });
1790
+ process.once("SIGINT", onSigint);
1791
+ };
1792
+ process.once("SIGINT", onSigint);
1793
+ uninstallActiveRunCancelHandler = () => {
1794
+ process.off("SIGINT", onSigint);
1795
+ uninstallActiveRunCancelHandler = undefined;
1796
+ };
1797
+ }
1798
+
1799
+ function uninstallRunCancelHandler(): void {
1800
+ uninstallActiveRunCancelHandler?.();
1801
+ }
1802
+
1803
+ function resolveEngineOptions(invocation: CliInvocation): { projectDir: string; configPath: string; statePath?: string } {
1804
+ const paths = resolveCommandConfigPaths(invocation);
1805
+ const options = invocation.global;
1806
+ return {
1807
+ projectDir: paths.projectDir,
1808
+ configPath: paths.configPath,
1809
+ statePath: options.state ? resolveGlobalPath(invocation, options.state) : undefined,
1810
+ };
1811
+ }
1812
+
1813
+ function resolveCommandConfigPaths(invocation: CliInvocation): { projectDir: string; configPath: string } {
1814
+ const options = invocation.global;
1815
+ return resolveConfigPaths({ chdir: options.chdir, config: options.config });
1816
+ }
1817
+
1818
+ function resolveGlobalPath(invocation: CliInvocation, path: string): string {
1819
+ return resolve(process.cwd(), invocation.global.chdir ?? ".", path);
1820
+ }
1821
+
1822
+ type HostRequestEvent = {
1823
+ type: "host.request";
1824
+ requestId?: string;
1825
+ id?: string;
1826
+ method: string;
1827
+ params: unknown;
1828
+ };
1829
+
1830
+ type HostCapabilityRequestEvent = {
1831
+ type: "host.capability.request";
1832
+ requestId?: string;
1833
+ id?: string;
1834
+ nodePath?: string;
1835
+ capability: string;
1836
+ params: unknown;
1837
+ };
1838
+
1839
+ type HostRequestHandlingOptions = {
1840
+ quietOpen?: boolean;
1841
+ };
1842
+
1843
+ type HostCapabilityLogOptions = {
1844
+ stream?: "stdout" | "stderr" | "info";
1845
+ label?: string;
1846
+ };
1847
+
1848
+ type HostCapabilityRequestHandlingOptions = {
1849
+ logger?: (data: string, options?: HostCapabilityLogOptions) => void;
1850
+ };
1851
+
1852
+ class UnsupportedHostCapabilityError extends Error {
1853
+ constructor(capability: string) {
1854
+ super(
1855
+ `Host capability "${capability}" is not registered in this Rigkit CLI host. ` +
1856
+ `Install or enable a local host capability handler to use it from this host.`,
1857
+ );
1858
+ this.name = "UnsupportedHostCapabilityError";
1859
+ }
1860
+ }
1861
+
1862
+ async function answerHostRequest(
1863
+ runtime: RuntimeClient,
1864
+ event: HostRequestEvent,
1865
+ options: HostRequestHandlingOptions = {},
1866
+ ): Promise<void> {
1867
+ if (!event.requestId) throw new Error(`Host request is missing requestId`);
1868
+ try {
1869
+ const result = await handleHostRequest(event.method, event.params, options);
1870
+ await runtime.control.hostResponse(event.requestId, { result });
1871
+ } catch (error) {
1872
+ await runtime.control.hostResponse(event.requestId, {
1873
+ error: {
1874
+ message: error instanceof Error ? error.message : String(error),
1875
+ },
1876
+ });
1877
+ }
1878
+ }
1879
+
1880
+ async function answerHostRequestOverSession(
1881
+ respond: (id: string, response: unknown) => void | Promise<void>,
1882
+ event: HostRequestEvent,
1883
+ options: HostRequestHandlingOptions = {},
1884
+ ): Promise<void> {
1885
+ const id = event.id ?? event.requestId;
1886
+ if (!id) throw new Error(`Host request is missing id`);
1887
+ try {
1888
+ const result = await handleHostRequest(event.method, event.params, options);
1889
+ await respond(id, { result });
1890
+ } catch (error) {
1891
+ await respond(id, {
1892
+ error: {
1893
+ message: error instanceof Error ? error.message : String(error),
1894
+ },
1895
+ });
1896
+ }
1897
+ }
1898
+
1899
+ async function answerHostCapabilityRequest(
1900
+ runtime: RuntimeClient,
1901
+ event: HostCapabilityRequestEvent,
1902
+ options: HostCapabilityRequestHandlingOptions = {},
1903
+ ): Promise<void> {
1904
+ const requestId = event.requestId ?? event.id;
1905
+ if (!requestId) throw new Error(`Host capability request is missing requestId`);
1906
+ try {
1907
+ const handled = await handleHostCapabilityRequest(event.capability, event.params, options);
1908
+ await runtime.control.hostResponse(requestId, { result: handled.result });
1909
+ } catch (error) {
1910
+ await runtime.control.hostResponse(requestId, {
1911
+ error: hostCapabilityError(error),
1912
+ });
1913
+ }
1914
+ }
1915
+
1916
+ async function answerHostCapabilityRequestOverSession(
1917
+ send: (message: unknown) => void | Promise<void>,
1918
+ event: HostCapabilityRequestEvent,
1919
+ options: HostCapabilityRequestHandlingOptions = {},
1920
+ ): Promise<void> {
1921
+ const id = event.id ?? event.requestId;
1922
+ if (!id) throw new Error(`Host capability request is missing id`);
1923
+ try {
1924
+ const handled = await handleHostCapabilityRequest(event.capability, event.params, options);
1925
+ await send({ type: "response", id, result: handled.result });
1926
+ if (handled.closed) reportHostCapabilityClosed(send, id, handled.closed);
1927
+ } catch (error) {
1928
+ await send({
1929
+ type: "response",
1930
+ id,
1931
+ error: hostCapabilityError(error),
1932
+ });
1933
+ }
1934
+ }
1935
+
1936
+ async function handleHostRequest(
1937
+ method: string,
1938
+ params: unknown,
1939
+ options: HostRequestHandlingOptions = {},
1940
+ ): Promise<unknown> {
1941
+ switch (method) {
1942
+ case "message.show":
1943
+ return showHostMessage(params);
1944
+ case "prompt.text":
1945
+ return await promptHostText(params);
1946
+ case "prompt.confirm":
1947
+ return await promptHostConfirm(params);
1948
+ case "prompt.select":
1949
+ return await promptHostSelect(params);
1950
+ case "open.external":
1951
+ return openHostExternal(params, options);
1952
+ case "host.command.run":
1953
+ return await runHostCommand(params);
1954
+ default:
1955
+ throw new Error(`Unsupported host method ${method}`);
1956
+ }
1957
+ }
1958
+
1959
+ function hostRequestNeedsTerminal(event: HostRequestEvent): boolean {
1960
+ switch (event.method) {
1961
+ case "open.external":
1962
+ return false;
1963
+ case "host.command.run":
1964
+ return !isTrustedCaptureHostCommand(event.params);
1965
+ default:
1966
+ return true;
1967
+ }
1968
+ }
1969
+
1970
+ function hostCapabilityNeedsTerminal(event: HostCapabilityRequestEvent): boolean {
1971
+ switch (event.capability) {
1972
+ case "cmux.open":
1973
+ return false;
1974
+ default:
1975
+ return true;
1976
+ }
1977
+ }
1978
+
1979
+ function createHostCapabilityLogger(
1980
+ event: HostCapabilityRequestEvent,
1981
+ presenter: RunPresenter | undefined,
1982
+ ): (data: string, options?: HostCapabilityLogOptions) => void {
1983
+ return (data, options = {}) => {
1984
+ if (presenter) {
1985
+ presenter.render({
1986
+ type: "log.output",
1987
+ nodePath: event.nodePath ?? "runtime",
1988
+ stream: options.stream ?? "info",
1989
+ label: options.label ?? event.capability,
1990
+ data,
1991
+ });
1992
+ return;
1993
+ }
1994
+ console.error(data);
1995
+ };
1996
+ }
1997
+
1998
+ function isTrustedCaptureHostCommand(params: unknown): boolean {
1999
+ return process.env.RIGKIT_TRUST_HOST_COMMANDS === "1" &&
2000
+ isRecord(params) &&
2001
+ params.mode !== "interactive";
2002
+ }
2003
+
2004
+ type HandledHostCapability = {
2005
+ result: unknown;
2006
+ closed?: Promise<void>;
2007
+ };
2008
+
2009
+ async function handleHostCapabilityRequest(
2010
+ capability: string,
2011
+ params: unknown,
2012
+ options: HostCapabilityRequestHandlingOptions = {},
2013
+ ): Promise<HandledHostCapability> {
2014
+ const handler = CLI_HOST_CAPABILITY_HANDLERS.get(capability);
2015
+ if (!handler) {
2016
+ throw new UnsupportedHostCapabilityError(capability);
2017
+ }
2018
+ return normalizeHostCapabilityResult(await handler.handle(params, {
2019
+ log: (data, logOptions) => options.logger?.(data, logOptions),
2020
+ }));
2021
+ }
2022
+
2023
+ function normalizeHostCapabilityResult(value: unknown): HandledHostCapability {
2024
+ if (isRecord(value) && isPromiseLike(value.closed)) {
2025
+ const { closed, ...result } = value as Record<string, unknown> & { closed: PromiseLike<unknown> };
2026
+ return {
2027
+ result,
2028
+ closed: Promise.resolve(closed).then(() => undefined),
2029
+ };
2030
+ }
2031
+ return { result: value };
2032
+ }
2033
+
2034
+ function reportHostCapabilityClosed(
2035
+ send: (message: unknown) => void | Promise<void>,
2036
+ id: string,
2037
+ closed: Promise<void>,
2038
+ ): void {
2039
+ void closed.then(
2040
+ () => send({ type: "host.capability.closed", id }),
2041
+ (error) => send({ type: "host.capability.closed", id, error: hostCapabilityError(error) }),
2042
+ ).catch(() => {});
2043
+ }
2044
+
2045
+ function hostCapabilityError(error: unknown): { code: string; message: string } {
2046
+ return {
2047
+ code: error instanceof UnsupportedHostCapabilityError ? "UNSUPPORTED_CAPABILITY" : "HOST_CAPABILITY_FAILED",
2048
+ message: error instanceof Error ? error.message : String(error),
2049
+ };
2050
+ }
2051
+
2052
+ function showHostMessage(params: unknown): null {
2053
+ const message = stringField(params, "message") ?? "";
2054
+ const level = stringField(params, "level") ?? "info";
2055
+ console.error(`${level}: ${message}`);
2056
+ return null;
2057
+ }
2058
+
2059
+ async function promptHostText(params: unknown): Promise<string> {
2060
+ const message = stringField(params, "message") ?? "Enter value";
2061
+ const defaultValue = stringField(params, "defaultValue");
2062
+ if (!canPrompt()) {
2063
+ if (defaultValue !== undefined) return defaultValue;
2064
+ throw new Error(`Host prompt requires an interactive terminal: ${message}`);
2065
+ }
2066
+ const answers = await inquirer.prompt<{ value: string }>([{
2067
+ type: "input",
2068
+ name: "value",
2069
+ message,
2070
+ default: defaultValue,
2071
+ }]);
2072
+ return answers.value || defaultValue || "";
2073
+ }
2074
+
2075
+ async function promptHostConfirm(params: unknown): Promise<boolean> {
2076
+ const message = stringField(params, "message") ?? "Continue?";
2077
+ const defaultValue = booleanField(params, "defaultValue") ?? false;
2078
+ if (!canPrompt()) return defaultValue;
2079
+ const answers = await inquirer.prompt<{ value: boolean }>([{
2080
+ type: "confirm",
2081
+ name: "value",
2082
+ message,
2083
+ default: defaultValue,
2084
+ }]);
2085
+ return answers.value;
2086
+ }
2087
+
2088
+ async function promptHostSelect(params: unknown): Promise<string> {
2089
+ const message = stringField(params, "message") ?? "Choose";
2090
+ const options = isRecord(params) && Array.isArray(params.options)
2091
+ ? params.options
2092
+ .filter(isRecord)
2093
+ .map((item) => ({
2094
+ value: typeof item.value === "string" ? item.value : "",
2095
+ label: typeof item.label === "string" ? item.label : typeof item.value === "string" ? item.value : "",
2096
+ hint: typeof item.description === "string" ? item.description : undefined,
2097
+ }))
2098
+ .filter((item) => item.value)
2099
+ : [];
2100
+ if (options.length === 0) throw new Error(`Host select prompt has no options`);
2101
+ const defaultValue = stringField(params, "defaultValue") ?? options[0]!.value;
2102
+ if (!canPrompt()) return defaultValue;
2103
+ const answers = await inquirer.prompt<{ value: string }>([{
2104
+ type: "select",
2105
+ name: "value",
2106
+ message,
2107
+ default: defaultValue,
2108
+ choices: options.map((option) => ({
2109
+ name: option.label,
2110
+ value: option.value,
2111
+ description: option.hint,
2112
+ })),
2113
+ }]);
2114
+ return answers.value;
2115
+ }
2116
+
2117
+ function openHostExternal(params: unknown, options: HostRequestHandlingOptions = {}): null {
2118
+ const target = stringField(params, "target");
2119
+ if (!target) throw new Error(`open.external requires target`);
2120
+ if (!options.quietOpen) console.error(`open ${target}`);
2121
+ openExternalTarget(target);
2122
+ return null;
2123
+ }
2124
+
2125
+ async function runHostCommand(params: unknown): Promise<{ exitCode: number; stdout: string | null; stderr: string | null }> {
2126
+ if (!isRecord(params) || !Array.isArray(params.argv) || params.argv.some((item) => typeof item !== "string")) {
2127
+ throw new Error(`host.command.run requires argv`);
2128
+ }
2129
+ const argv = params.argv as string[];
2130
+ if (argv.length === 0) throw new Error(`host.command.run argv must not be empty`);
2131
+ const mode = params.mode === "interactive" ? "interactive" : "capture";
2132
+ const cwd = stringField(params, "cwd");
2133
+ const reason = stringField(params, "reason");
2134
+ const env = isRecord(params.env)
2135
+ ? Object.fromEntries(Object.entries(params.env).filter(([, value]) => value === undefined || typeof value === "string")) as Record<string, string | undefined>
2136
+ : undefined;
2137
+ const stdin = params.stdin === null || typeof params.stdin === "string" ? params.stdin : undefined;
2138
+
2139
+ if (process.env.RIGKIT_TRUST_HOST_COMMANDS !== "1") {
2140
+ const allowed = await confirmHostCommand({ argv, cwd, env, mode, reason });
2141
+ if (!allowed) throw new Error(`Host command denied`);
2142
+ }
2143
+
2144
+ if (mode === "interactive") {
2145
+ const proc = Bun.spawn(argv, {
2146
+ cwd,
2147
+ env: env ? { ...process.env, ...env } : process.env,
2148
+ stdin: stdin === undefined || stdin === null ? "inherit" : "pipe",
2149
+ stdout: "inherit",
2150
+ stderr: "inherit",
2151
+ });
2152
+ if (stdin !== undefined && stdin !== null) {
2153
+ const writer = proc.stdin;
2154
+ if (!writer) throw new Error(`Host command stdin is unavailable`);
2155
+ writer.write(stdin);
2156
+ writer.end();
2157
+ }
2158
+ return { exitCode: await proc.exited, stdout: null, stderr: null };
2159
+ }
2160
+
2161
+ const proc = Bun.spawn(argv, {
2162
+ cwd,
2163
+ env: env ? { ...process.env, ...env } : process.env,
2164
+ stdin: stdin === undefined || stdin === null ? "ignore" : "pipe",
2165
+ stdout: "pipe",
2166
+ stderr: "pipe",
2167
+ });
2168
+ if (stdin !== undefined && stdin !== null) {
2169
+ const writer = proc.stdin;
2170
+ if (!writer) throw new Error(`Host command stdin is unavailable`);
2171
+ writer.write(stdin);
2172
+ writer.end();
2173
+ }
2174
+ const [exitCode, stdout, stderr] = await Promise.all([
2175
+ proc.exited,
2176
+ new Response(proc.stdout).text(),
2177
+ new Response(proc.stderr).text(),
2178
+ ]);
2179
+ return { exitCode, stdout, stderr };
2180
+ }
2181
+
2182
+ async function confirmHostCommand(input: {
2183
+ argv: string[];
2184
+ cwd?: string;
2185
+ env?: Record<string, string | undefined>;
2186
+ mode: "capture" | "interactive";
2187
+ reason?: string;
2188
+ }): Promise<boolean> {
2189
+ if (!canPrompt()) {
2190
+ throw new Error(`Host command requires confirmation in an interactive terminal`);
2191
+ }
2192
+
2193
+ const pairs: Array<[string, string]> = [
2194
+ ["command", input.argv.map(shellDisplay).join(" ")],
2195
+ ["mode", input.mode],
2196
+ ];
2197
+ if (input.cwd) pairs.push(["cwd", input.cwd]);
2198
+ if (input.env && Object.keys(input.env).length > 0) {
2199
+ pairs.push(["env", Object.keys(input.env).join(", ")]);
2200
+ }
2201
+ if (input.reason) pairs.push(["reason", input.reason]);
2202
+
2203
+ console.error("");
2204
+ console.error(`${ui.warn("!")} ${ui.bold("this config wants to run a command on your machine")}`);
2205
+ console.error("");
2206
+ console.error(ui.kvList(pairs));
2207
+ console.error("");
2208
+ return await promptHostConfirm({ message: "Allow?", defaultValue: false });
2209
+ }
2210
+
2211
+ function isHostRequestEvent(value: unknown): value is HostRequestEvent {
2212
+ return isRecord(value) &&
2213
+ value.type === "host.request" &&
2214
+ (typeof value.requestId === "string" || typeof value.id === "string") &&
2215
+ typeof value.method === "string";
2216
+ }
2217
+
2218
+ function isHostCapabilityRequestEvent(value: unknown): value is HostCapabilityRequestEvent {
2219
+ return isRecord(value) &&
2220
+ value.type === "host.capability.request" &&
2221
+ (typeof value.requestId === "string" || typeof value.id === "string") &&
2222
+ (value.nodePath === undefined || typeof value.nodePath === "string") &&
2223
+ typeof value.capability === "string";
2224
+ }
2225
+
2226
+ function isWorkflowPlan(value: unknown): value is WorkflowPlan {
2227
+ return isRecord(value) &&
2228
+ typeof value.workflow === "string" &&
2229
+ typeof value.cachedNodeCount === "number" &&
2230
+ typeof value.nodeCount === "number" &&
2231
+ Array.isArray(value.nodes);
2232
+ }
2233
+
2234
+ function isWorkspaceRecord(value: unknown): value is WorkspaceRecord {
2235
+ return isRecord(value) &&
2236
+ typeof value.name === "string" &&
2237
+ typeof value.workflow === "string" &&
2238
+ isRecord(value.ctx);
2239
+ }
2240
+
2241
+ function isDevMachineEvent(value: unknown): value is DevMachineEvent {
2242
+ return isRecord(value) &&
2243
+ typeof value.type === "string" &&
2244
+ !value.type.startsWith("run.") &&
2245
+ value.type !== "host.request" &&
2246
+ value.type !== "host.capability.request";
2247
+ }
2248
+
2249
+ function stringField(value: unknown, key: string): string | undefined {
2250
+ return isRecord(value) && typeof value[key] === "string" ? value[key] : undefined;
2251
+ }
2252
+
2253
+ function booleanField(value: unknown, key: string): boolean | undefined {
2254
+ return isRecord(value) && typeof value[key] === "boolean" ? value[key] : undefined;
2255
+ }
2256
+
2257
+ function isRecord(value: unknown): value is Record<string, unknown> {
2258
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2259
+ }
2260
+
2261
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
2262
+ return Boolean(value && typeof value === "object" && "then" in value && typeof value.then === "function");
2263
+ }
2264
+
2265
+ function shellDisplay(value: string): string {
2266
+ return /^[A-Za-z0-9_./:=@+-]+$/.test(value) ? value : JSON.stringify(value);
2267
+ }
2268
+
2269
+ function wantsJson(invocation: CliInvocation): boolean {
2270
+ return invocation.json;
2271
+ }
2272
+
2273
+ function printJson(value: unknown): void {
2274
+ console.log(JSON.stringify(value, null, 2));
2275
+ }
2276
+
2277
+ function printInteractiveOutputGap(invocation: CliInvocation): void {
2278
+ if (wantsJson(invocation) || !process.stdout.isTTY) return;
2279
+ console.log("");
2280
+ }
2281
+
2282
+ function printPlan(plan: WorkflowPlan): void {
2283
+ console.log(`${ui.bold(plan.workflow)} ${ui.dim(`${plan.cachedNodeCount}/${plan.nodeCount} cached`)}`);
2284
+ console.log("");
2285
+
2286
+ const rows = plan.nodes.map((node) => [
2287
+ { text: String(node.index + 1), style: ui.dim },
2288
+ { text: node.status, style: planStatusStyle(node.status) },
2289
+ { text: node.path },
2290
+ { text: node.reason ?? "", style: ui.dim },
2291
+ ]);
2292
+ console.log(ui.columns(["#", "status", "node", "reason"], rows));
2293
+ }
2294
+
2295
+ function planStatusStyle(status: string): (text: string) => string {
2296
+ switch (status) {
2297
+ case "cached":
2298
+ case "skipped":
2299
+ return ui.dim;
2300
+ case "pending":
2301
+ return ui.warn;
2302
+ case "completed":
2303
+ case "ready":
2304
+ case "applied":
2305
+ return ui.ok;
2306
+ case "failed":
2307
+ case "error":
2308
+ return ui.err;
2309
+ default:
2310
+ return ui.accent;
2311
+ }
2312
+ }
2313
+
2314
+ function printWorkspaces(
2315
+ workspaces: ReadonlyArray<Pick<WorkspaceRecord, "name" | "workflow" | "createdAt">>,
2316
+ ): void {
2317
+ if (workspaces.length === 0) {
2318
+ console.log(ui.dim("no workspaces"));
2319
+ return;
2320
+ }
2321
+
2322
+ const rows = workspaces.map((workspace) => [
2323
+ { text: workspace.name, style: ui.bold },
2324
+ { text: workspace.workflow },
2325
+ { text: workspace.createdAt, style: ui.dim },
2326
+ formatWorkspaceAge(workspace.createdAt),
2327
+ ]);
2328
+ console.log(ui.columns(["name", "workflow", "created", "age"], rows));
2329
+ }
2330
+
2331
+ function formatWorkspaceAge(createdAt: string): { text: string; style: (text: string) => string } {
2332
+ const createdTime = Date.parse(createdAt);
2333
+ if (Number.isNaN(createdTime)) return { text: "unknown", style: ui.dim };
2334
+
2335
+ const ageMs = Math.max(0, Date.now() - createdTime);
2336
+ const minute = 60 * 1000;
2337
+ const hour = 60 * minute;
2338
+ const day = 24 * hour;
2339
+ const text = ageMs < hour
2340
+ ? `${Math.max(1, Math.floor(ageMs / minute))}m`
2341
+ : ageMs < day
2342
+ ? `${Math.floor(ageMs / hour)}h`
2343
+ : `${Math.floor(ageMs / day)}d`;
2344
+
2345
+ if (ageMs < day) return { text, style: ui.ok };
2346
+ if (ageMs <= 3 * day) return { text, style: ui.warn };
2347
+ return { text, style: ui.dim };
2348
+ }
2349
+
2350
+ function printSnapshots(snapshots: SnapshotRecord[]): void {
2351
+ if (snapshots.length === 0) {
2352
+ console.log(ui.dim("no snapshots"));
2353
+ return;
2354
+ }
2355
+
2356
+ const rows = snapshots.map((snapshot) => [
2357
+ { text: snapshot.id, style: ui.dim },
2358
+ { text: snapshot.workflow },
2359
+ { text: snapshot.nodePath, style: ui.bold },
2360
+ { text: typeof snapshot.metadata.snapshotId === "string" ? snapshot.metadata.snapshotId : "" },
2361
+ { text: snapshot.createdAt, style: ui.dim },
2362
+ ]);
2363
+ console.log(ui.columns(["run", "workflow", "node", "snapshot", "created"], rows));
2364
+ }
2365
+
2366
+ function printCacheEntries(entries: ReadonlyArray<{
2367
+ scope: "local" | "global";
2368
+ workflow: string;
2369
+ nodePath: string;
2370
+ nodeName: string;
2371
+ createdAt: string;
2372
+ invalidated: boolean;
2373
+ fragmentHash?: string;
2374
+ }>): void {
2375
+ if (entries.length === 0) {
2376
+ console.log(ui.dim("no cache entries"));
2377
+ return;
2378
+ }
2379
+
2380
+ const rows = entries.map((entry) => [
2381
+ { text: entry.scope, style: ui.dim },
2382
+ { text: entry.workflow },
2383
+ { text: entry.nodePath || entry.nodeName, style: ui.bold },
2384
+ {
2385
+ text: entry.invalidated ? "invalidated" : "valid",
2386
+ style: entry.invalidated ? ui.warn : ui.ok,
2387
+ },
2388
+ { text: entry.fragmentHash ? entry.fragmentHash.slice(0, 19) : "", style: ui.dim },
2389
+ { text: entry.createdAt, style: ui.dim },
2390
+ ]);
2391
+ console.log(ui.columns(["scope", "workflow", "node", "status", "fragment", "created"], rows));
2392
+ }
2393
+
2394
+ function printConfig(info: EngineProjectInfo): void {
2395
+ console.log(ui.kvList([
2396
+ ["config", info.configPath],
2397
+ ["project", info.projectDir],
2398
+ ["state", info.statePath ?? ""],
2399
+ ["workflow", info.workflow?.name ?? ui.dim("(not loaded)")],
2400
+ ["providers", info.workflow?.providers.join(", ") ?? ""],
2401
+ ]));
2402
+ }
2403
+
2404
+ function normalizeListTarget(target: string | undefined): "workspaces" | "snapshots" | "config" {
2405
+ if (!target || target === "workspaces" || target === "workspace" || target === "vms" || target === "vm") {
2406
+ return "workspaces";
2407
+ }
2408
+ if (target === "snapshots" || target === "snapshot") return "snapshots";
2409
+ if (target === "config" || target === "machine" || target === "machines") return "config";
2410
+ throw new Error(`Unknown ls target ${target}. Expected workspaces, snapshots, or config.`);
2411
+ }
2412
+
2413
+ function renderEvent(event: DevMachineEvent): void {
2414
+ const write = (line: string) => process.stderr.write(`${line}\n`);
2415
+ switch (event.type) {
2416
+ case "definition.loaded":
2417
+ write(`${ui.dim(ui.sym.dot)} ${ui.dim(`loaded ${event.workflow}`)}`);
2418
+ return;
2419
+ case "plan.created":
2420
+ write(`${ui.accent(ui.sym.active)} ${ui.bold(event.workflow)} ${ui.dim(`${event.cachedNodeCount}/${event.nodeCount} cached`)}`);
2421
+ return;
2422
+ case "workflow.apply.started":
2423
+ write(`${ui.accent(ui.sym.active)} workflow ${ui.bold(event.workflow)}`);
2424
+ return;
2425
+ case "workflow.apply.completed": {
2426
+ const summary = event.nodeCount > 0
2427
+ ? ` ${ui.dim(`${event.cachedNodeCount}/${event.nodeCount} cached`)}`
2428
+ : "";
2429
+ write(`${ui.ok(ui.sym.ok)} ${ui.bold(event.workflow)} ${ui.dim("prepared")}${summary}`);
2430
+ return;
2431
+ }
2432
+ case "node.cached":
2433
+ write(` ${ui.dim(ui.sym.ok)} ${ui.dim(`${event.nodePath} cached`)}`);
2434
+ return;
2435
+ case "vm.created":
2436
+ write(` ${ui.dim(event.fromSnapshotId ? `vm ${event.vmId} from ${event.fromSnapshotId}` : `vm ${event.vmId} created`)}`);
2437
+ return;
2438
+ case "node.started":
2439
+ write(` ${ui.accent(ui.sym.active)} ${ui.bold(String(event.nodePath))}`);
2440
+ return;
2441
+ case "node.completed":
2442
+ write(` ${ui.ok(ui.sym.ok)} ${event.nodePath}`);
2443
+ return;
2444
+ case "command.started":
2445
+ write(` ${ui.dim(`$ ${event.commandName}`)}`);
2446
+ return;
2447
+ case "command.output":
2448
+ process.stderr.write(event.data);
2449
+ return;
2450
+ case "command.completed":
2451
+ if (event.exitCode !== 0) {
2452
+ write(` ${ui.err(`${event.commandName} exited ${event.exitCode}`)}`);
2453
+ }
2454
+ return;
2455
+ case "log.output": {
2456
+ const prefix = event.stream && event.stream !== "info" && event.stream !== "stdout"
2457
+ ? `[${event.stream}] `
2458
+ : "";
2459
+ for (const line of event.data.replace(/\r/g, "").split("\n")) {
2460
+ if (!line) continue;
2461
+ process.stderr.write(`${prefix}${line}\n`);
2462
+ }
2463
+ return;
2464
+ }
2465
+ case "interaction.awaiting_user":
2466
+ write(` ${ui.accent(ui.sym.arrow)} waiting on ${ui.bold(event.label)}`);
2467
+ write(` ${ui.dim(event.url)}`);
2468
+ return;
2469
+ case "interaction.completed":
2470
+ write(` ${ui.ok(ui.sym.ok)} ${ui.dim(`${event.label} completed`)}`);
2471
+ return;
2472
+ case "artifact.created":
2473
+ write(` ${ui.dim(`+ ${event.providerId}:${event.kind}`)}`);
2474
+ return;
2475
+ case "workspace.create.started":
2476
+ write(`${ui.accent(ui.sym.active)} creating workspace ${ui.bold(String(event.workspaceName))}`);
2477
+ return;
2478
+ case "workspace.ready":
2479
+ write(`${ui.ok(ui.sym.ok)} ${ui.bold(String(event.workspaceId))} ${ui.dim("ready")}`);
2480
+ return;
2481
+ case "workspace.remove.started":
2482
+ write(`${ui.accent(ui.sym.active)} removing workspace ${ui.bold(String(event.workspaceName))}`);
2483
+ return;
2484
+ case "workspace.remove.completed":
2485
+ write(`${ui.ok(ui.sym.ok)} removed ${ui.bold(String(event.workspaceName))}`);
2486
+ return;
2487
+ case "workspace.operation.started":
2488
+ write(`${ui.accent(ui.sym.active)} running ${ui.bold(String(event.operationId))} on ${ui.bold(String(event.workspaceName))}`);
2489
+ return;
2490
+ case "workspace.operation.completed":
2491
+ write(`${ui.ok(ui.sym.ok)} ran ${ui.bold(String(event.operationId))} on ${ui.bold(String(event.workspaceName))}`);
2492
+ return;
2493
+ default:
2494
+ return;
2495
+ }
2496
+ }