@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/README.md +34 -0
- package/package.json +42 -0
- package/src/cli.test.ts +419 -0
- package/src/cli.ts +2496 -0
- package/src/completion.test.ts +413 -0
- package/src/completion.ts +844 -0
- package/src/init.test.ts +90 -0
- package/src/init.ts +269 -0
- package/src/interaction.test.ts +28 -0
- package/src/interaction.ts +33 -0
- package/src/project.test.ts +81 -0
- package/src/project.ts +184 -0
- package/src/run-logger.test.ts +92 -0
- package/src/run-logger.ts +203 -0
- package/src/run-presenter.ts +250 -0
- package/src/ui.ts +159 -0
- package/src/version.ts +1 -0
- package/src/workspace-name.test.ts +17 -0
- package/src/workspace-name.ts +59 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { projectIdFor, runtimeFingerprintFor, runtimePaths, SUPPORTED_RUNTIME_API_VERSION } from "@rigkit/runtime-client";
|
|
6
|
+
import { completeRig, formatCompletionItems, formatWorkspaceAge, renderCompletionScript } from "./completion.ts";
|
|
7
|
+
|
|
8
|
+
describe("CLI completion", () => {
|
|
9
|
+
test("completes workspace targets from the runtime", async () => {
|
|
10
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
|
|
11
|
+
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
12
|
+
const items = await completeRig({
|
|
13
|
+
cwd: projectDir,
|
|
14
|
+
words: ["rig", "run", ""],
|
|
15
|
+
currentIndex: 2,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(items.map((item) => item.value)).toEqual(["api", "web"]);
|
|
19
|
+
expect(items[0]?.description).toBe("created 2h ago");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("does not complete provider resource ids as workspace targets", async () => {
|
|
24
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
|
|
25
|
+
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
26
|
+
const items = await completeRig({
|
|
27
|
+
cwd: projectDir,
|
|
28
|
+
words: ["rig", "run", "vm-"],
|
|
29
|
+
currentIndex: 2,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(items).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("respects -chdir when completing workspace targets", async () => {
|
|
37
|
+
const parentDir = mkdtempSync(join(tmpdir(), "rigkit-completion-parent-"));
|
|
38
|
+
const projectDir = join(parentDir, "project");
|
|
39
|
+
await withWorkspaceRuntime({ projectDir, cleanupDir: parentDir }, async () => {
|
|
40
|
+
const items = await completeRig({
|
|
41
|
+
cwd: parentDir,
|
|
42
|
+
words: ["rig", "-chdir=project", "run", ""],
|
|
43
|
+
currentIndex: 3,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(items.map((item) => item.value)).toEqual(["api", "web"]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("completes project directories for -chdir", async () => {
|
|
51
|
+
const cwd = mkdtempSync(join(tmpdir(), "rigkit-completion-dirs-"));
|
|
52
|
+
mkdirSync(join(cwd, "examples", "global-fragments"), { recursive: true });
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const roots = await completeRig({
|
|
56
|
+
cwd,
|
|
57
|
+
words: ["rig", "-chdir="],
|
|
58
|
+
currentIndex: 1,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(roots).toContainEqual({
|
|
62
|
+
value: "-chdir=examples/",
|
|
63
|
+
description: "directory",
|
|
64
|
+
noSpace: true,
|
|
65
|
+
group: "Paths",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const nested = await completeRig({
|
|
69
|
+
cwd,
|
|
70
|
+
words: ["rig", "-chdir=examples/g"],
|
|
71
|
+
currentIndex: 1,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(nested).toContainEqual({
|
|
75
|
+
value: "-chdir=examples/global-fragments/",
|
|
76
|
+
description: "directory",
|
|
77
|
+
noSpace: true,
|
|
78
|
+
group: "Paths",
|
|
79
|
+
});
|
|
80
|
+
} finally {
|
|
81
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("completes named config files", async () => {
|
|
86
|
+
const cwd = mkdtempSync(join(tmpdir(), "rigkit-completion-configs-"));
|
|
87
|
+
writeFileSync(join(cwd, "api.rig.config.ts"), "export default {}\n");
|
|
88
|
+
writeFileSync(join(cwd, "web.rig.config.ts"), "export default {}\n");
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const items = await completeRig({
|
|
92
|
+
cwd,
|
|
93
|
+
words: ["rig", "-config="],
|
|
94
|
+
currentIndex: 1,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(items.map((item) => item.value)).toEqual(["-config=api.rig.config.ts", "-config=web.rig.config.ts"]);
|
|
98
|
+
} finally {
|
|
99
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("respects -chdir when completing config files", async () => {
|
|
104
|
+
const cwd = mkdtempSync(join(tmpdir(), "rigkit-completion-configs-"));
|
|
105
|
+
const projectDir = join(cwd, "global-fragments");
|
|
106
|
+
mkdirSync(projectDir, { recursive: true });
|
|
107
|
+
writeFileSync(join(projectDir, "api.rig.config.ts"), "export default {}\n");
|
|
108
|
+
writeFileSync(join(projectDir, "worker.rig.config.ts"), "export default {}\n");
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const items = await completeRig({
|
|
112
|
+
cwd,
|
|
113
|
+
words: ["rig", "-chdir=global-fragments", "-config="],
|
|
114
|
+
currentIndex: 2,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(items.map((item) => item.value)).toEqual(["-config=api.rig.config.ts", "-config=worker.rig.config.ts"]);
|
|
118
|
+
} finally {
|
|
119
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("completes workspace operation targets", async () => {
|
|
124
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
|
|
125
|
+
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
126
|
+
const roots = await completeRig({
|
|
127
|
+
cwd: projectDir,
|
|
128
|
+
words: ["rig", "run", ""],
|
|
129
|
+
currentIndex: 2,
|
|
130
|
+
});
|
|
131
|
+
expect(roots.map((item) => item.value)).toEqual(["api", "web"]);
|
|
132
|
+
expect(roots[0]).toMatchObject({ description: "created 2h ago" });
|
|
133
|
+
|
|
134
|
+
const exactWorkspace = await completeRig({
|
|
135
|
+
cwd: projectDir,
|
|
136
|
+
words: ["rig", "run", "api"],
|
|
137
|
+
currentIndex: 2,
|
|
138
|
+
});
|
|
139
|
+
expect(exactWorkspace.map((item) => item.value)).toEqual(["api"]);
|
|
140
|
+
|
|
141
|
+
const workspaceAfterSpace = await completeRig({
|
|
142
|
+
cwd: projectDir,
|
|
143
|
+
words: ["rig", "run", "api", ""],
|
|
144
|
+
currentIndex: 3,
|
|
145
|
+
});
|
|
146
|
+
expect(workspaceAfterSpace.map((item) => item.value)).toEqual(["remove", "open-cmux"]);
|
|
147
|
+
|
|
148
|
+
const operationPrefix = await completeRig({
|
|
149
|
+
cwd: projectDir,
|
|
150
|
+
words: ["rig", "run", "api", "open"],
|
|
151
|
+
currentIndex: 3,
|
|
152
|
+
});
|
|
153
|
+
expect(operationPrefix.map((item) => item.value)).toEqual(["open-cmux"]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("completes rm workspace targets and confirmation flags", async () => {
|
|
158
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
|
|
159
|
+
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
160
|
+
const workspaces = await completeRig({
|
|
161
|
+
cwd: projectDir,
|
|
162
|
+
words: ["rig", "rm", ""],
|
|
163
|
+
currentIndex: 2,
|
|
164
|
+
});
|
|
165
|
+
expect(workspaces.map((item) => item.value)).toEqual(["api", "web"]);
|
|
166
|
+
|
|
167
|
+
const flags = await completeRig({
|
|
168
|
+
cwd: projectDir,
|
|
169
|
+
words: ["rig", "rm", "api", "-"],
|
|
170
|
+
currentIndex: 3,
|
|
171
|
+
});
|
|
172
|
+
expect(flags.map((item) => item.value)).toContain("-y");
|
|
173
|
+
expect(flags.map((item) => item.value)).toContain("--yes");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("completes top-level project commands at the root command position", async () => {
|
|
178
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
|
|
179
|
+
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
180
|
+
const items = await completeRig({
|
|
181
|
+
cwd: projectDir,
|
|
182
|
+
words: ["rig", "p"],
|
|
183
|
+
currentIndex: 1,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(items.map((item) => item.value)).toEqual(["plan", "projects"]);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("completes cache at the root command position after global options", async () => {
|
|
191
|
+
const items = await completeRig({
|
|
192
|
+
cwd: process.cwd(),
|
|
193
|
+
words: ["rig", "-config=api.rig.config.ts", "c"],
|
|
194
|
+
currentIndex: 2,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(items.map((item) => item.value)).toEqual(["create", "cache", "completion"]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("completes cache subcommands and flags", async () => {
|
|
201
|
+
const subcommands = await completeRig({
|
|
202
|
+
cwd: process.cwd(),
|
|
203
|
+
words: ["rig", "cache", ""],
|
|
204
|
+
currentIndex: 2,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(subcommands.map((item) => item.value)).toEqual(["ls", "clear"]);
|
|
208
|
+
|
|
209
|
+
const clearFlags = await completeRig({
|
|
210
|
+
cwd: process.cwd(),
|
|
211
|
+
words: ["rig", "cache", "clear", "--"],
|
|
212
|
+
currentIndex: 3,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(clearFlags.map((item) => item.value)).toEqual([
|
|
216
|
+
"--local",
|
|
217
|
+
"--global",
|
|
218
|
+
"--all",
|
|
219
|
+
"--json",
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("formats shell completion items", () => {
|
|
224
|
+
const items = [{ value: "api", description: "vm-api" }];
|
|
225
|
+
|
|
226
|
+
expect(formatCompletionItems(items, "bash")).toBe("api");
|
|
227
|
+
// zsh wire format is `value\tdescription\tmarker\tgroup`. Empty trailing
|
|
228
|
+
// fields are kept so the shell-side parser can index positionally.
|
|
229
|
+
expect(formatCompletionItems(items, "zsh")).toBe("api\tvm-api\t\t");
|
|
230
|
+
expect(formatCompletionItems(
|
|
231
|
+
[{ value: "api", description: "workspace smoke", noSpace: true, group: "Workspaces" }],
|
|
232
|
+
"zsh",
|
|
233
|
+
)).toBe("api\tworkspace smoke\tnospace\tWorkspaces");
|
|
234
|
+
expect(renderCompletionScript("zsh")).toContain("rig __complete");
|
|
235
|
+
expect(renderCompletionScript("zsh")).toContain("_describe");
|
|
236
|
+
expect(renderCompletionScript("zsh")).toContain(":completion:*:rig:*:descriptions");
|
|
237
|
+
expect(renderCompletionScript("zsh")).toContain("compdef _rig rig");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("formats workspace ages", () => {
|
|
241
|
+
const now = Date.parse("2026-05-14T12:00:00.000Z");
|
|
242
|
+
|
|
243
|
+
expect(formatWorkspaceAge("2026-05-14T11:59:45.000Z", now)).toBe("just now");
|
|
244
|
+
expect(formatWorkspaceAge("2026-05-14T11:30:00.000Z", now)).toBe("30m ago");
|
|
245
|
+
expect(formatWorkspaceAge("2026-05-14T09:00:00.000Z", now)).toBe("3h ago");
|
|
246
|
+
expect(formatWorkspaceAge("2026-05-11T12:00:00.000Z", now)).toBe("3d ago");
|
|
247
|
+
expect(formatWorkspaceAge("not-a-date", now)).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("completes ls targets", async () => {
|
|
251
|
+
const items = await completeRig({
|
|
252
|
+
cwd: process.cwd(),
|
|
253
|
+
words: ["rig", "ls", ""],
|
|
254
|
+
currentIndex: 2,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(items.map((item) => item.value)).toEqual(["workspaces", "snapshots", "config", "--json"]);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
async function withWorkspaceRuntime(
|
|
262
|
+
input: { projectDir: string; cleanupDir?: string },
|
|
263
|
+
run: () => Promise<void>,
|
|
264
|
+
): Promise<void> {
|
|
265
|
+
const previousHome = process.env.RIGKIT_HOME;
|
|
266
|
+
const rigkitHome = mkdtempSync(join(tmpdir(), "rigkit-home-"));
|
|
267
|
+
const token = "test-token";
|
|
268
|
+
const configPath = join(input.projectDir, "rig.config.ts");
|
|
269
|
+
mkdirSync(input.projectDir, { recursive: true });
|
|
270
|
+
writeFileSync(configPath, "export default {}\n");
|
|
271
|
+
const projectId = projectIdFor({ projectDir: input.projectDir, configPath });
|
|
272
|
+
const runtimeFingerprint = runtimeFingerprintFor({ projectDir: input.projectDir, configPath });
|
|
273
|
+
const paths = runtimePaths(projectId, rigkitHome);
|
|
274
|
+
mkdirSync(paths.root, { recursive: true });
|
|
275
|
+
writeFileSync(paths.tokenPath, `${token}\n`);
|
|
276
|
+
|
|
277
|
+
const server = Bun.serve({
|
|
278
|
+
hostname: "127.0.0.1",
|
|
279
|
+
port: 0,
|
|
280
|
+
fetch(request) {
|
|
281
|
+
if (request.headers.get("authorization") !== `Bearer ${token}`) {
|
|
282
|
+
return runtimeJson({ error: { message: "Unauthorized" } }, { status: 401 });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const { pathname } = new URL(request.url);
|
|
286
|
+
if (pathname === "/health") {
|
|
287
|
+
return runtimeJson({
|
|
288
|
+
ok: true,
|
|
289
|
+
projectId,
|
|
290
|
+
runtimeFingerprint,
|
|
291
|
+
projectDir: input.projectDir,
|
|
292
|
+
configPath,
|
|
293
|
+
engineVersion: "engine-test",
|
|
294
|
+
runtimeVersion: "runtime-test",
|
|
295
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (pathname === "/workspaces") {
|
|
299
|
+
const nowMs = Date.now();
|
|
300
|
+
const apiCreatedAt = new Date(nowMs - 2 * 60 * 60 * 1000).toISOString();
|
|
301
|
+
const webCreatedAt = new Date(nowMs - 5 * 60 * 1000).toISOString();
|
|
302
|
+
const updatedAt = new Date(nowMs).toISOString();
|
|
303
|
+
return runtimeJson({
|
|
304
|
+
workspaces: [
|
|
305
|
+
{
|
|
306
|
+
id: "workspace-api",
|
|
307
|
+
name: "api",
|
|
308
|
+
workflow: "smoke",
|
|
309
|
+
ctx: {},
|
|
310
|
+
createdAt: apiCreatedAt,
|
|
311
|
+
updatedAt,
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: "workspace-web",
|
|
315
|
+
name: "web",
|
|
316
|
+
workflow: "smoke",
|
|
317
|
+
ctx: {},
|
|
318
|
+
createdAt: webCreatedAt,
|
|
319
|
+
updatedAt,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
if (pathname === "/operations") {
|
|
325
|
+
return runtimeJson({
|
|
326
|
+
operations: [
|
|
327
|
+
{
|
|
328
|
+
id: "ssh",
|
|
329
|
+
kind: "command",
|
|
330
|
+
source: "core",
|
|
331
|
+
title: "SSH",
|
|
332
|
+
description: "open SSH",
|
|
333
|
+
cli: {
|
|
334
|
+
positionals: [{ name: "workspaceOrVmId", index: 0 }],
|
|
335
|
+
options: [{ name: "print", flag: "--print", type: "boolean", runtime: false }],
|
|
336
|
+
},
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: "object",
|
|
339
|
+
additionalProperties: false,
|
|
340
|
+
properties: {
|
|
341
|
+
workspaceOrVmId: { type: "string" },
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
workspaceOperations: [
|
|
347
|
+
{
|
|
348
|
+
id: "remove",
|
|
349
|
+
kind: "workspace-action",
|
|
350
|
+
source: "core",
|
|
351
|
+
title: "Remove",
|
|
352
|
+
description: "remove workspace",
|
|
353
|
+
cli: {
|
|
354
|
+
options: [{ name: "yes", flag: "--yes", aliases: ["-y"], type: "boolean", runtime: false }],
|
|
355
|
+
},
|
|
356
|
+
inputSchema: {
|
|
357
|
+
type: "object",
|
|
358
|
+
additionalProperties: false,
|
|
359
|
+
properties: {},
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: "open-cmux",
|
|
364
|
+
kind: "workspace-action",
|
|
365
|
+
source: "config",
|
|
366
|
+
title: "Open cmux",
|
|
367
|
+
description: "open cmux",
|
|
368
|
+
inputSchema: {
|
|
369
|
+
type: "object",
|
|
370
|
+
additionalProperties: false,
|
|
371
|
+
properties: {},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return runtimeJson({ error: { message: "Not found" } }, { status: 404 });
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
writeFileSync(
|
|
382
|
+
paths.handlePath,
|
|
383
|
+
`${JSON.stringify({
|
|
384
|
+
projectId,
|
|
385
|
+
runtimeFingerprint,
|
|
386
|
+
projectDir: input.projectDir,
|
|
387
|
+
configPath,
|
|
388
|
+
pid: process.pid,
|
|
389
|
+
url: `http://127.0.0.1:${server.port}`,
|
|
390
|
+
tokenPath: paths.tokenPath,
|
|
391
|
+
}, null, 2)}\n`,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
process.env.RIGKIT_HOME = rigkitHome;
|
|
395
|
+
try {
|
|
396
|
+
await run();
|
|
397
|
+
} finally {
|
|
398
|
+
if (previousHome === undefined) {
|
|
399
|
+
delete process.env.RIGKIT_HOME;
|
|
400
|
+
} else {
|
|
401
|
+
process.env.RIGKIT_HOME = previousHome;
|
|
402
|
+
}
|
|
403
|
+
server.stop(true);
|
|
404
|
+
rmSync(rigkitHome, { recursive: true, force: true });
|
|
405
|
+
rmSync(input.cleanupDir ?? input.projectDir, { recursive: true, force: true });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function runtimeJson(body: unknown, init: ResponseInit = {}): Response {
|
|
410
|
+
const headers = new Headers(init.headers);
|
|
411
|
+
headers.set("x-rigkit-api-version", String(SUPPORTED_RUNTIME_API_VERSION));
|
|
412
|
+
return Response.json(body, { ...init, headers });
|
|
413
|
+
}
|