@rigkit/cli 0.2.5 → 0.2.7
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 +3 -3
- package/package.json +4 -4
- package/src/cli.test.ts +7 -5
- package/src/cli.ts +95 -68
- package/src/completion.test.ts +41 -25
- package/src/completion.ts +110 -106
- package/src/init.test.ts +3 -3
- package/src/init.ts +1 -1
- 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
|
@@ -5,9 +5,9 @@ Global `rig` CLI.
|
|
|
5
5
|
```bash
|
|
6
6
|
npm i -g @rigkit/cli
|
|
7
7
|
rig init
|
|
8
|
-
rig
|
|
8
|
+
rig plan
|
|
9
9
|
rig ls
|
|
10
|
-
rig
|
|
10
|
+
rig plan github:owner/repo
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
`rig init` asks for a project name, Freestyle API key, and package manager. It creates a project folder containing a workflow-based `rig.config.ts`, `.env`, `.env.example`, `package.json`, and local ignore rules.
|
|
@@ -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 run <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.7",
|
|
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/
|
|
29
|
-
"@rigkit/runtime-client": "0.2.
|
|
30
|
-
"@rigkit/
|
|
28
|
+
"@rigkit/provider-cmux": "0.2.7",
|
|
29
|
+
"@rigkit/runtime-client": "0.2.7",
|
|
30
|
+
"@rigkit/engine": "0.2.7"
|
|
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,7 +35,8 @@ 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
42
|
test("rejects operation shorthand at the root", async () => {
|
|
@@ -110,7 +112,7 @@ describe("CLI entrypoint", () => {
|
|
|
110
112
|
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-cli-create-name-"));
|
|
111
113
|
|
|
112
114
|
await withWorkspaceRuntime({ projectDir }, async ({ env }) => {
|
|
113
|
-
const result = await runCli(["-C", projectDir, "
|
|
115
|
+
const result = await runCli(["-C", projectDir, "create", "--name", "some workspace", "--json"], { env });
|
|
114
116
|
|
|
115
117
|
expect(result.exitCode).toBe(1);
|
|
116
118
|
expect(result.stdout).toBe("");
|
|
@@ -122,7 +124,7 @@ describe("CLI entrypoint", () => {
|
|
|
122
124
|
const cwd = mkdtempSync(join(tmpdir(), "rigkit-cli-run-all-"));
|
|
123
125
|
|
|
124
126
|
try {
|
|
125
|
-
const result = await runCli(["
|
|
127
|
+
const result = await runCli(["plan", "--all", "--json"], { cwd });
|
|
126
128
|
|
|
127
129
|
expect(result.exitCode).toBe(1);
|
|
128
130
|
expect(result.stdout).toBe("");
|
|
@@ -140,7 +142,7 @@ describe("CLI entrypoint", () => {
|
|
|
140
142
|
writeFileSync(join(cwd, "web", "rig.config.ts"), "export default {}\n");
|
|
141
143
|
|
|
142
144
|
try {
|
|
143
|
-
const result = await runCli(["
|
|
145
|
+
const result = await runCli(["plan", "--discover", "--json"], { cwd });
|
|
144
146
|
|
|
145
147
|
expect(result.exitCode).toBe(1);
|
|
146
148
|
expect(result.stdout).toBe("");
|
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;
|
|
@@ -196,22 +197,46 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
196
197
|
});
|
|
197
198
|
});
|
|
198
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
|
+
|
|
199
228
|
program
|
|
200
|
-
.command("run <operation> [args...]")
|
|
201
|
-
.description("Run a
|
|
229
|
+
.command("run <workspace> <operation> [args...]")
|
|
230
|
+
.description("Run a workspace operation")
|
|
202
231
|
.allowUnknownOption(true)
|
|
203
|
-
.option("--all", "Run against every discovered project")
|
|
204
|
-
.option("--discover", "Discover projects below the selected directory")
|
|
205
232
|
.option("--json", "Print machine-readable JSON")
|
|
206
233
|
.action(async (
|
|
234
|
+
workspace: string,
|
|
207
235
|
operation: string,
|
|
208
236
|
args: string[],
|
|
209
|
-
options: {
|
|
237
|
+
options: { json?: boolean },
|
|
210
238
|
) => {
|
|
211
|
-
await
|
|
212
|
-
all: Boolean(options.all),
|
|
213
|
-
discover: Boolean(options.discover),
|
|
214
|
-
});
|
|
239
|
+
await runWorkspaceOperation(makeInvocation(rootOptions(program), options.json), workspace, operation, args ?? []);
|
|
215
240
|
});
|
|
216
241
|
|
|
217
242
|
program
|
|
@@ -468,11 +493,12 @@ async function promptPackageManager(defaultValue: PackageManager): Promise<Packa
|
|
|
468
493
|
return answers.packageManager;
|
|
469
494
|
}
|
|
470
495
|
|
|
471
|
-
async function promptWorkspaceName(): Promise<string> {
|
|
496
|
+
async function promptWorkspaceName(defaultValue: string): Promise<string> {
|
|
472
497
|
const answers = await inquirer.prompt<{ name: string }>([{
|
|
473
498
|
type: "input",
|
|
474
499
|
name: "name",
|
|
475
500
|
message: "Workspace name:",
|
|
501
|
+
default: defaultValue,
|
|
476
502
|
validate(value: string) {
|
|
477
503
|
return validateWorkspaceName(value.trim());
|
|
478
504
|
},
|
|
@@ -497,21 +523,11 @@ function assertValidWorkspaceName(value: unknown): void {
|
|
|
497
523
|
if (valid !== true) throw new Error(`Invalid workspace name "${value}". ${valid}`);
|
|
498
524
|
}
|
|
499
525
|
|
|
500
|
-
async function
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
type: "select",
|
|
506
|
-
name: "operation",
|
|
507
|
-
message: `Operation for ${workspace}:`,
|
|
508
|
-
choices: operations.map((operation) => ({
|
|
509
|
-
name: operation.id,
|
|
510
|
-
value: operation.id,
|
|
511
|
-
description: operation.description || operation.title || "workspace operation",
|
|
512
|
-
})),
|
|
513
|
-
}]);
|
|
514
|
-
return answers.operation;
|
|
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);
|
|
515
531
|
}
|
|
516
532
|
|
|
517
533
|
async function runPackageManagerInstall(
|
|
@@ -595,7 +611,7 @@ function printInitResult(result: InitProjectResult, install: InitInstallResult):
|
|
|
595
611
|
if (install.skipped) {
|
|
596
612
|
console.log(` ${detectInstallCommand(result.packageJsonPath)}`);
|
|
597
613
|
}
|
|
598
|
-
console.log(" rig
|
|
614
|
+
console.log(" rig plan");
|
|
599
615
|
}
|
|
600
616
|
|
|
601
617
|
function displayProjectDir(projectDir: string): string {
|
|
@@ -667,6 +683,47 @@ async function runProjectOperation(
|
|
|
667
683
|
printInteractiveOutputGap(invocation);
|
|
668
684
|
}
|
|
669
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);
|
|
725
|
+
}
|
|
726
|
+
|
|
670
727
|
async function runDiscoveredProjectOperation(
|
|
671
728
|
invocation: CliInvocation,
|
|
672
729
|
requestedOperation: string,
|
|
@@ -791,13 +848,13 @@ async function executeRuntimeOperation(
|
|
|
791
848
|
result: unknown;
|
|
792
849
|
}> {
|
|
793
850
|
const manifest = await readRuntimeOperations(runtime);
|
|
794
|
-
const resolved =
|
|
851
|
+
const resolved = findRuntimeOperation(manifest, requestedOperation);
|
|
795
852
|
if (!resolved) {
|
|
796
853
|
throw new Error(`This project does not define a Rigkit operation named "${requestedOperation}".`);
|
|
797
854
|
}
|
|
798
855
|
const { operation, runOperation } = resolved;
|
|
799
856
|
|
|
800
|
-
const parsed = await parseOperationArgsWithPrompts(invocation, operation, args);
|
|
857
|
+
const parsed = await parseOperationArgsWithPrompts(invocation, runtime, operation, args);
|
|
801
858
|
enforceHostOnlyBooleanGuards(operation, parsed);
|
|
802
859
|
|
|
803
860
|
const result = await runRuntimeOperation<unknown>(
|
|
@@ -810,55 +867,19 @@ async function executeRuntimeOperation(
|
|
|
810
867
|
return { operation, parsed, result };
|
|
811
868
|
}
|
|
812
869
|
|
|
813
|
-
async function resolveRequestedRuntimeOperation(
|
|
814
|
-
invocation: CliInvocation,
|
|
815
|
-
runtime: RuntimeClient,
|
|
816
|
-
manifest: RuntimeOperationManifest,
|
|
817
|
-
requestedOperation: string,
|
|
818
|
-
): Promise<{ operation: RuntimeOperationDefinition; runOperation: string } | undefined> {
|
|
819
|
-
const resolved = findRuntimeOperation(manifest, requestedOperation);
|
|
820
|
-
if (resolved) return resolved;
|
|
821
|
-
|
|
822
|
-
if (requestedOperation.includes("/") || wantsJson(invocation) || !canPrompt()) return undefined;
|
|
823
|
-
|
|
824
|
-
const workspaces = await runtime.control.workspaces()
|
|
825
|
-
.then((response) => response.workspaces as WorkspaceRecord[])
|
|
826
|
-
.catch(() => []);
|
|
827
|
-
const workspace = workspaces.find((item) => item.name === requestedOperation);
|
|
828
|
-
if (!workspace || (manifest.workspaceOperations ?? []).length === 0) return undefined;
|
|
829
|
-
|
|
830
|
-
const operationId = await promptWorkspaceOperation(workspace.name, manifest.workspaceOperations ?? []);
|
|
831
|
-
const operation = (manifest.workspaceOperations ?? []).find((item) => item.id === operationId);
|
|
832
|
-
return operation ? { operation, runOperation: `${workspace.name}/${operation.id}` } : undefined;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
870
|
function findRuntimeOperation(
|
|
836
871
|
manifest: RuntimeOperationManifest,
|
|
837
872
|
requestedOperation: string,
|
|
838
873
|
): { operation: RuntimeOperationDefinition; runOperation: string } | undefined {
|
|
839
|
-
const workspaceOperation = parseWorkspaceOperationId(requestedOperation);
|
|
840
|
-
if (workspaceOperation) {
|
|
841
|
-
const operation = (manifest.workspaceOperations ?? []).find((item) => item.id === workspaceOperation.operation);
|
|
842
|
-
return operation ? { operation, runOperation: requestedOperation } : undefined;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
874
|
const operation = manifest.operations.find((operation) =>
|
|
846
875
|
operation.id === requestedOperation || operation.aliases?.includes(requestedOperation)
|
|
847
876
|
);
|
|
848
877
|
return operation ? { operation, runOperation: operation.id } : undefined;
|
|
849
878
|
}
|
|
850
879
|
|
|
851
|
-
function parseWorkspaceOperationId(value: string): { workspace: string; operation: string } | undefined {
|
|
852
|
-
const slash = value.indexOf("/");
|
|
853
|
-
if (slash <= 0 || slash === value.length - 1) return undefined;
|
|
854
|
-
return {
|
|
855
|
-
workspace: value.slice(0, slash),
|
|
856
|
-
operation: value.slice(slash + 1),
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
|
|
860
880
|
async function parseOperationArgsWithPrompts(
|
|
861
881
|
invocation: CliInvocation,
|
|
882
|
+
runtime: RuntimeClient,
|
|
862
883
|
operation: RuntimeOperationDefinition,
|
|
863
884
|
args: string[],
|
|
864
885
|
): Promise<ParsedOperationInput> {
|
|
@@ -872,7 +893,7 @@ async function parseOperationArgsWithPrompts(
|
|
|
872
893
|
!wantsJson(invocation) &&
|
|
873
894
|
canPrompt()
|
|
874
895
|
) {
|
|
875
|
-
parsed.input.name = await promptWorkspaceName();
|
|
896
|
+
parsed.input.name = await promptWorkspaceName(await defaultWorkspaceName(runtime));
|
|
876
897
|
}
|
|
877
898
|
if (operation.createsWorkspace && parsed.input.name !== undefined) {
|
|
878
899
|
assertValidWorkspaceName(parsed.input.name);
|
|
@@ -1148,7 +1169,10 @@ async function runHelp(invocation: CliInvocation): Promise<void> {
|
|
|
1148
1169
|
commands: [
|
|
1149
1170
|
{ name: "help", description: "Show Rigkit CLI help" },
|
|
1150
1171
|
{ name: "init", description: "Initialize a Rigkit project" },
|
|
1151
|
-
{ 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" },
|
|
1152
1176
|
{ name: "ls", description: "List project workspaces" },
|
|
1153
1177
|
{ name: "projects", description: "Discover Rigkit projects below the current directory" },
|
|
1154
1178
|
{ name: "doctor", description: "Show Rigkit runtime diagnostics" },
|
|
@@ -1167,7 +1191,10 @@ async function runHelp(invocation: CliInvocation): Promise<void> {
|
|
|
1167
1191
|
"Commands:",
|
|
1168
1192
|
" help Show Rigkit CLI help",
|
|
1169
1193
|
" init Initialize a Rigkit project",
|
|
1170
|
-
"
|
|
1194
|
+
" plan Plan project workflow changes",
|
|
1195
|
+
" apply Apply project workflow changes",
|
|
1196
|
+
" create Create a workspace",
|
|
1197
|
+
" run Run a workspace operation",
|
|
1171
1198
|
" ls List project workspaces",
|
|
1172
1199
|
" projects Discover Rigkit projects below the current directory",
|
|
1173
1200
|
" doctor Show Rigkit runtime diagnostics",
|
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", "run", "
|
|
15
|
-
currentIndex:
|
|
14
|
+
words: ["rig", "run", ""],
|
|
15
|
+
currentIndex: 2,
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
expect(items.map((item) => item.value)).toEqual(["api", "web"]);
|
|
19
|
-
expect(items[0]?.description).toBe("
|
|
19
|
+
expect(items[0]?.description).toBe("created 2h ago");
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -25,8 +25,8 @@ describe("CLI completion", () => {
|
|
|
25
25
|
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
26
26
|
const items = await completeRig({
|
|
27
27
|
cwd: projectDir,
|
|
28
|
-
words: ["rig", "run", "
|
|
29
|
-
currentIndex:
|
|
28
|
+
words: ["rig", "run", "vm-"],
|
|
29
|
+
currentIndex: 2,
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
expect(items).toEqual([]);
|
|
@@ -39,8 +39,8 @@ 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", "run", "
|
|
43
|
-
currentIndex:
|
|
42
|
+
words: ["rig", "-C", "project", "run", ""],
|
|
43
|
+
currentIndex: 4,
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
expect(items.map((item) => item.value)).toEqual(["api", "web"]);
|
|
@@ -55,42 +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: "created 2h ago" });
|
|
60
60
|
|
|
61
61
|
const exactWorkspace = await completeRig({
|
|
62
62
|
cwd: projectDir,
|
|
63
63
|
words: ["rig", "run", "api"],
|
|
64
64
|
currentIndex: 2,
|
|
65
65
|
});
|
|
66
|
-
expect(exactWorkspace.map((item) => item.value)).toEqual(["api
|
|
66
|
+
expect(exactWorkspace.map((item) => item.value)).toEqual(["api"]);
|
|
67
67
|
|
|
68
68
|
const workspaceAfterSpace = await completeRig({
|
|
69
69
|
cwd: projectDir,
|
|
70
70
|
words: ["rig", "run", "api", ""],
|
|
71
71
|
currentIndex: 3,
|
|
72
72
|
});
|
|
73
|
-
expect(workspaceAfterSpace.map((item) => item.value)).toEqual(["
|
|
73
|
+
expect(workspaceAfterSpace.map((item) => item.value)).toEqual(["remove", "open-cmux"]);
|
|
74
74
|
|
|
75
|
-
const
|
|
75
|
+
const operationPrefix = await completeRig({
|
|
76
76
|
cwd: projectDir,
|
|
77
|
-
words: ["rig", "run", "api
|
|
78
|
-
currentIndex:
|
|
77
|
+
words: ["rig", "run", "api", "open"],
|
|
78
|
+
currentIndex: 3,
|
|
79
79
|
});
|
|
80
|
-
expect(
|
|
80
|
+
expect(operationPrefix.map((item) => item.value)).toEqual(["open-cmux"]);
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
test("
|
|
84
|
+
test("completes top-level project commands at the root command position", async () => {
|
|
85
85
|
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
|
|
86
86
|
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
87
87
|
const items = await completeRig({
|
|
88
88
|
cwd: projectDir,
|
|
89
|
-
words: ["rig", "
|
|
89
|
+
words: ["rig", "p"],
|
|
90
90
|
currentIndex: 1,
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
expect(items.map((item) => item.value)).
|
|
93
|
+
expect(items.map((item) => item.value)).toEqual(["plan", "projects"]);
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
96
|
|
|
@@ -102,7 +102,20 @@ describe("CLI completion", () => {
|
|
|
102
102
|
expect(formatCompletionItems([{ value: "api", description: "workspace smoke", noSpace: true }], "zsh"))
|
|
103
103
|
.toBe("api\tworkspace smoke\tnospace");
|
|
104
104
|
expect(renderCompletionScript("zsh")).toContain("rig __complete");
|
|
105
|
-
expect(renderCompletionScript("zsh")).toContain("compadd -
|
|
105
|
+
expect(renderCompletionScript("zsh")).toContain("compadd -ld displays -a values");
|
|
106
|
+
expect(renderCompletionScript("zsh")).toContain("compadd -S '' -ld nospace_displays -a nospace_values");
|
|
107
|
+
expect(renderCompletionScript("zsh")).toContain('display="${value} -- ${description}"');
|
|
108
|
+
expect(renderCompletionScript("zsh")).not.toContain("_describe");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("formats workspace ages", () => {
|
|
112
|
+
const now = Date.parse("2026-05-14T12:00:00.000Z");
|
|
113
|
+
|
|
114
|
+
expect(formatWorkspaceAge("2026-05-14T11:59:45.000Z", now)).toBe("just now");
|
|
115
|
+
expect(formatWorkspaceAge("2026-05-14T11:30:00.000Z", now)).toBe("30m ago");
|
|
116
|
+
expect(formatWorkspaceAge("2026-05-14T09:00:00.000Z", now)).toBe("3h ago");
|
|
117
|
+
expect(formatWorkspaceAge("2026-05-11T12:00:00.000Z", now)).toBe("3d ago");
|
|
118
|
+
expect(formatWorkspaceAge("not-a-date", now)).toBeUndefined();
|
|
106
119
|
});
|
|
107
120
|
|
|
108
121
|
test("completes ls targets", async () => {
|
|
@@ -154,7 +167,10 @@ async function withWorkspaceRuntime(
|
|
|
154
167
|
});
|
|
155
168
|
}
|
|
156
169
|
if (pathname === "/workspaces") {
|
|
157
|
-
const
|
|
170
|
+
const nowMs = Date.now();
|
|
171
|
+
const apiCreatedAt = new Date(nowMs - 2 * 60 * 60 * 1000).toISOString();
|
|
172
|
+
const webCreatedAt = new Date(nowMs - 5 * 60 * 1000).toISOString();
|
|
173
|
+
const updatedAt = new Date(nowMs).toISOString();
|
|
158
174
|
return runtimeJson({
|
|
159
175
|
workspaces: [
|
|
160
176
|
{
|
|
@@ -162,16 +178,16 @@ async function withWorkspaceRuntime(
|
|
|
162
178
|
name: "api",
|
|
163
179
|
workflow: "smoke",
|
|
164
180
|
ctx: {},
|
|
165
|
-
createdAt:
|
|
166
|
-
updatedAt
|
|
181
|
+
createdAt: apiCreatedAt,
|
|
182
|
+
updatedAt,
|
|
167
183
|
},
|
|
168
184
|
{
|
|
169
185
|
id: "workspace-web",
|
|
170
186
|
name: "web",
|
|
171
187
|
workflow: "smoke",
|
|
172
188
|
ctx: {},
|
|
173
|
-
createdAt:
|
|
174
|
-
updatedAt
|
|
189
|
+
createdAt: webCreatedAt,
|
|
190
|
+
updatedAt,
|
|
175
191
|
},
|
|
176
192
|
],
|
|
177
193
|
});
|
package/src/completion.ts
CHANGED
|
@@ -18,7 +18,10 @@ type CompleteRigInput = {
|
|
|
18
18
|
const COMMANDS: CompletionItem[] = [
|
|
19
19
|
{ value: "help", description: "show CLI help" },
|
|
20
20
|
{ value: "init", description: "initialize a Rigkit project" },
|
|
21
|
-
{ 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" },
|
|
22
25
|
{ value: "ls", description: "list project workspaces" },
|
|
23
26
|
{ value: "projects", description: "discover Rigkit projects" },
|
|
24
27
|
{ value: "doctor", description: "show runtime diagnostics" },
|
|
@@ -46,11 +49,22 @@ const COMMAND_OPTIONS: Record<string, CompletionItem[]> = {
|
|
|
46
49
|
{ value: "--force", description: "overwrite existing config" },
|
|
47
50
|
{ value: "--json", description: "print JSON" },
|
|
48
51
|
],
|
|
49
|
-
|
|
52
|
+
plan: [
|
|
50
53
|
{ value: "--all", description: "run against every discovered project" },
|
|
51
54
|
{ value: "--discover", description: "discover projects below the selected directory" },
|
|
52
55
|
{ value: "--json", description: "print JSON" },
|
|
53
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
|
+
],
|
|
54
68
|
ls: [
|
|
55
69
|
{ value: "workspaces", description: "list workspaces" },
|
|
56
70
|
{ value: "snapshots", description: "list snapshots" },
|
|
@@ -67,6 +81,8 @@ const COMMAND_OPTIONS: Record<string, CompletionItem[]> = {
|
|
|
67
81
|
],
|
|
68
82
|
};
|
|
69
83
|
|
|
84
|
+
const PROJECT_OPERATION_COMMANDS = new Set(["plan", "apply", "create"]);
|
|
85
|
+
|
|
70
86
|
const OPTIONS_WITH_VALUES = new Set([
|
|
71
87
|
"-C",
|
|
72
88
|
"--project",
|
|
@@ -92,6 +108,13 @@ type RuntimeOperationDefinition = {
|
|
|
92
108
|
};
|
|
93
109
|
};
|
|
94
110
|
|
|
111
|
+
type RuntimeWorkspaceCompletion = {
|
|
112
|
+
name: string;
|
|
113
|
+
workflow: string;
|
|
114
|
+
createdAt: string;
|
|
115
|
+
updatedAt: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
95
118
|
export async function completeRig(input: CompleteRigInput): Promise<CompletionItem[]> {
|
|
96
119
|
const cwd = input.cwd ?? process.cwd();
|
|
97
120
|
const words = input.words.length > 0 ? input.words : ["rig"];
|
|
@@ -113,38 +136,39 @@ export async function completeRig(input: CompleteRigInput): Promise<CompletionIt
|
|
|
113
136
|
|
|
114
137
|
if (current.startsWith("-")) {
|
|
115
138
|
if (command === "run") {
|
|
116
|
-
const run =
|
|
117
|
-
if (run.operation) {
|
|
118
|
-
const operation = await
|
|
139
|
+
const run = parseWorkspaceRunCommand(before);
|
|
140
|
+
if (run.workspace && run.operation) {
|
|
141
|
+
const operation = await safeResolveWorkspaceOperation(resolveProjectDir(words, cwd), run.operation);
|
|
119
142
|
return filterItems([
|
|
120
143
|
...(operation?.cli?.options ?? []).flatMap((option) => [
|
|
121
144
|
{ value: option.flag, description: option.name },
|
|
122
145
|
...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name })),
|
|
123
146
|
]),
|
|
124
|
-
...COMMAND_OPTIONS.
|
|
147
|
+
...COMMAND_OPTIONS.run,
|
|
125
148
|
...GLOBAL_OPTIONS,
|
|
126
149
|
], current);
|
|
127
150
|
}
|
|
128
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
|
+
}
|
|
129
163
|
return filterItems([...(COMMAND_OPTIONS[command] ?? []), ...GLOBAL_OPTIONS], current);
|
|
130
164
|
}
|
|
131
165
|
|
|
132
166
|
const positionalCount = countPositionals(before, command);
|
|
133
167
|
|
|
134
168
|
if (command === "run") {
|
|
135
|
-
const run =
|
|
136
|
-
if (!run.
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), run.operation);
|
|
140
|
-
if (!operation && run.args.length === 0) {
|
|
141
|
-
return filterItems(await safeWorkspaceOperationTargets(resolveProjectDir(words, cwd), run.operation), current);
|
|
142
|
-
}
|
|
143
|
-
const operationPositionalCount = countRunOperationPositionals(run.args);
|
|
144
|
-
const positional = operation?.cli?.positionals?.find((item) => item.index === operationPositionalCount);
|
|
145
|
-
if (positional && /workspace|vm/i.test(positional.name)) {
|
|
146
|
-
return filterItems(await workspaceTargets(resolveProjectDir(words, cwd), current, /vm/i.test(positional.name)), current);
|
|
147
|
-
}
|
|
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);
|
|
148
172
|
}
|
|
149
173
|
|
|
150
174
|
if (command === "completion" && positionalCount === 0) {
|
|
@@ -206,8 +230,8 @@ complete -c rig -f -a "(__rig_complete)"
|
|
|
206
230
|
return `#compdef rig
|
|
207
231
|
# rig zsh completion
|
|
208
232
|
_rig() {
|
|
209
|
-
local -a raw values
|
|
210
|
-
local line value description rest marker
|
|
233
|
+
local -a raw values displays nospace_values nospace_displays
|
|
234
|
+
local line value description rest marker display
|
|
211
235
|
raw=("\${(@f)$(command rig __complete --shell zsh --index $((CURRENT - 1)) -- "\${words[@]}" 2>/dev/null)}")
|
|
212
236
|
for line in "\${raw[@]}"; do
|
|
213
237
|
value="\${line%%$'\\t'*}"
|
|
@@ -220,16 +244,20 @@ _rig() {
|
|
|
220
244
|
marker="\${rest#*$'\\t'}"
|
|
221
245
|
fi
|
|
222
246
|
fi
|
|
247
|
+
display="\${value}"
|
|
248
|
+
if [[ -n "$description" ]]; then
|
|
249
|
+
display="\${value} -- \${description}"
|
|
250
|
+
fi
|
|
223
251
|
if [[ "$marker" == "nospace" ]]; then
|
|
224
252
|
nospace_values+=("\${value}")
|
|
225
|
-
|
|
253
|
+
nospace_displays+=("\${display}")
|
|
226
254
|
else
|
|
227
255
|
values+=("\${value}")
|
|
228
|
-
|
|
256
|
+
displays+=("\${display}")
|
|
229
257
|
fi
|
|
230
258
|
done
|
|
231
|
-
(( \${#nospace_values} )) && compadd -S '' -
|
|
232
|
-
(( \${#values} )) && compadd -
|
|
259
|
+
(( \${#nospace_values} )) && compadd -S '' -ld nospace_displays -a nospace_values
|
|
260
|
+
(( \${#values} )) && compadd -ld displays -a values
|
|
233
261
|
}
|
|
234
262
|
compdef _rig rig
|
|
235
263
|
`;
|
|
@@ -275,7 +303,7 @@ function countPositionals(words: string[], command: string): number {
|
|
|
275
303
|
return count;
|
|
276
304
|
}
|
|
277
305
|
|
|
278
|
-
function
|
|
306
|
+
function parseWorkspaceRunCommand(words: string[]): { workspace?: string; operation?: string; args: string[] } {
|
|
279
307
|
let foundRun = false;
|
|
280
308
|
const args: string[] = [];
|
|
281
309
|
for (let index = 0; index < words.length; index += 1) {
|
|
@@ -292,22 +320,7 @@ function parseRunCommand(words: string[]): { operation?: string; args: string[]
|
|
|
292
320
|
}
|
|
293
321
|
args.push(word);
|
|
294
322
|
}
|
|
295
|
-
return {
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function countRunOperationPositionals(args: string[]): number {
|
|
299
|
-
let count = 0;
|
|
300
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
301
|
-
const arg = args[index]!;
|
|
302
|
-
if (OPTIONS_WITH_VALUES.has(arg)) {
|
|
303
|
-
index += 1;
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
if (arg.startsWith("--") && arg.includes("=")) continue;
|
|
307
|
-
if (arg.startsWith("-")) continue;
|
|
308
|
-
count += 1;
|
|
309
|
-
}
|
|
310
|
-
return count;
|
|
323
|
+
return { workspace: args[0], operation: args[1], args: args.slice(2) };
|
|
311
324
|
}
|
|
312
325
|
|
|
313
326
|
function expectsOptionValue(words: string[]): boolean {
|
|
@@ -344,112 +357,85 @@ function projectPaths(projectDir: string): { projectDir: string; configPath: str
|
|
|
344
357
|
|
|
345
358
|
async function workspaceTargets(
|
|
346
359
|
paths: { projectDir: string; configPath: string },
|
|
347
|
-
_current: string,
|
|
348
|
-
_includeVmIds: boolean,
|
|
349
360
|
): Promise<CompletionItem[]> {
|
|
350
361
|
const workspaces = await readWorkspaces(paths);
|
|
351
362
|
const items = workspaces.map((workspace) => ({
|
|
352
363
|
value: workspace.name,
|
|
353
|
-
description: workspace
|
|
364
|
+
description: workspaceDescription(workspace),
|
|
354
365
|
}));
|
|
355
366
|
|
|
356
367
|
return dedupeItems(items);
|
|
357
368
|
}
|
|
358
369
|
|
|
359
|
-
async function readWorkspaces(paths: { projectDir: string; configPath: string }): Promise<
|
|
370
|
+
async function readWorkspaces(paths: { projectDir: string; configPath: string }): Promise<RuntimeWorkspaceCompletion[]> {
|
|
360
371
|
const runtime = await getOrStartRuntime(paths);
|
|
361
372
|
const { workspaces } = await runtime.control.workspaces();
|
|
362
373
|
return workspaces.map((workspace) => ({
|
|
363
374
|
name: workspace.name,
|
|
364
375
|
workflow: workspace.workflow,
|
|
376
|
+
createdAt: workspace.createdAt,
|
|
377
|
+
updatedAt: workspace.updatedAt,
|
|
365
378
|
}));
|
|
366
379
|
}
|
|
367
380
|
|
|
368
|
-
async function operationTargets(
|
|
369
|
-
paths: { projectDir: string; configPath: string },
|
|
370
|
-
current: string,
|
|
371
|
-
): Promise<CompletionItem[]> {
|
|
372
|
-
const manifest = await readOperations(paths);
|
|
373
|
-
if (current.includes("/")) {
|
|
374
|
-
return workspaceOperationTargets(manifest, current);
|
|
375
|
-
}
|
|
376
|
-
const workspaces = await readWorkspaces(paths).catch(() => []);
|
|
377
|
-
if (workspaces.some((workspace) => workspace.name === current)) {
|
|
378
|
-
return workspaceOperationTargets(manifest, `${current}/`);
|
|
379
|
-
}
|
|
380
|
-
return manifest.operations.flatMap((operation) => [
|
|
381
|
-
{ value: operation.id, description: operation.description },
|
|
382
|
-
...(operation.aliases ?? []).map((alias) => ({ value: alias, description: operation.description })),
|
|
383
|
-
]).concat(workspaces.map((workspace) => ({
|
|
384
|
-
value: workspace.name,
|
|
385
|
-
description: `workspace ${workspace.workflow}`,
|
|
386
|
-
noSpace: true,
|
|
387
|
-
})));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
381
|
async function safeWorkspaceOperationTargets(
|
|
391
382
|
paths: { projectDir: string; configPath: string },
|
|
392
|
-
workspace: string,
|
|
393
383
|
): Promise<CompletionItem[]> {
|
|
394
384
|
try {
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
readWorkspaces(paths),
|
|
398
|
-
]);
|
|
399
|
-
if (!workspaces.some((item) => item.name === workspace)) return [];
|
|
400
|
-
return workspaceOperationTargets(manifest, `${workspace}/`);
|
|
385
|
+
const manifest = await readOperations(paths);
|
|
386
|
+
return workspaceOperationTargets(manifest);
|
|
401
387
|
} catch {
|
|
402
388
|
return [];
|
|
403
389
|
}
|
|
404
390
|
}
|
|
405
391
|
|
|
406
|
-
function workspaceOperationTargets(manifest: RuntimeOperationManifest
|
|
407
|
-
const slash = current.indexOf("/");
|
|
408
|
-
if (slash < 0) return [];
|
|
409
|
-
const workspace = current.slice(0, slash);
|
|
410
|
-
if (!workspace) return [];
|
|
392
|
+
function workspaceOperationTargets(manifest: RuntimeOperationManifest): CompletionItem[] {
|
|
411
393
|
return (manifest.workspaceOperations ?? []).flatMap((operation) => [
|
|
412
|
-
{ value:
|
|
394
|
+
{ value: operation.id, description: operation.description ?? "workspace operation" },
|
|
413
395
|
...(operation.aliases ?? []).map((alias) => ({
|
|
414
|
-
value:
|
|
396
|
+
value: alias,
|
|
415
397
|
description: operation.description ?? "workspace operation",
|
|
416
398
|
})),
|
|
417
399
|
]);
|
|
418
400
|
}
|
|
419
401
|
|
|
420
|
-
async function
|
|
402
|
+
async function resolveRuntimeOperation(
|
|
421
403
|
paths: { projectDir: string; configPath: string },
|
|
422
|
-
|
|
423
|
-
): 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> {
|
|
424
416
|
try {
|
|
425
|
-
return await
|
|
417
|
+
return await resolveRuntimeOperation(paths, operationId);
|
|
426
418
|
} catch {
|
|
427
|
-
return
|
|
419
|
+
return undefined;
|
|
428
420
|
}
|
|
429
421
|
}
|
|
430
422
|
|
|
431
|
-
async function
|
|
423
|
+
async function resolveWorkspaceOperation(
|
|
432
424
|
paths: { projectDir: string; configPath: string },
|
|
433
425
|
operationId: string,
|
|
434
426
|
): Promise<RuntimeOperationDefinition | undefined> {
|
|
435
427
|
const manifest = await readOperations(paths);
|
|
436
|
-
|
|
437
|
-
if (workspaceOperation) {
|
|
438
|
-
return (manifest.workspaceOperations ?? []).find((operation) =>
|
|
439
|
-
operation.id === workspaceOperation.operation || operation.aliases?.includes(workspaceOperation.operation)
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
return manifest.operations.find((operation) =>
|
|
428
|
+
return (manifest.workspaceOperations ?? []).find((operation) =>
|
|
443
429
|
operation.id === operationId || operation.aliases?.includes(operationId)
|
|
444
430
|
);
|
|
445
431
|
}
|
|
446
432
|
|
|
447
|
-
async function
|
|
433
|
+
async function safeResolveWorkspaceOperation(
|
|
448
434
|
paths: { projectDir: string; configPath: string },
|
|
449
435
|
operationId: string,
|
|
450
436
|
): Promise<RuntimeOperationDefinition | undefined> {
|
|
451
437
|
try {
|
|
452
|
-
return await
|
|
438
|
+
return await resolveWorkspaceOperation(paths, operationId);
|
|
453
439
|
} catch {
|
|
454
440
|
return undefined;
|
|
455
441
|
}
|
|
@@ -460,15 +446,6 @@ async function readOperations(paths: { projectDir: string; configPath: string })
|
|
|
460
446
|
return await runtime.control.operations() as unknown as RuntimeOperationManifest;
|
|
461
447
|
}
|
|
462
448
|
|
|
463
|
-
function parseWorkspaceOperationId(value: string): { workspace: string; operation: string } | undefined {
|
|
464
|
-
const slash = value.indexOf("/");
|
|
465
|
-
if (slash <= 0 || slash === value.length - 1) return undefined;
|
|
466
|
-
return {
|
|
467
|
-
workspace: value.slice(0, slash),
|
|
468
|
-
operation: value.slice(slash + 1),
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
449
|
function filterItems(items: CompletionItem[], current: string): CompletionItem[] {
|
|
473
450
|
return dedupeItems(items).filter((item) => item.value.startsWith(current));
|
|
474
451
|
}
|
|
@@ -483,3 +460,30 @@ function dedupeItems(items: CompletionItem[]): CompletionItem[] {
|
|
|
483
460
|
}
|
|
484
461
|
return deduped;
|
|
485
462
|
}
|
|
463
|
+
|
|
464
|
+
function workspaceDescription(workspace: RuntimeWorkspaceCompletion): string {
|
|
465
|
+
const age = formatWorkspaceAge(workspace.createdAt);
|
|
466
|
+
return age ? `created ${age}` : "created date unknown";
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function formatWorkspaceAge(createdAt: string, nowMs = Date.now()): string | undefined {
|
|
470
|
+
const createdAtMs = Date.parse(createdAt);
|
|
471
|
+
if (!Number.isFinite(createdAtMs)) return undefined;
|
|
472
|
+
|
|
473
|
+
const elapsedSeconds = Math.max(0, Math.floor((nowMs - createdAtMs) / 1000));
|
|
474
|
+
if (elapsedSeconds < 60) return "just now";
|
|
475
|
+
|
|
476
|
+
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
477
|
+
if (elapsedMinutes < 60) return `${elapsedMinutes}m ago`;
|
|
478
|
+
|
|
479
|
+
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
|
480
|
+
if (elapsedHours < 48) return `${elapsedHours}h ago`;
|
|
481
|
+
|
|
482
|
+
const elapsedDays = Math.floor(elapsedHours / 24);
|
|
483
|
+
if (elapsedDays < 30) return `${elapsedDays}d ago`;
|
|
484
|
+
|
|
485
|
+
const elapsedMonths = Math.floor(elapsedDays / 30);
|
|
486
|
+
if (elapsedMonths < 24) return `${elapsedMonths}mo ago`;
|
|
487
|
+
|
|
488
|
+
return `${Math.floor(elapsedMonths / 12)}y ago`;
|
|
489
|
+
}
|
package/src/init.test.ts
CHANGED
|
@@ -42,8 +42,8 @@ describe("initProject", () => {
|
|
|
42
42
|
|
|
43
43
|
const pkg = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8"));
|
|
44
44
|
expect(pkg.name).toBe("platform-api");
|
|
45
|
-
expect(pkg.scripts.plan).toBe("rig
|
|
46
|
-
expect(pkg.scripts.apply).toBe("rig
|
|
45
|
+
expect(pkg.scripts.plan).toBe("rig plan");
|
|
46
|
+
expect(pkg.scripts.apply).toBe("rig apply");
|
|
47
47
|
expect(pkg.devDependencies[PROJECT_PACKAGE_NAME]).toBe(RIGKIT_CLI_VERSION);
|
|
48
48
|
expect(pkg.devDependencies[FREESTYLE_PROVIDER_PACKAGE_NAME]).toBe(RIGKIT_CLI_VERSION);
|
|
49
49
|
expect(pkg.devDependencies[FREESTYLE_SDK_PACKAGE_NAME]).toBe(FREESTYLE_SDK_PACKAGE_VERSION);
|
|
@@ -74,7 +74,7 @@ describe("initProject", () => {
|
|
|
74
74
|
const pkg = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8"));
|
|
75
75
|
expect(pkg.name).toBe("existing");
|
|
76
76
|
expect(pkg.scripts.test).toBe("bun test");
|
|
77
|
-
expect(pkg.scripts.plan).toBe("rig
|
|
77
|
+
expect(pkg.scripts.plan).toBe("rig plan");
|
|
78
78
|
});
|
|
79
79
|
});
|
|
80
80
|
|
package/src/init.ts
CHANGED
|
@@ -229,7 +229,7 @@ function ensureProjectPackageJson(
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
const scripts = pkg.scripts as Record<string, string>;
|
|
232
|
-
for (const [key, value] of Object.entries({ plan: "rig
|
|
232
|
+
for (const [key, value] of Object.entries({ plan: "rig plan", apply: "rig apply" })) {
|
|
233
233
|
if (scripts[key] !== value) {
|
|
234
234
|
scripts[key] = value;
|
|
235
235
|
updated = true;
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const RIGKIT_CLI_VERSION = "0.2.
|
|
1
|
+
export const RIGKIT_CLI_VERSION = "0.2.7";
|
|
@@ -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
|
+
}
|