@rigkit/cli 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +4 -4
- package/src/cli.test.ts +42 -4
- package/src/cli.ts +172 -64
- package/src/completion.test.ts +59 -15
- package/src/completion.ts +132 -125
- package/src/version.ts +1 -1
- package/src/workspace-name.test.ts +17 -0
- package/src/workspace-name.ts +59 -0
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Interactive terminals use Inquirer prompts and a chalk/log-update run timeline.
|
|
|
16
16
|
|
|
17
17
|
Interactive providers can ask the CLI to open provider-owned URLs. For example, Freestyle terminal sessions are served by the Freestyle provider, while the CLI only opens the presented URL in a browser.
|
|
18
18
|
|
|
19
|
-
Workspace-specific operations run as `rig <workspace
|
|
19
|
+
Workspace-specific operations run as `rig run <workspace> <operation>`, for example `rig run website-workspace open-cmux` or `rig run website-workspace remove -y`.
|
|
20
20
|
|
|
21
21
|
`rig ls` lists workspaces for the selected project. `rig ls snapshots` lists cached snapshot runs, and `rig ls config` shows the resolved project paths.
|
|
22
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigkit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"log-symbols": "^7.0.1",
|
|
26
26
|
"log-update": "^8.0.0",
|
|
27
27
|
"ora": "^9.4.0",
|
|
28
|
-
"@rigkit/provider-cmux": "0.2.
|
|
29
|
-
"@rigkit/engine": "0.2.
|
|
30
|
-
"@rigkit/runtime-client": "0.2.
|
|
28
|
+
"@rigkit/provider-cmux": "0.2.6",
|
|
29
|
+
"@rigkit/engine": "0.2.6",
|
|
30
|
+
"@rigkit/runtime-client": "0.2.6"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/bun": "latest",
|
package/src/cli.test.ts
CHANGED
|
@@ -23,7 +23,8 @@ describe("CLI entrypoint", () => {
|
|
|
23
23
|
expect(rootHelp.exitCode).toBe(0);
|
|
24
24
|
expect(rootHelp.stderr).toBe("");
|
|
25
25
|
expect(rootHelp.stdout).toContain("rig ");
|
|
26
|
-
expect(rootHelp.stdout).toContain("
|
|
26
|
+
expect(rootHelp.stdout).toContain("plan Plan project workflow changes");
|
|
27
|
+
expect(rootHelp.stdout).toContain("run Run a workspace operation");
|
|
27
28
|
|
|
28
29
|
const version = await runCli(["version"]);
|
|
29
30
|
expect(version.exitCode).toBe(0);
|
|
@@ -34,15 +35,16 @@ describe("CLI entrypoint", () => {
|
|
|
34
35
|
expect(help.exitCode).toBe(0);
|
|
35
36
|
expect(help.stderr).toBe("");
|
|
36
37
|
expect(help.stdout).toContain("rig ");
|
|
37
|
-
expect(help.stdout).toContain("
|
|
38
|
+
expect(help.stdout).toContain("plan Plan project workflow changes");
|
|
39
|
+
expect(help.stdout).toContain("run Run a workspace operation");
|
|
38
40
|
});
|
|
39
41
|
|
|
40
|
-
test("
|
|
42
|
+
test("rejects operation shorthand at the root", async () => {
|
|
41
43
|
const result = await runCli(["unknown"]);
|
|
42
44
|
|
|
43
45
|
expect(result.exitCode).toBe(1);
|
|
44
46
|
expect(result.stdout).toBe("");
|
|
45
|
-
expect(result.stderr).toContain("
|
|
47
|
+
expect(result.stderr).toContain("unknown command 'unknown'");
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
test("serves dynamic shell completion endpoint", async () => {
|
|
@@ -106,6 +108,18 @@ describe("CLI entrypoint", () => {
|
|
|
106
108
|
});
|
|
107
109
|
});
|
|
108
110
|
|
|
111
|
+
test("rejects workspace create names that are not shell-safe", async () => {
|
|
112
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-cli-create-name-"));
|
|
113
|
+
|
|
114
|
+
await withWorkspaceRuntime({ projectDir }, async ({ env }) => {
|
|
115
|
+
const result = await runCli(["-C", projectDir, "create", "--name", "some workspace", "--json"], { env });
|
|
116
|
+
|
|
117
|
+
expect(result.exitCode).toBe(1);
|
|
118
|
+
expect(result.stdout).toBe("");
|
|
119
|
+
expect(result.stderr).toContain('Invalid workspace name "some workspace"');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
109
123
|
test("requires discovered projects for operation --all", async () => {
|
|
110
124
|
const cwd = mkdtempSync(join(tmpdir(), "rigkit-cli-run-all-"));
|
|
111
125
|
|
|
@@ -216,6 +230,30 @@ async function withWorkspaceRuntime(
|
|
|
216
230
|
}],
|
|
217
231
|
});
|
|
218
232
|
}
|
|
233
|
+
if (pathname === "/operations") {
|
|
234
|
+
return runtimeJson({
|
|
235
|
+
operations: [{
|
|
236
|
+
id: "create",
|
|
237
|
+
kind: "command",
|
|
238
|
+
source: "core",
|
|
239
|
+
title: "Create",
|
|
240
|
+
description: "Create a workspace",
|
|
241
|
+
createsWorkspace: true,
|
|
242
|
+
cli: {
|
|
243
|
+
options: [{ name: "name", flag: "--name", required: true, type: "string" }],
|
|
244
|
+
},
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: "object",
|
|
247
|
+
additionalProperties: false,
|
|
248
|
+
properties: {
|
|
249
|
+
name: { type: "string", minLength: 1 },
|
|
250
|
+
},
|
|
251
|
+
required: ["name"],
|
|
252
|
+
},
|
|
253
|
+
}],
|
|
254
|
+
workspaceOperations: [],
|
|
255
|
+
});
|
|
256
|
+
}
|
|
219
257
|
return runtimeJson({ error: { message: "Not found" } }, { status: 404 });
|
|
220
258
|
},
|
|
221
259
|
});
|
package/src/cli.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
resolveCompletionShell,
|
|
37
37
|
type CompletionShell,
|
|
38
38
|
} from "./completion.ts";
|
|
39
|
+
import { generateWorkspaceName } from "./workspace-name.ts";
|
|
39
40
|
|
|
40
41
|
type GlobalOptions = {
|
|
41
42
|
project?: string;
|
|
@@ -148,41 +149,10 @@ const CLI_HOST_CAPABILITIES: Array<{ id: string; schemaHash?: string }> = [
|
|
|
148
149
|
...(capability.schemaHash ? { schemaHash: capability.schemaHash } : {}),
|
|
149
150
|
}));
|
|
150
151
|
|
|
151
|
-
const MANAGEMENT_COMMANDS = new Set([
|
|
152
|
-
"completion",
|
|
153
|
-
"doctor",
|
|
154
|
-
"help",
|
|
155
|
-
"init",
|
|
156
|
-
"ls",
|
|
157
|
-
"projects",
|
|
158
|
-
"run",
|
|
159
|
-
"version",
|
|
160
|
-
]);
|
|
161
|
-
|
|
162
|
-
const GLOBAL_OPTIONS_WITH_VALUES = new Set(["-C", "--project", "--config", "--state"]);
|
|
163
|
-
|
|
164
152
|
if (process.argv[2] === "__complete") {
|
|
165
153
|
runCompletionEndpoint(process.argv.slice(3)).catch(handleCliError);
|
|
166
154
|
} else {
|
|
167
|
-
runCli(
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function normalizeOperationArgv(argv: string[]): string[] {
|
|
171
|
-
const args = argv.slice(2);
|
|
172
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
173
|
-
const arg = args[index]!;
|
|
174
|
-
if (arg === "--") return argv;
|
|
175
|
-
if (GLOBAL_OPTIONS_WITH_VALUES.has(arg)) {
|
|
176
|
-
index += 1;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if ([...GLOBAL_OPTIONS_WITH_VALUES].some((option) => arg.startsWith(`${option}=`))) continue;
|
|
180
|
-
if (arg === "--json") continue;
|
|
181
|
-
if (arg.startsWith("-")) return argv;
|
|
182
|
-
if (MANAGEMENT_COMMANDS.has(arg)) return argv;
|
|
183
|
-
return [...argv.slice(0, 2), ...args.slice(0, index), "run", ...args.slice(index)];
|
|
184
|
-
}
|
|
185
|
-
return argv;
|
|
155
|
+
runCli(process.argv).catch(handleCliError);
|
|
186
156
|
}
|
|
187
157
|
|
|
188
158
|
async function runCli(argv: string[]): Promise<void> {
|
|
@@ -190,7 +160,7 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
190
160
|
program
|
|
191
161
|
.name("rig")
|
|
192
162
|
.description("Rigkit workflow CLI")
|
|
193
|
-
.usage("[options] <command
|
|
163
|
+
.usage("[options] <command>")
|
|
194
164
|
.version(RIGKIT_CLI_VERSION, "-v, --version", "Show Rigkit CLI version")
|
|
195
165
|
.showHelpAfterError()
|
|
196
166
|
.exitOverride()
|
|
@@ -227,22 +197,46 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
227
197
|
});
|
|
228
198
|
});
|
|
229
199
|
|
|
200
|
+
for (const operation of ["plan", "apply"] as const) {
|
|
201
|
+
program
|
|
202
|
+
.command(`${operation} [args...]`)
|
|
203
|
+
.description(operation === "plan" ? "Plan project workflow changes" : "Apply project workflow changes")
|
|
204
|
+
.allowUnknownOption(true)
|
|
205
|
+
.option("--all", "Run against every discovered project")
|
|
206
|
+
.option("--discover", "Discover projects below the selected directory")
|
|
207
|
+
.option("--json", "Print machine-readable JSON")
|
|
208
|
+
.action(async (args: string[], options: { all?: boolean; discover?: boolean; json?: boolean }) => {
|
|
209
|
+
await runProjectOperation(makeInvocation(rootOptions(program), options.json), operation, args ?? [], {
|
|
210
|
+
all: Boolean(options.all),
|
|
211
|
+
discover: Boolean(options.discover),
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
program
|
|
217
|
+
.command("create [args...]")
|
|
218
|
+
.description("Create a workspace")
|
|
219
|
+
.allowUnknownOption(true)
|
|
220
|
+
.option("--json", "Print machine-readable JSON")
|
|
221
|
+
.action(async (args: string[], options: { json?: boolean }) => {
|
|
222
|
+
await runProjectOperation(makeInvocation(rootOptions(program), options.json), "create", args ?? [], {
|
|
223
|
+
all: false,
|
|
224
|
+
discover: false,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
230
228
|
program
|
|
231
|
-
.command("run <operation> [args...]"
|
|
232
|
-
.description("Run a
|
|
229
|
+
.command("run <workspace> <operation> [args...]")
|
|
230
|
+
.description("Run a workspace operation")
|
|
233
231
|
.allowUnknownOption(true)
|
|
234
|
-
.option("--all", "Run against every discovered project")
|
|
235
|
-
.option("--discover", "Discover projects below the selected directory")
|
|
236
232
|
.option("--json", "Print machine-readable JSON")
|
|
237
233
|
.action(async (
|
|
234
|
+
workspace: string,
|
|
238
235
|
operation: string,
|
|
239
236
|
args: string[],
|
|
240
|
-
options: {
|
|
237
|
+
options: { json?: boolean },
|
|
241
238
|
) => {
|
|
242
|
-
await
|
|
243
|
-
all: Boolean(options.all),
|
|
244
|
-
discover: Boolean(options.discover),
|
|
245
|
-
});
|
|
239
|
+
await runWorkspaceOperation(makeInvocation(rootOptions(program), options.json), workspace, operation, args ?? []);
|
|
246
240
|
});
|
|
247
241
|
|
|
248
242
|
program
|
|
@@ -499,6 +493,43 @@ async function promptPackageManager(defaultValue: PackageManager): Promise<Packa
|
|
|
499
493
|
return answers.packageManager;
|
|
500
494
|
}
|
|
501
495
|
|
|
496
|
+
async function promptWorkspaceName(defaultValue: string): Promise<string> {
|
|
497
|
+
const answers = await inquirer.prompt<{ name: string }>([{
|
|
498
|
+
type: "input",
|
|
499
|
+
name: "name",
|
|
500
|
+
message: "Workspace name:",
|
|
501
|
+
default: defaultValue,
|
|
502
|
+
validate(value: string) {
|
|
503
|
+
return validateWorkspaceName(value.trim());
|
|
504
|
+
},
|
|
505
|
+
filter: (value: string) => value.trim(),
|
|
506
|
+
}]);
|
|
507
|
+
return answers.name;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const workspaceNamePattern = /^(?!-)[A-Za-z0-9._-]+$/;
|
|
511
|
+
|
|
512
|
+
function validateWorkspaceName(value: string): true | string {
|
|
513
|
+
if (!value) return "Workspace name is required.";
|
|
514
|
+
if (!workspaceNamePattern.test(value)) {
|
|
515
|
+
return 'Use only letters, numbers, ".", "_", and "-", and do not start with "-".';
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function assertValidWorkspaceName(value: unknown): void {
|
|
521
|
+
if (typeof value !== "string") throw new Error(`Workspace name must be a string`);
|
|
522
|
+
const valid = validateWorkspaceName(value);
|
|
523
|
+
if (valid !== true) throw new Error(`Invalid workspace name "${value}". ${valid}`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function defaultWorkspaceName(runtime: RuntimeClient): Promise<string> {
|
|
527
|
+
const existingNames = await runtime.control.workspaces()
|
|
528
|
+
.then((response) => response.workspaces.map((workspace) => workspace.name))
|
|
529
|
+
.catch(() => []);
|
|
530
|
+
return generateWorkspaceName(existingNames);
|
|
531
|
+
}
|
|
532
|
+
|
|
502
533
|
async function runPackageManagerInstall(
|
|
503
534
|
projectDir: string,
|
|
504
535
|
packageManager: PackageManager,
|
|
@@ -649,6 +680,48 @@ async function runProjectOperation(
|
|
|
649
680
|
}
|
|
650
681
|
|
|
651
682
|
await renderOperationResult(operation, result, parsed.hostOptions);
|
|
683
|
+
printInteractiveOutputGap(invocation);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function runWorkspaceOperation(
|
|
687
|
+
invocation: CliInvocation,
|
|
688
|
+
workspaceName: string,
|
|
689
|
+
requestedOperation: string,
|
|
690
|
+
args: string[],
|
|
691
|
+
): Promise<void> {
|
|
692
|
+
const runtime = await loadRuntime(invocation);
|
|
693
|
+
const manifest = await readRuntimeOperations(runtime);
|
|
694
|
+
const operation = (manifest.workspaceOperations ?? []).find((item) =>
|
|
695
|
+
item.id === requestedOperation || item.aliases?.includes(requestedOperation)
|
|
696
|
+
);
|
|
697
|
+
if (!operation) {
|
|
698
|
+
throw new Error(`This project does not define a workspace operation named "${requestedOperation}".`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const workspaces = await runtime.control.workspaces()
|
|
702
|
+
.then((response) => response.workspaces as WorkspaceRecord[])
|
|
703
|
+
.catch(() => []);
|
|
704
|
+
if (!workspaces.some((workspace) => workspace.name === workspaceName)) {
|
|
705
|
+
throw new Error(`This project does not have a workspace named "${workspaceName}".`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const parsed = parseOperationArgs(operation, args);
|
|
709
|
+
enforceHostOnlyBooleanGuards(operation, parsed);
|
|
710
|
+
|
|
711
|
+
const result = await runRuntimeOperation<unknown>(
|
|
712
|
+
runtime,
|
|
713
|
+
`${workspaceName}/${operation.id}`,
|
|
714
|
+
parsed.input,
|
|
715
|
+
{ renderEvents: !wantsJson(invocation) },
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
if (wantsJson(invocation)) {
|
|
719
|
+
printJson(result);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
await renderOperationResult(operation, result, parsed.hostOptions);
|
|
724
|
+
printInteractiveOutputGap(invocation);
|
|
652
725
|
}
|
|
653
726
|
|
|
654
727
|
async function runDiscoveredProjectOperation(
|
|
@@ -704,6 +777,7 @@ async function runDiscoveredProjectOperation(
|
|
|
704
777
|
console.log(chalk.bold(displayProjectDir(project.projectDir)));
|
|
705
778
|
}
|
|
706
779
|
await renderOperationResult(operation, result, parsed.hostOptions);
|
|
780
|
+
printInteractiveOutputGap(invocation);
|
|
707
781
|
}
|
|
708
782
|
}
|
|
709
783
|
|
|
@@ -780,7 +854,7 @@ async function executeRuntimeOperation(
|
|
|
780
854
|
}
|
|
781
855
|
const { operation, runOperation } = resolved;
|
|
782
856
|
|
|
783
|
-
const parsed =
|
|
857
|
+
const parsed = await parseOperationArgsWithPrompts(invocation, runtime, operation, args);
|
|
784
858
|
enforceHostOnlyBooleanGuards(operation, parsed);
|
|
785
859
|
|
|
786
860
|
const result = await runRuntimeOperation<unknown>(
|
|
@@ -797,28 +871,43 @@ function findRuntimeOperation(
|
|
|
797
871
|
manifest: RuntimeOperationManifest,
|
|
798
872
|
requestedOperation: string,
|
|
799
873
|
): { operation: RuntimeOperationDefinition; runOperation: string } | undefined {
|
|
800
|
-
const workspaceOperation = parseWorkspaceOperationId(requestedOperation);
|
|
801
|
-
if (workspaceOperation) {
|
|
802
|
-
const operation = (manifest.workspaceOperations ?? []).find((item) => item.id === workspaceOperation.operation);
|
|
803
|
-
return operation ? { operation, runOperation: requestedOperation } : undefined;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
874
|
const operation = manifest.operations.find((operation) =>
|
|
807
875
|
operation.id === requestedOperation || operation.aliases?.includes(requestedOperation)
|
|
808
876
|
);
|
|
809
877
|
return operation ? { operation, runOperation: operation.id } : undefined;
|
|
810
878
|
}
|
|
811
879
|
|
|
812
|
-
function
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
880
|
+
async function parseOperationArgsWithPrompts(
|
|
881
|
+
invocation: CliInvocation,
|
|
882
|
+
runtime: RuntimeClient,
|
|
883
|
+
operation: RuntimeOperationDefinition,
|
|
884
|
+
args: string[],
|
|
885
|
+
): Promise<ParsedOperationInput> {
|
|
886
|
+
const parsed = parseOperationArgs(operation, args, {
|
|
887
|
+
allowMissingRequired: !wantsJson(invocation) && canPrompt(),
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
if (
|
|
891
|
+
operation.createsWorkspace &&
|
|
892
|
+
parsed.input.name === undefined &&
|
|
893
|
+
!wantsJson(invocation) &&
|
|
894
|
+
canPrompt()
|
|
895
|
+
) {
|
|
896
|
+
parsed.input.name = await promptWorkspaceName(await defaultWorkspaceName(runtime));
|
|
897
|
+
}
|
|
898
|
+
if (operation.createsWorkspace && parsed.input.name !== undefined) {
|
|
899
|
+
assertValidWorkspaceName(parsed.input.name);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
enforceRequiredOperationInputs(operation, parsed);
|
|
903
|
+
return parsed;
|
|
819
904
|
}
|
|
820
905
|
|
|
821
|
-
function parseOperationArgs(
|
|
906
|
+
function parseOperationArgs(
|
|
907
|
+
operation: RuntimeOperationDefinition,
|
|
908
|
+
args: string[],
|
|
909
|
+
options: { allowMissingRequired?: boolean } = {},
|
|
910
|
+
): ParsedOperationInput {
|
|
822
911
|
const cli = inferCliMetadata(operation);
|
|
823
912
|
const input: Record<string, unknown> = {};
|
|
824
913
|
const hostOptions: Record<string, unknown> = {};
|
|
@@ -857,19 +946,27 @@ function parseOperationArgs(operation: RuntimeOperationDefinition, args: string[
|
|
|
857
946
|
assignPositional(positionals, positionalIndex++, arg, input);
|
|
858
947
|
}
|
|
859
948
|
|
|
949
|
+
if (!options.allowMissingRequired) enforceRequiredOperationInputs(operation, { input, hostOptions });
|
|
950
|
+
|
|
951
|
+
return { input, hostOptions };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function enforceRequiredOperationInputs(
|
|
955
|
+
operation: RuntimeOperationDefinition,
|
|
956
|
+
parsed: ParsedOperationInput,
|
|
957
|
+
): void {
|
|
958
|
+
const cli = inferCliMetadata(operation);
|
|
860
959
|
for (const option of cli.options ?? []) {
|
|
861
|
-
if (option.required && input[option.name] === undefined && hostOptions[option.name] === undefined) {
|
|
960
|
+
if (option.required && parsed.input[option.name] === undefined && parsed.hostOptions[option.name] === undefined) {
|
|
862
961
|
throw new Error(`Operation ${operation.id} requires ${option.flag}`);
|
|
863
962
|
}
|
|
864
963
|
}
|
|
865
964
|
|
|
866
965
|
for (const name of operation.inputSchema?.required ?? []) {
|
|
867
|
-
if (input[name] === undefined) {
|
|
966
|
+
if (parsed.input[name] === undefined) {
|
|
868
967
|
throw new Error(`Operation ${operation.id} requires ${name}`);
|
|
869
968
|
}
|
|
870
969
|
}
|
|
871
|
-
|
|
872
|
-
return { input, hostOptions };
|
|
873
970
|
}
|
|
874
971
|
|
|
875
972
|
function enforceHostOnlyBooleanGuards(
|
|
@@ -1072,7 +1169,10 @@ async function runHelp(invocation: CliInvocation): Promise<void> {
|
|
|
1072
1169
|
commands: [
|
|
1073
1170
|
{ name: "help", description: "Show Rigkit CLI help" },
|
|
1074
1171
|
{ name: "init", description: "Initialize a Rigkit project" },
|
|
1075
|
-
{ name: "
|
|
1172
|
+
{ name: "plan", description: "Plan project workflow changes" },
|
|
1173
|
+
{ name: "apply", description: "Apply project workflow changes" },
|
|
1174
|
+
{ name: "create", description: "Create a workspace" },
|
|
1175
|
+
{ name: "run", description: "Run a workspace operation" },
|
|
1076
1176
|
{ name: "ls", description: "List project workspaces" },
|
|
1077
1177
|
{ name: "projects", description: "Discover Rigkit projects below the current directory" },
|
|
1078
1178
|
{ name: "doctor", description: "Show Rigkit runtime diagnostics" },
|
|
@@ -1086,12 +1186,15 @@ async function runHelp(invocation: CliInvocation): Promise<void> {
|
|
|
1086
1186
|
`rig ${RIGKIT_CLI_VERSION}`,
|
|
1087
1187
|
"",
|
|
1088
1188
|
"Usage:",
|
|
1089
|
-
" rig [options] <command
|
|
1189
|
+
" rig [options] <command>",
|
|
1090
1190
|
"",
|
|
1091
1191
|
"Commands:",
|
|
1092
1192
|
" help Show Rigkit CLI help",
|
|
1093
1193
|
" init Initialize a Rigkit project",
|
|
1094
|
-
"
|
|
1194
|
+
" plan Plan project workflow changes",
|
|
1195
|
+
" apply Apply project workflow changes",
|
|
1196
|
+
" create Create a workspace",
|
|
1197
|
+
" run Run a workspace operation",
|
|
1095
1198
|
" ls List project workspaces",
|
|
1096
1199
|
" projects Discover Rigkit projects below the current directory",
|
|
1097
1200
|
" doctor Show Rigkit runtime diagnostics",
|
|
@@ -1773,6 +1876,11 @@ function printJson(value: unknown): void {
|
|
|
1773
1876
|
console.log(JSON.stringify(value, null, 2));
|
|
1774
1877
|
}
|
|
1775
1878
|
|
|
1879
|
+
function printInteractiveOutputGap(invocation: CliInvocation): void {
|
|
1880
|
+
if (wantsJson(invocation) || !process.stdout.isTTY) return;
|
|
1881
|
+
console.log("");
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1776
1884
|
function printPlan(plan: WorkflowPlan): void {
|
|
1777
1885
|
console.log(`${plan.workflow}: ${plan.cachedNodeCount}/${plan.nodeCount} nodes cached`);
|
|
1778
1886
|
|
package/src/completion.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { projectIdFor, runtimeFingerprintFor, runtimePaths, SUPPORTED_RUNTIME_API_VERSION } from "@rigkit/runtime-client";
|
|
6
|
-
import { completeRig, formatCompletionItems, renderCompletionScript } from "./completion.ts";
|
|
6
|
+
import { completeRig, formatCompletionItems, formatWorkspaceAge, renderCompletionScript } from "./completion.ts";
|
|
7
7
|
|
|
8
8
|
describe("CLI completion", () => {
|
|
9
9
|
test("completes workspace targets from the runtime", async () => {
|
|
@@ -11,12 +11,12 @@ describe("CLI completion", () => {
|
|
|
11
11
|
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
12
12
|
const items = await completeRig({
|
|
13
13
|
cwd: projectDir,
|
|
14
|
-
words: ["rig", "
|
|
14
|
+
words: ["rig", "run", ""],
|
|
15
15
|
currentIndex: 2,
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
expect(items.map((item) => item.value)).toEqual(["api", "web"]);
|
|
19
|
-
expect(items[0]?.description).toBe("smoke");
|
|
19
|
+
expect(items[0]?.description).toBe("smoke - 2h old");
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -25,7 +25,7 @@ describe("CLI completion", () => {
|
|
|
25
25
|
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
26
26
|
const items = await completeRig({
|
|
27
27
|
cwd: projectDir,
|
|
28
|
-
words: ["rig", "
|
|
28
|
+
words: ["rig", "run", "vm-"],
|
|
29
29
|
currentIndex: 2,
|
|
30
30
|
});
|
|
31
31
|
|
|
@@ -39,7 +39,7 @@ describe("CLI completion", () => {
|
|
|
39
39
|
await withWorkspaceRuntime({ projectDir, cleanupDir: parentDir }, async () => {
|
|
40
40
|
const items = await completeRig({
|
|
41
41
|
cwd: parentDir,
|
|
42
|
-
words: ["rig", "-C", "project", "
|
|
42
|
+
words: ["rig", "-C", "project", "run", ""],
|
|
43
43
|
currentIndex: 4,
|
|
44
44
|
});
|
|
45
45
|
|
|
@@ -55,15 +55,42 @@ describe("CLI completion", () => {
|
|
|
55
55
|
words: ["rig", "run", ""],
|
|
56
56
|
currentIndex: 2,
|
|
57
57
|
});
|
|
58
|
-
expect(roots.map((item) => item.value)).
|
|
59
|
-
expect(roots.
|
|
58
|
+
expect(roots.map((item) => item.value)).toEqual(["api", "web"]);
|
|
59
|
+
expect(roots[0]).toMatchObject({ description: "smoke - 2h old" });
|
|
60
60
|
|
|
61
|
-
const
|
|
61
|
+
const exactWorkspace = await completeRig({
|
|
62
62
|
cwd: projectDir,
|
|
63
|
-
words: ["rig", "run", "api
|
|
63
|
+
words: ["rig", "run", "api"],
|
|
64
64
|
currentIndex: 2,
|
|
65
65
|
});
|
|
66
|
-
expect(
|
|
66
|
+
expect(exactWorkspace.map((item) => item.value)).toEqual(["api"]);
|
|
67
|
+
|
|
68
|
+
const workspaceAfterSpace = await completeRig({
|
|
69
|
+
cwd: projectDir,
|
|
70
|
+
words: ["rig", "run", "api", ""],
|
|
71
|
+
currentIndex: 3,
|
|
72
|
+
});
|
|
73
|
+
expect(workspaceAfterSpace.map((item) => item.value)).toEqual(["remove", "open-cmux"]);
|
|
74
|
+
|
|
75
|
+
const operationPrefix = await completeRig({
|
|
76
|
+
cwd: projectDir,
|
|
77
|
+
words: ["rig", "run", "api", "open"],
|
|
78
|
+
currentIndex: 3,
|
|
79
|
+
});
|
|
80
|
+
expect(operationPrefix.map((item) => item.value)).toEqual(["open-cmux"]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("completes top-level project commands at the root command position", async () => {
|
|
85
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
|
|
86
|
+
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
87
|
+
const items = await completeRig({
|
|
88
|
+
cwd: projectDir,
|
|
89
|
+
words: ["rig", "p"],
|
|
90
|
+
currentIndex: 1,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(items.map((item) => item.value)).toEqual(["plan", "projects"]);
|
|
67
94
|
});
|
|
68
95
|
});
|
|
69
96
|
|
|
@@ -72,7 +99,21 @@ describe("CLI completion", () => {
|
|
|
72
99
|
|
|
73
100
|
expect(formatCompletionItems(items, "bash")).toBe("api");
|
|
74
101
|
expect(formatCompletionItems(items, "zsh")).toBe("api\tvm-api");
|
|
102
|
+
expect(formatCompletionItems([{ value: "api", description: "workspace smoke", noSpace: true }], "zsh"))
|
|
103
|
+
.toBe("api\tworkspace smoke\tnospace");
|
|
75
104
|
expect(renderCompletionScript("zsh")).toContain("rig __complete");
|
|
105
|
+
expect(renderCompletionScript("zsh")).toContain("compadd -S ''");
|
|
106
|
+
expect(renderCompletionScript("zsh")).toContain('display="${value} -- ${description}"');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("formats workspace ages", () => {
|
|
110
|
+
const now = Date.parse("2026-05-14T12:00:00.000Z");
|
|
111
|
+
|
|
112
|
+
expect(formatWorkspaceAge("2026-05-14T11:59:45.000Z", now)).toBe("just now");
|
|
113
|
+
expect(formatWorkspaceAge("2026-05-14T11:30:00.000Z", now)).toBe("30m old");
|
|
114
|
+
expect(formatWorkspaceAge("2026-05-14T09:00:00.000Z", now)).toBe("3h old");
|
|
115
|
+
expect(formatWorkspaceAge("2026-05-11T12:00:00.000Z", now)).toBe("3d old");
|
|
116
|
+
expect(formatWorkspaceAge("not-a-date", now)).toBeUndefined();
|
|
76
117
|
});
|
|
77
118
|
|
|
78
119
|
test("completes ls targets", async () => {
|
|
@@ -124,7 +165,10 @@ async function withWorkspaceRuntime(
|
|
|
124
165
|
});
|
|
125
166
|
}
|
|
126
167
|
if (pathname === "/workspaces") {
|
|
127
|
-
const
|
|
168
|
+
const nowMs = Date.now();
|
|
169
|
+
const apiCreatedAt = new Date(nowMs - 2 * 60 * 60 * 1000).toISOString();
|
|
170
|
+
const webCreatedAt = new Date(nowMs - 5 * 60 * 1000).toISOString();
|
|
171
|
+
const updatedAt = new Date(nowMs).toISOString();
|
|
128
172
|
return runtimeJson({
|
|
129
173
|
workspaces: [
|
|
130
174
|
{
|
|
@@ -132,16 +176,16 @@ async function withWorkspaceRuntime(
|
|
|
132
176
|
name: "api",
|
|
133
177
|
workflow: "smoke",
|
|
134
178
|
ctx: {},
|
|
135
|
-
createdAt:
|
|
136
|
-
updatedAt
|
|
179
|
+
createdAt: apiCreatedAt,
|
|
180
|
+
updatedAt,
|
|
137
181
|
},
|
|
138
182
|
{
|
|
139
183
|
id: "workspace-web",
|
|
140
184
|
name: "web",
|
|
141
185
|
workflow: "smoke",
|
|
142
186
|
ctx: {},
|
|
143
|
-
createdAt:
|
|
144
|
-
updatedAt
|
|
187
|
+
createdAt: webCreatedAt,
|
|
188
|
+
updatedAt,
|
|
145
189
|
},
|
|
146
190
|
],
|
|
147
191
|
});
|
package/src/completion.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type CompletionShell = "bash" | "fish" | "zsh";
|
|
|
6
6
|
export type CompletionItem = {
|
|
7
7
|
value: string;
|
|
8
8
|
description?: string;
|
|
9
|
+
noSpace?: boolean;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
type CompleteRigInput = {
|
|
@@ -17,7 +18,10 @@ type CompleteRigInput = {
|
|
|
17
18
|
const COMMANDS: CompletionItem[] = [
|
|
18
19
|
{ value: "help", description: "show CLI help" },
|
|
19
20
|
{ value: "init", description: "initialize a Rigkit project" },
|
|
20
|
-
{ value: "
|
|
21
|
+
{ value: "plan", description: "plan project workflow changes" },
|
|
22
|
+
{ value: "apply", description: "apply project workflow changes" },
|
|
23
|
+
{ value: "create", description: "create a workspace" },
|
|
24
|
+
{ value: "run", description: "run a workspace operation" },
|
|
21
25
|
{ value: "ls", description: "list project workspaces" },
|
|
22
26
|
{ value: "projects", description: "discover Rigkit projects" },
|
|
23
27
|
{ value: "doctor", description: "show runtime diagnostics" },
|
|
@@ -45,11 +49,22 @@ const COMMAND_OPTIONS: Record<string, CompletionItem[]> = {
|
|
|
45
49
|
{ value: "--force", description: "overwrite existing config" },
|
|
46
50
|
{ value: "--json", description: "print JSON" },
|
|
47
51
|
],
|
|
48
|
-
|
|
52
|
+
plan: [
|
|
49
53
|
{ value: "--all", description: "run against every discovered project" },
|
|
50
54
|
{ value: "--discover", description: "discover projects below the selected directory" },
|
|
51
55
|
{ value: "--json", description: "print JSON" },
|
|
52
56
|
],
|
|
57
|
+
apply: [
|
|
58
|
+
{ value: "--all", description: "run against every discovered project" },
|
|
59
|
+
{ value: "--discover", description: "discover projects below the selected directory" },
|
|
60
|
+
{ value: "--json", description: "print JSON" },
|
|
61
|
+
],
|
|
62
|
+
create: [
|
|
63
|
+
{ value: "--json", description: "print JSON" },
|
|
64
|
+
],
|
|
65
|
+
run: [
|
|
66
|
+
{ value: "--json", description: "print JSON" },
|
|
67
|
+
],
|
|
53
68
|
ls: [
|
|
54
69
|
{ value: "workspaces", description: "list workspaces" },
|
|
55
70
|
{ value: "snapshots", description: "list snapshots" },
|
|
@@ -66,6 +81,8 @@ const COMMAND_OPTIONS: Record<string, CompletionItem[]> = {
|
|
|
66
81
|
],
|
|
67
82
|
};
|
|
68
83
|
|
|
84
|
+
const PROJECT_OPERATION_COMMANDS = new Set(["plan", "apply", "create"]);
|
|
85
|
+
|
|
69
86
|
const OPTIONS_WITH_VALUES = new Set([
|
|
70
87
|
"-C",
|
|
71
88
|
"--project",
|
|
@@ -91,6 +108,13 @@ type RuntimeOperationDefinition = {
|
|
|
91
108
|
};
|
|
92
109
|
};
|
|
93
110
|
|
|
111
|
+
type RuntimeWorkspaceCompletion = {
|
|
112
|
+
name: string;
|
|
113
|
+
workflow: string;
|
|
114
|
+
createdAt: string;
|
|
115
|
+
updatedAt: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
94
118
|
export async function completeRig(input: CompleteRigInput): Promise<CompletionItem[]> {
|
|
95
119
|
const cwd = input.cwd ?? process.cwd();
|
|
96
120
|
const words = input.words.length > 0 ? input.words : ["rig"];
|
|
@@ -102,66 +126,49 @@ export async function completeRig(input: CompleteRigInput): Promise<CompletionIt
|
|
|
102
126
|
if (expectsOptionValue(before)) return [];
|
|
103
127
|
|
|
104
128
|
if (!command) {
|
|
105
|
-
const rootOperation = parseRootOperation(before);
|
|
106
|
-
if (rootOperation.operation) {
|
|
107
|
-
if (current.startsWith("-")) {
|
|
108
|
-
const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), rootOperation.operation);
|
|
109
|
-
return filterItems([
|
|
110
|
-
...(operation?.cli?.options ?? []).flatMap((option) => [
|
|
111
|
-
{ value: option.flag, description: option.name },
|
|
112
|
-
...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name })),
|
|
113
|
-
]),
|
|
114
|
-
...COMMAND_OPTIONS.operation,
|
|
115
|
-
...GLOBAL_OPTIONS,
|
|
116
|
-
], current);
|
|
117
|
-
}
|
|
118
|
-
const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), rootOperation.operation);
|
|
119
|
-
const operationPositionalCount = countRunOperationPositionals(rootOperation.args);
|
|
120
|
-
const positional = operation?.cli?.positionals?.find((item) => item.index === operationPositionalCount);
|
|
121
|
-
if (positional && /workspace|vm/i.test(positional.name)) {
|
|
122
|
-
return filterItems(await workspaceTargets(resolveProjectDir(words, cwd), current, /vm/i.test(positional.name)), current);
|
|
123
|
-
}
|
|
124
|
-
return [];
|
|
125
|
-
}
|
|
126
129
|
return filterItems(
|
|
127
130
|
current.startsWith("-")
|
|
128
131
|
? GLOBAL_OPTIONS
|
|
129
|
-
: [...COMMANDS, ...
|
|
132
|
+
: [...COMMANDS, ...GLOBAL_OPTIONS],
|
|
130
133
|
current,
|
|
131
134
|
);
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
if (current.startsWith("-")) {
|
|
135
138
|
if (command === "run") {
|
|
136
|
-
const run =
|
|
137
|
-
if (run.operation) {
|
|
138
|
-
const operation = await
|
|
139
|
+
const run = parseWorkspaceRunCommand(before);
|
|
140
|
+
if (run.workspace && run.operation) {
|
|
141
|
+
const operation = await safeResolveWorkspaceOperation(resolveProjectDir(words, cwd), run.operation);
|
|
139
142
|
return filterItems([
|
|
140
143
|
...(operation?.cli?.options ?? []).flatMap((option) => [
|
|
141
144
|
{ value: option.flag, description: option.name },
|
|
142
145
|
...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name })),
|
|
143
146
|
]),
|
|
144
|
-
...COMMAND_OPTIONS.
|
|
147
|
+
...COMMAND_OPTIONS.run,
|
|
145
148
|
...GLOBAL_OPTIONS,
|
|
146
149
|
], current);
|
|
147
150
|
}
|
|
148
151
|
}
|
|
152
|
+
if (PROJECT_OPERATION_COMMANDS.has(command)) {
|
|
153
|
+
const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), command);
|
|
154
|
+
return filterItems([
|
|
155
|
+
...(operation?.cli?.options ?? []).flatMap((option) => [
|
|
156
|
+
{ value: option.flag, description: option.name },
|
|
157
|
+
...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name })),
|
|
158
|
+
]),
|
|
159
|
+
...(COMMAND_OPTIONS[command] ?? []),
|
|
160
|
+
...GLOBAL_OPTIONS,
|
|
161
|
+
], current);
|
|
162
|
+
}
|
|
149
163
|
return filterItems([...(COMMAND_OPTIONS[command] ?? []), ...GLOBAL_OPTIONS], current);
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
const positionalCount = countPositionals(before, command);
|
|
153
167
|
|
|
154
168
|
if (command === "run") {
|
|
155
|
-
const run =
|
|
156
|
-
if (!run.
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), run.operation);
|
|
160
|
-
const operationPositionalCount = countRunOperationPositionals(run.args);
|
|
161
|
-
const positional = operation?.cli?.positionals?.find((item) => item.index === operationPositionalCount);
|
|
162
|
-
if (positional && /workspace|vm/i.test(positional.name)) {
|
|
163
|
-
return filterItems(await workspaceTargets(resolveProjectDir(words, cwd), current, /vm/i.test(positional.name)), current);
|
|
164
|
-
}
|
|
169
|
+
const run = parseWorkspaceRunCommand(before);
|
|
170
|
+
if (!run.workspace) return filterItems(await workspaceTargets(resolveProjectDir(words, cwd)), current);
|
|
171
|
+
if (!run.operation) return filterItems(await safeWorkspaceOperationTargets(resolveProjectDir(words, cwd)), current);
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
if (command === "completion" && positionalCount === 0) {
|
|
@@ -178,6 +185,9 @@ export async function completeRig(input: CompleteRigInput): Promise<CompletionIt
|
|
|
178
185
|
export function formatCompletionItems(items: CompletionItem[], shell: CompletionShell): string {
|
|
179
186
|
const lines = items.map((item) => {
|
|
180
187
|
if (shell === "bash") return item.value;
|
|
188
|
+
if (shell === "zsh" && item.noSpace) {
|
|
189
|
+
return `${item.value}\t${item.description ?? ""}\tnospace`;
|
|
190
|
+
}
|
|
181
191
|
return item.description ? `${item.value}\t${item.description}` : item.value;
|
|
182
192
|
});
|
|
183
193
|
return lines.join("\n");
|
|
@@ -217,22 +227,37 @@ complete -c rig -f -a "(__rig_complete)"
|
|
|
217
227
|
`;
|
|
218
228
|
}
|
|
219
229
|
|
|
220
|
-
|
|
230
|
+
return `#compdef rig
|
|
221
231
|
# rig zsh completion
|
|
222
232
|
_rig() {
|
|
223
|
-
local -a raw
|
|
224
|
-
local line value description
|
|
233
|
+
local -a raw values displays nospace_values nospace_displays
|
|
234
|
+
local line value description rest marker display
|
|
225
235
|
raw=("\${(@f)$(command rig __complete --shell zsh --index $((CURRENT - 1)) -- "\${words[@]}" 2>/dev/null)}")
|
|
226
236
|
for line in "\${raw[@]}"; do
|
|
227
237
|
value="\${line%%$'\\t'*}"
|
|
238
|
+
description=""
|
|
239
|
+
marker=""
|
|
228
240
|
if [[ "$line" == *$'\\t'* ]]; then
|
|
229
|
-
|
|
230
|
-
|
|
241
|
+
rest="\${line#*$'\\t'}"
|
|
242
|
+
description="\${rest%%$'\\t'*}"
|
|
243
|
+
if [[ "$rest" == *$'\\t'* ]]; then
|
|
244
|
+
marker="\${rest#*$'\\t'}"
|
|
245
|
+
fi
|
|
246
|
+
fi
|
|
247
|
+
display="\${value}"
|
|
248
|
+
if [[ -n "$description" ]]; then
|
|
249
|
+
display="\${value} -- \${description}"
|
|
250
|
+
fi
|
|
251
|
+
if [[ "$marker" == "nospace" ]]; then
|
|
252
|
+
nospace_values+=("\${value}")
|
|
253
|
+
nospace_displays+=("\${display}")
|
|
231
254
|
else
|
|
232
|
-
|
|
255
|
+
values+=("\${value}")
|
|
256
|
+
displays+=("\${display}")
|
|
233
257
|
fi
|
|
234
258
|
done
|
|
235
|
-
|
|
259
|
+
(( \${#nospace_values} )) && compadd -S '' -d nospace_displays -a nospace_values
|
|
260
|
+
(( \${#values} )) && compadd -d displays -a values
|
|
236
261
|
}
|
|
237
262
|
compdef _rig rig
|
|
238
263
|
`;
|
|
@@ -278,7 +303,7 @@ function countPositionals(words: string[], command: string): number {
|
|
|
278
303
|
return count;
|
|
279
304
|
}
|
|
280
305
|
|
|
281
|
-
function
|
|
306
|
+
function parseWorkspaceRunCommand(words: string[]): { workspace?: string; operation?: string; args: string[] } {
|
|
282
307
|
let foundRun = false;
|
|
283
308
|
const args: string[] = [];
|
|
284
309
|
for (let index = 0; index < words.length; index += 1) {
|
|
@@ -295,37 +320,7 @@ function parseRunCommand(words: string[]): { operation?: string; args: string[]
|
|
|
295
320
|
}
|
|
296
321
|
args.push(word);
|
|
297
322
|
}
|
|
298
|
-
return {
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function parseRootOperation(words: string[]): { operation?: string; args: string[] } {
|
|
302
|
-
const args: string[] = [];
|
|
303
|
-
for (let index = 0; index < words.length; index += 1) {
|
|
304
|
-
const word = words[index]!;
|
|
305
|
-
if (OPTIONS_WITH_VALUES.has(word)) {
|
|
306
|
-
index += 1;
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
if (word.startsWith("--") && word.includes("=")) continue;
|
|
310
|
-
if (word.startsWith("-")) continue;
|
|
311
|
-
args.push(word);
|
|
312
|
-
}
|
|
313
|
-
return { operation: args[0], args: args.slice(1) };
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function countRunOperationPositionals(args: string[]): number {
|
|
317
|
-
let count = 0;
|
|
318
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
319
|
-
const arg = args[index]!;
|
|
320
|
-
if (OPTIONS_WITH_VALUES.has(arg)) {
|
|
321
|
-
index += 1;
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
if (arg.startsWith("--") && arg.includes("=")) continue;
|
|
325
|
-
if (arg.startsWith("-")) continue;
|
|
326
|
-
count += 1;
|
|
327
|
-
}
|
|
328
|
-
return count;
|
|
323
|
+
return { workspace: args[0], operation: args[1], args: args.slice(2) };
|
|
329
324
|
}
|
|
330
325
|
|
|
331
326
|
function expectsOptionValue(words: string[]): boolean {
|
|
@@ -362,92 +357,85 @@ function projectPaths(projectDir: string): { projectDir: string; configPath: str
|
|
|
362
357
|
|
|
363
358
|
async function workspaceTargets(
|
|
364
359
|
paths: { projectDir: string; configPath: string },
|
|
365
|
-
_current: string,
|
|
366
|
-
_includeVmIds: boolean,
|
|
367
360
|
): Promise<CompletionItem[]> {
|
|
368
361
|
const workspaces = await readWorkspaces(paths);
|
|
369
362
|
const items = workspaces.map((workspace) => ({
|
|
370
363
|
value: workspace.name,
|
|
371
|
-
description: workspace
|
|
364
|
+
description: workspaceDescription(workspace, { prefix: false }),
|
|
372
365
|
}));
|
|
373
366
|
|
|
374
367
|
return dedupeItems(items);
|
|
375
368
|
}
|
|
376
369
|
|
|
377
|
-
async function readWorkspaces(paths: { projectDir: string; configPath: string }): Promise<
|
|
370
|
+
async function readWorkspaces(paths: { projectDir: string; configPath: string }): Promise<RuntimeWorkspaceCompletion[]> {
|
|
378
371
|
const runtime = await getOrStartRuntime(paths);
|
|
379
372
|
const { workspaces } = await runtime.control.workspaces();
|
|
380
373
|
return workspaces.map((workspace) => ({
|
|
381
374
|
name: workspace.name,
|
|
382
375
|
workflow: workspace.workflow,
|
|
376
|
+
createdAt: workspace.createdAt,
|
|
377
|
+
updatedAt: workspace.updatedAt,
|
|
383
378
|
}));
|
|
384
379
|
}
|
|
385
380
|
|
|
386
|
-
async function
|
|
381
|
+
async function safeWorkspaceOperationTargets(
|
|
387
382
|
paths: { projectDir: string; configPath: string },
|
|
388
|
-
current: string,
|
|
389
383
|
): Promise<CompletionItem[]> {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return workspaceOperationTargets(manifest
|
|
384
|
+
try {
|
|
385
|
+
const manifest = await readOperations(paths);
|
|
386
|
+
return workspaceOperationTargets(manifest);
|
|
387
|
+
} catch {
|
|
388
|
+
return [];
|
|
393
389
|
}
|
|
394
|
-
const workspaces = await readWorkspaces(paths).catch(() => []);
|
|
395
|
-
return manifest.operations.flatMap((operation) => [
|
|
396
|
-
{ value: operation.id, description: operation.description },
|
|
397
|
-
...(operation.aliases ?? []).map((alias) => ({ value: alias, description: operation.description })),
|
|
398
|
-
]).concat(workspaces.map((workspace) => ({
|
|
399
|
-
value: `${workspace.name}/`,
|
|
400
|
-
description: `workspace ${workspace.workflow}`,
|
|
401
|
-
})));
|
|
402
390
|
}
|
|
403
391
|
|
|
404
|
-
function workspaceOperationTargets(manifest: RuntimeOperationManifest
|
|
405
|
-
const slash = current.indexOf("/");
|
|
406
|
-
if (slash < 0) return [];
|
|
407
|
-
const workspace = current.slice(0, slash);
|
|
408
|
-
if (!workspace) return [];
|
|
392
|
+
function workspaceOperationTargets(manifest: RuntimeOperationManifest): CompletionItem[] {
|
|
409
393
|
return (manifest.workspaceOperations ?? []).flatMap((operation) => [
|
|
410
|
-
{ value:
|
|
394
|
+
{ value: operation.id, description: operation.description ?? "workspace operation" },
|
|
411
395
|
...(operation.aliases ?? []).map((alias) => ({
|
|
412
|
-
value:
|
|
396
|
+
value: alias,
|
|
413
397
|
description: operation.description ?? "workspace operation",
|
|
414
398
|
})),
|
|
415
399
|
]);
|
|
416
400
|
}
|
|
417
401
|
|
|
418
|
-
async function
|
|
402
|
+
async function resolveRuntimeOperation(
|
|
419
403
|
paths: { projectDir: string; configPath: string },
|
|
420
|
-
|
|
421
|
-
): Promise<
|
|
404
|
+
operationId: string,
|
|
405
|
+
): Promise<RuntimeOperationDefinition | undefined> {
|
|
406
|
+
const manifest = await readOperations(paths);
|
|
407
|
+
return manifest.operations.find((operation) =>
|
|
408
|
+
operation.id === operationId || operation.aliases?.includes(operationId)
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function safeResolveRuntimeOperation(
|
|
413
|
+
paths: { projectDir: string; configPath: string },
|
|
414
|
+
operationId: string,
|
|
415
|
+
): Promise<RuntimeOperationDefinition | undefined> {
|
|
422
416
|
try {
|
|
423
|
-
return await
|
|
417
|
+
return await resolveRuntimeOperation(paths, operationId);
|
|
424
418
|
} catch {
|
|
425
|
-
return
|
|
419
|
+
return undefined;
|
|
426
420
|
}
|
|
427
421
|
}
|
|
428
422
|
|
|
429
|
-
async function
|
|
423
|
+
async function resolveWorkspaceOperation(
|
|
430
424
|
paths: { projectDir: string; configPath: string },
|
|
431
425
|
operationId: string,
|
|
432
426
|
): Promise<RuntimeOperationDefinition | undefined> {
|
|
433
427
|
const manifest = await readOperations(paths);
|
|
434
|
-
|
|
435
|
-
if (workspaceOperation) {
|
|
436
|
-
return (manifest.workspaceOperations ?? []).find((operation) =>
|
|
437
|
-
operation.id === workspaceOperation.operation || operation.aliases?.includes(workspaceOperation.operation)
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
return manifest.operations.find((operation) =>
|
|
428
|
+
return (manifest.workspaceOperations ?? []).find((operation) =>
|
|
441
429
|
operation.id === operationId || operation.aliases?.includes(operationId)
|
|
442
430
|
);
|
|
443
431
|
}
|
|
444
432
|
|
|
445
|
-
async function
|
|
433
|
+
async function safeResolveWorkspaceOperation(
|
|
446
434
|
paths: { projectDir: string; configPath: string },
|
|
447
435
|
operationId: string,
|
|
448
436
|
): Promise<RuntimeOperationDefinition | undefined> {
|
|
449
437
|
try {
|
|
450
|
-
return await
|
|
438
|
+
return await resolveWorkspaceOperation(paths, operationId);
|
|
451
439
|
} catch {
|
|
452
440
|
return undefined;
|
|
453
441
|
}
|
|
@@ -458,15 +446,6 @@ async function readOperations(paths: { projectDir: string; configPath: string })
|
|
|
458
446
|
return await runtime.control.operations() as unknown as RuntimeOperationManifest;
|
|
459
447
|
}
|
|
460
448
|
|
|
461
|
-
function parseWorkspaceOperationId(value: string): { workspace: string; operation: string } | undefined {
|
|
462
|
-
const slash = value.indexOf("/");
|
|
463
|
-
if (slash <= 0 || slash === value.length - 1) return undefined;
|
|
464
|
-
return {
|
|
465
|
-
workspace: value.slice(0, slash),
|
|
466
|
-
operation: value.slice(slash + 1),
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
|
|
470
449
|
function filterItems(items: CompletionItem[], current: string): CompletionItem[] {
|
|
471
450
|
return dedupeItems(items).filter((item) => item.value.startsWith(current));
|
|
472
451
|
}
|
|
@@ -481,3 +460,31 @@ function dedupeItems(items: CompletionItem[]): CompletionItem[] {
|
|
|
481
460
|
}
|
|
482
461
|
return deduped;
|
|
483
462
|
}
|
|
463
|
+
|
|
464
|
+
function workspaceDescription(workspace: RuntimeWorkspaceCompletion, options: { prefix: boolean }): string {
|
|
465
|
+
const label = options.prefix ? `workspace ${workspace.workflow}` : workspace.workflow;
|
|
466
|
+
const age = formatWorkspaceAge(workspace.createdAt);
|
|
467
|
+
return age ? `${label} - ${age}` : label;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function formatWorkspaceAge(createdAt: string, nowMs = Date.now()): string | undefined {
|
|
471
|
+
const createdAtMs = Date.parse(createdAt);
|
|
472
|
+
if (!Number.isFinite(createdAtMs)) return undefined;
|
|
473
|
+
|
|
474
|
+
const elapsedSeconds = Math.max(0, Math.floor((nowMs - createdAtMs) / 1000));
|
|
475
|
+
if (elapsedSeconds < 60) return "just now";
|
|
476
|
+
|
|
477
|
+
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
478
|
+
if (elapsedMinutes < 60) return `${elapsedMinutes}m old`;
|
|
479
|
+
|
|
480
|
+
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
|
481
|
+
if (elapsedHours < 48) return `${elapsedHours}h old`;
|
|
482
|
+
|
|
483
|
+
const elapsedDays = Math.floor(elapsedHours / 24);
|
|
484
|
+
if (elapsedDays < 30) return `${elapsedDays}d old`;
|
|
485
|
+
|
|
486
|
+
const elapsedMonths = Math.floor(elapsedDays / 30);
|
|
487
|
+
if (elapsedMonths < 24) return `${elapsedMonths}mo old`;
|
|
488
|
+
|
|
489
|
+
return `${Math.floor(elapsedMonths / 12)}y old`;
|
|
490
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const RIGKIT_CLI_VERSION = "0.2.
|
|
1
|
+
export const RIGKIT_CLI_VERSION = "0.2.6";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { generateWorkspaceName } from "./workspace-name.ts";
|
|
3
|
+
|
|
4
|
+
describe("workspace name defaults", () => {
|
|
5
|
+
test("generates shell-safe adjective noun names", () => {
|
|
6
|
+
expect(generateWorkspaceName([], () => 0)).toBe("snowy-ridge");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("skips generated names that already exist", () => {
|
|
10
|
+
const randomValues = [0, 0, 0.5, 0.5];
|
|
11
|
+
let index = 0;
|
|
12
|
+
const name = generateWorkspaceName(["snowy-ridge"], () => randomValues[index++] ?? 0.5);
|
|
13
|
+
|
|
14
|
+
expect(name).not.toBe("snowy-ridge");
|
|
15
|
+
expect(name).toMatch(/^(?!-)[A-Za-z0-9._-]+$/);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const adjectives = [
|
|
2
|
+
"snowy",
|
|
3
|
+
"bright",
|
|
4
|
+
"quiet",
|
|
5
|
+
"rapid",
|
|
6
|
+
"silver",
|
|
7
|
+
"clear",
|
|
8
|
+
"steady",
|
|
9
|
+
"bold",
|
|
10
|
+
"lucky",
|
|
11
|
+
"fresh",
|
|
12
|
+
"golden",
|
|
13
|
+
"nimble",
|
|
14
|
+
"calm",
|
|
15
|
+
"brisk",
|
|
16
|
+
"sharp",
|
|
17
|
+
"sunny",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
const nouns = [
|
|
21
|
+
"ridge",
|
|
22
|
+
"harbor",
|
|
23
|
+
"signal",
|
|
24
|
+
"orbit",
|
|
25
|
+
"bridge",
|
|
26
|
+
"summit",
|
|
27
|
+
"field",
|
|
28
|
+
"grove",
|
|
29
|
+
"spark",
|
|
30
|
+
"stone",
|
|
31
|
+
"valley",
|
|
32
|
+
"meadow",
|
|
33
|
+
"trail",
|
|
34
|
+
"cove",
|
|
35
|
+
"anchor",
|
|
36
|
+
"beacon",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
export function generateWorkspaceName(
|
|
40
|
+
existingNames: Iterable<string> = [],
|
|
41
|
+
random: () => number = Math.random,
|
|
42
|
+
): string {
|
|
43
|
+
const existing = new Set(existingNames);
|
|
44
|
+
|
|
45
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
46
|
+
const name = `${pick(adjectives, random)}-${pick(nouns, random)}`;
|
|
47
|
+
if (!existing.has(name)) return name;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return `workspace-${Date.now().toString(36)}-${Math.floor(random() * 10000).toString(36)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pick<const Values extends readonly string[]>(
|
|
54
|
+
values: Values,
|
|
55
|
+
random: () => number,
|
|
56
|
+
): Values[number] {
|
|
57
|
+
const index = Math.min(values.length - 1, Math.floor(random() * values.length));
|
|
58
|
+
return values[index]!;
|
|
59
|
+
}
|