@rigkit/cli 0.2.8 → 0.2.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigkit/cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,9 +22,9 @@
22
22
  "chalk": "^5.6.2",
23
23
  "commander": "^14.0.3",
24
24
  "inquirer": "^13.4.3",
25
- "@rigkit/provider-cmux": "0.2.8",
26
- "@rigkit/runtime-client": "0.2.8",
27
- "@rigkit/engine": "0.2.8"
25
+ "@rigkit/provider-cmux": "0.2.10",
26
+ "@rigkit/engine": "0.2.10",
27
+ "@rigkit/runtime-client": "0.2.10"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/bun": "latest",
package/src/cli.test.ts CHANGED
@@ -43,6 +43,76 @@ describe("CLI entrypoint", () => {
43
43
  expect(help.stdout).toContain("cache Inspect and clear Rigkit cache");
44
44
  });
45
45
 
46
+ test("prints an update notice when latest metadata is newer", async () => {
47
+ const rigkitHome = mkdtempSync(join(tmpdir(), "rigkit-cli-update-"));
48
+ const latestVersion = nextPatchVersion(RIGKIT_CLI_VERSION);
49
+ const server = Bun.serve({
50
+ hostname: "127.0.0.1",
51
+ port: 0,
52
+ fetch() {
53
+ return Response.json({
54
+ version: latestVersion,
55
+ installerUrl: "https://www.rigkit.dev/install",
56
+ });
57
+ },
58
+ });
59
+
60
+ try {
61
+ const result = await runCli(["doctor", "--cli"], {
62
+ env: {
63
+ RIGKIT_HOME: rigkitHome,
64
+ RIGKIT_UPDATE_CHECK: "1",
65
+ RIGKIT_UPDATE_TIMEOUT_MS: "2000",
66
+ RIGKIT_UPDATE_URL: `http://127.0.0.1:${server.port}/latest.json`,
67
+ },
68
+ });
69
+
70
+ expect(result.exitCode).toBe(0);
71
+ expect(result.stdout).toContain(RIGKIT_CLI_VERSION);
72
+ expect(result.stderr).toContain(`rig ${latestVersion} is available`);
73
+ expect(result.stderr).toContain("update with: curl -fsSL https://www.rigkit.dev/install | sh");
74
+ } finally {
75
+ server.stop(true);
76
+ rmSync(rigkitHome, { recursive: true, force: true });
77
+ }
78
+ });
79
+
80
+ test("does not print update notices for JSON output", async () => {
81
+ const rigkitHome = mkdtempSync(join(tmpdir(), "rigkit-cli-update-json-"));
82
+ let requests = 0;
83
+ const server = Bun.serve({
84
+ hostname: "127.0.0.1",
85
+ port: 0,
86
+ fetch() {
87
+ requests += 1;
88
+ return Response.json({
89
+ version: nextPatchVersion(RIGKIT_CLI_VERSION),
90
+ installerUrl: "https://www.rigkit.dev/install",
91
+ });
92
+ },
93
+ });
94
+
95
+ try {
96
+ const result = await runCli(["doctor", "--cli", "--json"], {
97
+ env: {
98
+ RIGKIT_HOME: rigkitHome,
99
+ RIGKIT_UPDATE_CHECK: "1",
100
+ RIGKIT_UPDATE_URL: `http://127.0.0.1:${server.port}/latest.json`,
101
+ },
102
+ });
103
+
104
+ expect(result.exitCode).toBe(0);
105
+ expect(result.stderr).toBe("");
106
+ expect(JSON.parse(result.stdout)).toMatchObject({
107
+ cliVersion: RIGKIT_CLI_VERSION,
108
+ });
109
+ expect(requests).toBe(0);
110
+ } finally {
111
+ server.stop(true);
112
+ rmSync(rigkitHome, { recursive: true, force: true });
113
+ }
114
+ });
115
+
46
116
  test("rejects operation shorthand at the root", async () => {
47
117
  const result = await runCli(["unknown"]);
48
118
 
@@ -124,6 +194,31 @@ describe("CLI entrypoint", () => {
124
194
  }
125
195
  });
126
196
 
197
+ test("does not render a success marker when cache invalidation is a no-op", async () => {
198
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-cli-cache-invalidate-"));
199
+
200
+ await withWorkspaceRuntime({ projectDir, cacheInvalidated: 0 }, async ({ env }) => {
201
+ const result = await runCli([`-chdir=${projectDir}`, "cache", "invalidate", "missing-task"], { env });
202
+
203
+ expect(result.exitCode).toBe(0);
204
+ expect(result.stderr).toBe("");
205
+ expect(result.stdout.trim()).toBe("no cache entries invalidated");
206
+ expect(result.stdout).not.toContain("✓");
207
+ });
208
+ });
209
+
210
+ test("preserves JSON output for zero cache invalidations", async () => {
211
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-cli-cache-invalidate-json-"));
212
+
213
+ await withWorkspaceRuntime({ projectDir, cacheInvalidated: 0 }, async ({ env }) => {
214
+ const result = await runCli([`-chdir=${projectDir}`, "cache", "invalidate", "missing-task", "--json"], { env });
215
+
216
+ expect(result.exitCode).toBe(0);
217
+ expect(result.stderr).toBe("");
218
+ expect(JSON.parse(result.stdout)).toEqual({ ok: true, invalidated: 0 });
219
+ });
220
+ });
221
+
127
222
  test("lists workspaces from the project runtime", async () => {
128
223
  const projectDir = mkdtempSync(join(tmpdir(), "rigkit-cli-ls-"));
129
224
 
@@ -241,6 +336,7 @@ async function runCli(
241
336
  stderr: "pipe",
242
337
  env: {
243
338
  ...process.env,
339
+ RIGKIT_UPDATE_CHECK: "0",
244
340
  ...options.env,
245
341
  FORCE_COLOR: "0",
246
342
  NO_COLOR: "1",
@@ -255,8 +351,14 @@ async function runCli(
255
351
  return { exitCode, stdout, stderr };
256
352
  }
257
353
 
354
+ function nextPatchVersion(version: string): string {
355
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
356
+ if (!match) return "999.0.0";
357
+ return `${match[1]}.${match[2]}.${Number(match[3]) + 1}`;
358
+ }
359
+
258
360
  async function withWorkspaceRuntime(
259
- input: { projectDir: string },
361
+ input: { projectDir: string; cacheInvalidated?: number },
260
362
  run: (context: { env: Record<string, string> }) => Promise<void>,
261
363
  ): Promise<void> {
262
364
  const rigkitHome = mkdtempSync(join(tmpdir(), "rigkit-home-"));
@@ -306,6 +408,9 @@ async function withWorkspaceRuntime(
306
408
  }],
307
409
  });
308
410
  }
411
+ if (pathname === "/cache/invalidate") {
412
+ return runtimeJson({ ok: true, invalidated: input.cacheInvalidated ?? 1 });
413
+ }
309
414
  if (pathname === "/operations") {
310
415
  return runtimeJson({
311
416
  operations: [{
package/src/cli.ts CHANGED
@@ -25,6 +25,7 @@ import { initProject, normalizeMachineName, type InitProjectResult } from "./ini
25
25
  import { openExternalTarget } from "./interaction.ts";
26
26
  import { createRunPresenter, type RunPresenter } from "./run-presenter.ts";
27
27
  import { createRunLogger, type RunLogger } from "./run-logger.ts";
28
+ import { maybePrintUpdateNotice } from "./update-check.ts";
28
29
  import {
29
30
  completeRig,
30
31
  formatCompletionItems,
@@ -220,6 +221,14 @@ async function runCli(argv: string[]): Promise<void> {
220
221
  await runHelp(makeInvocation(rootOptions(program)));
221
222
  });
222
223
 
224
+ program.hook("postAction", async (_thisCommand, actionCommand) => {
225
+ await maybePrintUpdateNotice({
226
+ commandName: actionCommand.name(),
227
+ currentVersion: RIGKIT_CLI_VERSION,
228
+ json: commandWantsJson(program, actionCommand),
229
+ });
230
+ });
231
+
223
232
  program
224
233
  .command("init")
225
234
  .description("Initialize a Rigkit project")
@@ -408,6 +417,11 @@ function rootOptions(program: Command): GlobalOptions {
408
417
  };
409
418
  }
410
419
 
420
+ function commandWantsJson(program: Command, actionCommand: Command): boolean {
421
+ const options = actionCommand.opts<{ json?: boolean }>();
422
+ return Boolean(rootOptions(program).json || options.json);
423
+ }
424
+
411
425
  function parsePackageManagerOption(value: string | undefined): PackageManager | undefined {
412
426
  if (value === undefined) return undefined;
413
427
  if (isPackageManager(value)) return value;
@@ -1144,6 +1158,10 @@ async function runCacheInvalidate(invocation: CliInvocation, options: CacheInval
1144
1158
  printJson(result);
1145
1159
  return;
1146
1160
  }
1161
+ if (result.invalidated === 0) {
1162
+ console.log(ui.dim("no cache entries invalidated"));
1163
+ return;
1164
+ }
1147
1165
  console.log(
1148
1166
  `${ui.ok(ui.sym.ok)} invalidated ${result.invalidated} cache ${result.invalidated === 1 ? "entry" : "entries"}`,
1149
1167
  );
@@ -15,7 +15,7 @@ describe("CLI completion", () => {
15
15
  currentIndex: 2,
16
16
  });
17
17
 
18
- expect(items.map((item) => item.value)).toEqual(["api", "web"]);
18
+ expect(items.map((item) => item.value)).toEqual(["api", "web", "--json", "--help"]);
19
19
  expect(items[0]?.description).toBe("created 2h ago");
20
20
  });
21
21
  });
@@ -43,7 +43,7 @@ describe("CLI completion", () => {
43
43
  currentIndex: 3,
44
44
  });
45
45
 
46
- expect(items.map((item) => item.value)).toEqual(["api", "web"]);
46
+ expect(items.map((item) => item.value)).toEqual(["api", "web", "--json", "--help"]);
47
47
  });
48
48
  });
49
49
 
@@ -128,7 +128,7 @@ describe("CLI completion", () => {
128
128
  words: ["rig", "run", ""],
129
129
  currentIndex: 2,
130
130
  });
131
- expect(roots.map((item) => item.value)).toEqual(["api", "web"]);
131
+ expect(roots.map((item) => item.value)).toEqual(["api", "web", "--json", "--help"]);
132
132
  expect(roots[0]).toMatchObject({ description: "created 2h ago" });
133
133
 
134
134
  const exactWorkspace = await completeRig({
@@ -143,7 +143,7 @@ describe("CLI completion", () => {
143
143
  words: ["rig", "run", "api", ""],
144
144
  currentIndex: 3,
145
145
  });
146
- expect(workspaceAfterSpace.map((item) => item.value)).toEqual(["remove", "open-cmux"]);
146
+ expect(workspaceAfterSpace.map((item) => item.value)).toEqual(["remove", "open-cmux", "--json", "--help"]);
147
147
 
148
148
  const operationPrefix = await completeRig({
149
149
  cwd: projectDir,
@@ -162,7 +162,7 @@ describe("CLI completion", () => {
162
162
  words: ["rig", "rm", ""],
163
163
  currentIndex: 2,
164
164
  });
165
- expect(workspaces.map((item) => item.value)).toEqual(["api", "web"]);
165
+ expect(workspaces.map((item) => item.value)).toEqual(["api", "web", "-y", "--yes", "--all", "--json", "--help"]);
166
166
 
167
167
  const flags = await completeRig({
168
168
  cwd: projectDir,
@@ -204,7 +204,7 @@ describe("CLI completion", () => {
204
204
  currentIndex: 2,
205
205
  });
206
206
 
207
- expect(subcommands.map((item) => item.value)).toEqual(["ls", "clear"]);
207
+ expect(subcommands.map((item) => item.value)).toEqual(["ls", "clear", "invalidate"]);
208
208
 
209
209
  const clearFlags = await completeRig({
210
210
  cwd: process.cwd(),
@@ -217,9 +217,129 @@ describe("CLI completion", () => {
217
217
  "--global",
218
218
  "--all",
219
219
  "--json",
220
+ "--help",
220
221
  ]);
221
222
  });
222
223
 
224
+ test("completes project operation flags and workflow values", async () => {
225
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
226
+ await withWorkspaceRuntime({ projectDir }, async () => {
227
+ const flags = await completeRig({
228
+ cwd: projectDir,
229
+ words: ["rig", "apply", "--"],
230
+ currentIndex: 2,
231
+ });
232
+
233
+ expect(flags.map((item) => item.value)).toEqual([
234
+ "--workflow",
235
+ "--dry-run",
236
+ "--all",
237
+ "--discover",
238
+ "--json",
239
+ "--help",
240
+ ]);
241
+
242
+ const workflowValues = await completeRig({
243
+ cwd: projectDir,
244
+ words: ["rig", "apply", "--workflow", ""],
245
+ currentIndex: 3,
246
+ });
247
+ expect(workflowValues.map((item) => item.value)).toEqual(["smoke", "api"]);
248
+
249
+ const inlineWorkflow = await completeRig({
250
+ cwd: projectDir,
251
+ words: ["rig", "apply", "--workflow=s"],
252
+ currentIndex: 2,
253
+ });
254
+ expect(inlineWorkflow.map((item) => item.value)).toEqual(["--workflow=smoke"]);
255
+ });
256
+ });
257
+
258
+ test("completes workspace operation flags and enum values", async () => {
259
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
260
+ await withWorkspaceRuntime({ projectDir }, async () => {
261
+ const flags = await completeRig({
262
+ cwd: projectDir,
263
+ words: ["rig", "run", "api", "open-cmux", "--"],
264
+ currentIndex: 4,
265
+ });
266
+
267
+ expect(flags.map((item) => item.value)).toEqual(["--layout", "--json", "--help"]);
268
+
269
+ const values = await completeRig({
270
+ cwd: projectDir,
271
+ words: ["rig", "run", "api", "open-cmux", "--layout", ""],
272
+ currentIndex: 5,
273
+ });
274
+ expect(values.map((item) => item.value)).toEqual(["tabs", "splits"]);
275
+ });
276
+ });
277
+
278
+ test("completes cache invalidate targets and flags", async () => {
279
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
280
+ await withWorkspaceRuntime({ projectDir }, async () => {
281
+ const targets = await completeRig({
282
+ cwd: projectDir,
283
+ words: ["rig", "cache", "invalidate", ""],
284
+ currentIndex: 3,
285
+ });
286
+
287
+ expect(targets.map((item) => item.value)).toEqual([
288
+ "install-tooling",
289
+ "build",
290
+ "--all",
291
+ "-y",
292
+ "--yes",
293
+ "--json",
294
+ "--help",
295
+ ]);
296
+
297
+ const flags = await completeRig({
298
+ cwd: projectDir,
299
+ words: ["rig", "cache", "invalidate", "--"],
300
+ currentIndex: 3,
301
+ });
302
+ expect(flags.map((item) => item.value)).toEqual(["--all", "--yes", "--json", "--help"]);
303
+ });
304
+ });
305
+
306
+ test("completes static command flags and option values", async () => {
307
+ const initFlags = await completeRig({
308
+ cwd: process.cwd(),
309
+ words: ["rig", "init", "--"],
310
+ currentIndex: 2,
311
+ });
312
+ expect(initFlags.map((item) => item.value)).toEqual([
313
+ "--name",
314
+ "--api-key",
315
+ "--package-manager",
316
+ "--force",
317
+ "--json",
318
+ "--help",
319
+ ]);
320
+
321
+ const packageManagers = await completeRig({
322
+ cwd: process.cwd(),
323
+ words: ["rig", "init", "--package-manager", "p"],
324
+ currentIndex: 3,
325
+ });
326
+ expect(packageManagers.map((item) => item.value)).toEqual(["pnpm"]);
327
+
328
+ const doctorFlags = await completeRig({
329
+ cwd: process.cwd(),
330
+ words: ["rig", "doctor", "--"],
331
+ currentIndex: 2,
332
+ });
333
+ expect(doctorFlags.map((item) => item.value)).toEqual(["--cli", "--json", "--help"]);
334
+
335
+ const completionShells = await completeRig({
336
+ cwd: process.cwd(),
337
+ words: ["rig", "completion", ""],
338
+ currentIndex: 2,
339
+ });
340
+ expect(completionShells.map((item) => item.value)).toEqual(["bash", "fish", "zsh", "--help"]);
341
+ });
342
+
223
343
  test("formats shell completion items", () => {
224
344
  const items = [{ value: "api", description: "vm-api" }];
225
345
 
@@ -254,7 +374,7 @@ describe("CLI completion", () => {
254
374
  currentIndex: 2,
255
375
  });
256
376
 
257
- expect(items.map((item) => item.value)).toEqual(["workspaces", "snapshots", "config", "--json"]);
377
+ expect(items.map((item) => item.value)).toEqual(["workspaces", "snapshots", "config", "--json", "--help"]);
258
378
  });
259
379
  });
260
380
 
@@ -321,9 +441,139 @@ async function withWorkspaceRuntime(
321
441
  ],
322
442
  });
323
443
  }
444
+ if (pathname === "/workflows") {
445
+ return runtimeJson({
446
+ workflows: [
447
+ {
448
+ name: "smoke",
449
+ providers: [],
450
+ nodes: ["install-tooling", "build"],
451
+ operations: ["plan", "apply", "create"],
452
+ createsWorkspace: true,
453
+ },
454
+ {
455
+ name: "api",
456
+ providers: [],
457
+ nodes: ["install-tooling"],
458
+ operations: ["plan", "apply"],
459
+ createsWorkspace: false,
460
+ },
461
+ ],
462
+ });
463
+ }
464
+ if (pathname === "/cache") {
465
+ const nowMs = Date.now();
466
+ return runtimeJson({
467
+ entries: [
468
+ {
469
+ scope: "local",
470
+ workflow: "smoke",
471
+ nodePath: "install-tooling",
472
+ nodeName: "install-tooling",
473
+ nodeKind: "task",
474
+ runId: "run-install",
475
+ invalidated: false,
476
+ createdAt: new Date(nowMs - 60_000).toISOString(),
477
+ },
478
+ {
479
+ scope: "local",
480
+ workflow: "smoke",
481
+ nodePath: "build",
482
+ nodeName: "build",
483
+ nodeKind: "task",
484
+ runId: "run-build",
485
+ invalidated: false,
486
+ createdAt: new Date(nowMs - 30_000).toISOString(),
487
+ },
488
+ {
489
+ scope: "local",
490
+ workflow: "smoke",
491
+ nodePath: "old-task",
492
+ nodeName: "old-task",
493
+ nodeKind: "task",
494
+ runId: "run-old",
495
+ invalidated: true,
496
+ createdAt: new Date(nowMs - 10_000).toISOString(),
497
+ },
498
+ {
499
+ scope: "global",
500
+ workflow: "smoke",
501
+ nodePath: "base",
502
+ nodeName: "base",
503
+ nodeKind: "task",
504
+ runId: "run-base",
505
+ invalidated: false,
506
+ createdAt: new Date(nowMs - 5_000).toISOString(),
507
+ fragmentHash: "fragment",
508
+ },
509
+ ],
510
+ });
511
+ }
324
512
  if (pathname === "/operations") {
325
513
  return runtimeJson({
326
514
  operations: [
515
+ {
516
+ id: "plan",
517
+ kind: "command",
518
+ source: "core",
519
+ title: "Plan",
520
+ description: "Show cached and pending steps",
521
+ cli: {
522
+ options: [{ name: "workflow", flag: "--workflow" }],
523
+ },
524
+ inputSchema: {
525
+ type: "object",
526
+ additionalProperties: false,
527
+ properties: {
528
+ workflow: { type: "string", enum: ["smoke", "api"] },
529
+ },
530
+ },
531
+ },
532
+ {
533
+ id: "apply",
534
+ kind: "command",
535
+ source: "core",
536
+ title: "Apply",
537
+ description: "Resolve the workflow",
538
+ cli: {
539
+ options: [
540
+ { name: "workflow", flag: "--workflow" },
541
+ { name: "dryRun", flag: "--dry-run", type: "boolean" },
542
+ ],
543
+ },
544
+ inputSchema: {
545
+ type: "object",
546
+ additionalProperties: false,
547
+ properties: {
548
+ workflow: { type: "string", enum: ["smoke", "api"] },
549
+ dryRun: { type: "boolean" },
550
+ },
551
+ },
552
+ },
553
+ {
554
+ id: "create",
555
+ kind: "command",
556
+ source: "core",
557
+ title: "Create",
558
+ description: "Create a workspace",
559
+ createsWorkspace: true,
560
+ cli: {
561
+ positionals: [{ name: "name", index: 0 }],
562
+ options: [
563
+ { name: "workflow", flag: "--workflow" },
564
+ { name: "name", flag: "--name", required: true },
565
+ ],
566
+ },
567
+ inputSchema: {
568
+ type: "object",
569
+ additionalProperties: false,
570
+ required: ["name"],
571
+ properties: {
572
+ workflow: { type: "string", enum: ["smoke", "api"] },
573
+ name: { type: "string" },
574
+ },
575
+ },
576
+ },
327
577
  {
328
578
  id: "ssh",
329
579
  kind: "command",
@@ -365,10 +615,15 @@ async function withWorkspaceRuntime(
365
615
  source: "config",
366
616
  title: "Open cmux",
367
617
  description: "open cmux",
618
+ cli: {
619
+ options: [{ name: "layout", flag: "--layout", type: "string" }],
620
+ },
368
621
  inputSchema: {
369
622
  type: "object",
370
623
  additionalProperties: false,
371
- properties: {},
624
+ properties: {
625
+ layout: { type: "string", enum: ["tabs", "splits"] },
626
+ },
372
627
  },
373
628
  },
374
629
  ],