@rigkit/cli 0.2.3 → 0.2.5
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 +42 -6
- package/src/cli.ts +213 -78
- package/src/completion.test.ts +39 -9
- package/src/completion.ts +47 -45
- package/src/init.test.ts +16 -9
- package/src/init.ts +46 -22
- package/src/project.ts +2 -0
- package/src/version.ts +1 -1
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 plan
|
|
8
|
+
rig run plan
|
|
9
9
|
rig ls
|
|
10
|
-
rig plan github:owner/repo
|
|
10
|
+
rig run 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 <workspace>/<operation>`, for example `rig website-workspace/open-cmux` or `rig website-workspace/remove -y`.
|
|
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.5",
|
|
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/engine": "0.2.
|
|
29
|
-
"@rigkit/runtime-client": "0.2.
|
|
30
|
-
"@rigkit/provider-cmux": "0.2.
|
|
28
|
+
"@rigkit/engine": "0.2.5",
|
|
29
|
+
"@rigkit/runtime-client": "0.2.5",
|
|
30
|
+
"@rigkit/provider-cmux": "0.2.5"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/bun": "latest",
|
package/src/cli.test.ts
CHANGED
|
@@ -23,7 +23,7 @@ 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("run Run a project operation exposed by the runtime");
|
|
27
27
|
|
|
28
28
|
const version = await runCli(["version"]);
|
|
29
29
|
expect(version.exitCode).toBe(0);
|
|
@@ -34,15 +34,15 @@ describe("CLI entrypoint", () => {
|
|
|
34
34
|
expect(help.exitCode).toBe(0);
|
|
35
35
|
expect(help.stderr).toBe("");
|
|
36
36
|
expect(help.stdout).toContain("rig ");
|
|
37
|
-
expect(help.stdout).toContain("
|
|
37
|
+
expect(help.stdout).toContain("run Run a project operation exposed by the runtime");
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
test("
|
|
40
|
+
test("rejects operation shorthand at the root", async () => {
|
|
41
41
|
const result = await runCli(["unknown"]);
|
|
42
42
|
|
|
43
43
|
expect(result.exitCode).toBe(1);
|
|
44
44
|
expect(result.stdout).toBe("");
|
|
45
|
-
expect(result.stderr).toContain("
|
|
45
|
+
expect(result.stderr).toContain("unknown command 'unknown'");
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
test("serves dynamic shell completion endpoint", async () => {
|
|
@@ -106,11 +106,23 @@ describe("CLI entrypoint", () => {
|
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
+
test("rejects workspace create names that are not shell-safe", async () => {
|
|
110
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-cli-create-name-"));
|
|
111
|
+
|
|
112
|
+
await withWorkspaceRuntime({ projectDir }, async ({ env }) => {
|
|
113
|
+
const result = await runCli(["-C", projectDir, "run", "create", "--name", "some workspace", "--json"], { env });
|
|
114
|
+
|
|
115
|
+
expect(result.exitCode).toBe(1);
|
|
116
|
+
expect(result.stdout).toBe("");
|
|
117
|
+
expect(result.stderr).toContain('Invalid workspace name "some workspace"');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
109
121
|
test("requires discovered projects for operation --all", async () => {
|
|
110
122
|
const cwd = mkdtempSync(join(tmpdir(), "rigkit-cli-run-all-"));
|
|
111
123
|
|
|
112
124
|
try {
|
|
113
|
-
const result = await runCli(["plan", "--all", "--json"], { cwd });
|
|
125
|
+
const result = await runCli(["run", "plan", "--all", "--json"], { cwd });
|
|
114
126
|
|
|
115
127
|
expect(result.exitCode).toBe(1);
|
|
116
128
|
expect(result.stdout).toBe("");
|
|
@@ -128,7 +140,7 @@ describe("CLI entrypoint", () => {
|
|
|
128
140
|
writeFileSync(join(cwd, "web", "rig.config.ts"), "export default {}\n");
|
|
129
141
|
|
|
130
142
|
try {
|
|
131
|
-
const result = await runCli(["plan", "--discover", "--json"], { cwd });
|
|
143
|
+
const result = await runCli(["run", "plan", "--discover", "--json"], { cwd });
|
|
132
144
|
|
|
133
145
|
expect(result.exitCode).toBe(1);
|
|
134
146
|
expect(result.stdout).toBe("");
|
|
@@ -216,6 +228,30 @@ async function withWorkspaceRuntime(
|
|
|
216
228
|
}],
|
|
217
229
|
});
|
|
218
230
|
}
|
|
231
|
+
if (pathname === "/operations") {
|
|
232
|
+
return runtimeJson({
|
|
233
|
+
operations: [{
|
|
234
|
+
id: "create",
|
|
235
|
+
kind: "command",
|
|
236
|
+
source: "core",
|
|
237
|
+
title: "Create",
|
|
238
|
+
description: "Create a workspace",
|
|
239
|
+
createsWorkspace: true,
|
|
240
|
+
cli: {
|
|
241
|
+
options: [{ name: "name", flag: "--name", required: true, type: "string" }],
|
|
242
|
+
},
|
|
243
|
+
inputSchema: {
|
|
244
|
+
type: "object",
|
|
245
|
+
additionalProperties: false,
|
|
246
|
+
properties: {
|
|
247
|
+
name: { type: "string", minLength: 1 },
|
|
248
|
+
},
|
|
249
|
+
required: ["name"],
|
|
250
|
+
},
|
|
251
|
+
}],
|
|
252
|
+
workspaceOperations: [],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
219
255
|
return runtimeJson({ error: { message: "Not found" } }, { status: 404 });
|
|
220
256
|
},
|
|
221
257
|
});
|
package/src/cli.ts
CHANGED
|
@@ -148,41 +148,10 @@ const CLI_HOST_CAPABILITIES: Array<{ id: string; schemaHash?: string }> = [
|
|
|
148
148
|
...(capability.schemaHash ? { schemaHash: capability.schemaHash } : {}),
|
|
149
149
|
}));
|
|
150
150
|
|
|
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
151
|
if (process.argv[2] === "__complete") {
|
|
165
152
|
runCompletionEndpoint(process.argv.slice(3)).catch(handleCliError);
|
|
166
153
|
} 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;
|
|
154
|
+
runCli(process.argv).catch(handleCliError);
|
|
186
155
|
}
|
|
187
156
|
|
|
188
157
|
async function runCli(argv: string[]): Promise<void> {
|
|
@@ -190,7 +159,7 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
190
159
|
program
|
|
191
160
|
.name("rig")
|
|
192
161
|
.description("Rigkit workflow CLI")
|
|
193
|
-
.usage("[options] <command
|
|
162
|
+
.usage("[options] <command>")
|
|
194
163
|
.version(RIGKIT_CLI_VERSION, "-v, --version", "Show Rigkit CLI version")
|
|
195
164
|
.showHelpAfterError()
|
|
196
165
|
.exitOverride()
|
|
@@ -228,7 +197,7 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
228
197
|
});
|
|
229
198
|
|
|
230
199
|
program
|
|
231
|
-
.command("run <operation> [args...]"
|
|
200
|
+
.command("run <operation> [args...]")
|
|
232
201
|
.description("Run a project operation exposed by the runtime")
|
|
233
202
|
.allowUnknownOption(true)
|
|
234
203
|
.option("--all", "Run against every discovered project")
|
|
@@ -405,29 +374,29 @@ async function runInit(invocation: CliInvocation, options: InitOptions): Promise
|
|
|
405
374
|
async function resolveInitAnswers(
|
|
406
375
|
options: InitOptions,
|
|
407
376
|
jsonMode: boolean,
|
|
408
|
-
): Promise<{ name: string; apiKey
|
|
409
|
-
if (jsonMode &&
|
|
410
|
-
throw new Error(`rig init --json requires --name
|
|
377
|
+
): Promise<{ name: string; apiKey?: string; packageManager: PackageManager }> {
|
|
378
|
+
if (jsonMode && options.name === undefined) {
|
|
379
|
+
throw new Error(`rig init --json requires --name`);
|
|
411
380
|
}
|
|
412
381
|
|
|
413
382
|
if (jsonMode && options.packageManager && options.packageManager !== "skip") {
|
|
414
383
|
throw new Error(`rig init --json only supports --package-manager skip`);
|
|
415
384
|
}
|
|
416
385
|
|
|
417
|
-
if (options.name === undefined
|
|
386
|
+
if (options.name === undefined) {
|
|
418
387
|
assertInteractiveInit();
|
|
419
388
|
}
|
|
420
389
|
|
|
421
390
|
if (!jsonMode) {
|
|
422
391
|
console.log(chalk.bold("Initialize Rigkit"));
|
|
423
|
-
console.log(chalk.dim("This creates a project folder with rig.config.ts,
|
|
392
|
+
console.log(chalk.dim("This creates a project folder with rig.config.ts, package.json, and local ignore rules."));
|
|
424
393
|
console.log("");
|
|
425
394
|
}
|
|
426
395
|
|
|
427
396
|
const name = options.name !== undefined
|
|
428
397
|
? normalizeMachineName(options.name)
|
|
429
398
|
: await promptName();
|
|
430
|
-
const apiKey = options.apiKey?.trim()
|
|
399
|
+
const apiKey = options.apiKey?.trim();
|
|
431
400
|
const packageManager = options.packageManager ?? (jsonMode || !canPrompt() ? "skip" : await promptPackageManager("skip"));
|
|
432
401
|
|
|
433
402
|
return {
|
|
@@ -439,7 +408,7 @@ async function resolveInitAnswers(
|
|
|
439
408
|
|
|
440
409
|
function assertInteractiveInit(): void {
|
|
441
410
|
if (canPrompt()) return;
|
|
442
|
-
throw new Error(`rig init needs --name
|
|
411
|
+
throw new Error(`rig init needs --name when not running in an interactive terminal`);
|
|
443
412
|
}
|
|
444
413
|
|
|
445
414
|
function canPrompt(): boolean {
|
|
@@ -478,14 +447,6 @@ async function promptName(): Promise<string> {
|
|
|
478
447
|
return answers.name;
|
|
479
448
|
}
|
|
480
449
|
|
|
481
|
-
async function promptRequiredSecret(label: string): Promise<string> {
|
|
482
|
-
for (;;) {
|
|
483
|
-
const value = (await promptSecret(label)).trim();
|
|
484
|
-
if (value) return value;
|
|
485
|
-
console.log(chalk.red(`${label} is required.`));
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
450
|
async function promptPackageManager(defaultValue: PackageManager): Promise<PackageManager> {
|
|
490
451
|
const choices: Array<{ value: PackageManager; label: string; hint: string }> = [
|
|
491
452
|
{ value: "npm", label: "npm", hint: "npm install" },
|
|
@@ -507,14 +468,50 @@ async function promptPackageManager(defaultValue: PackageManager): Promise<Packa
|
|
|
507
468
|
return answers.packageManager;
|
|
508
469
|
}
|
|
509
470
|
|
|
510
|
-
async function
|
|
511
|
-
const answers = await inquirer.prompt<{
|
|
512
|
-
type: "
|
|
513
|
-
name: "
|
|
514
|
-
message:
|
|
515
|
-
|
|
471
|
+
async function promptWorkspaceName(): Promise<string> {
|
|
472
|
+
const answers = await inquirer.prompt<{ name: string }>([{
|
|
473
|
+
type: "input",
|
|
474
|
+
name: "name",
|
|
475
|
+
message: "Workspace name:",
|
|
476
|
+
validate(value: string) {
|
|
477
|
+
return validateWorkspaceName(value.trim());
|
|
478
|
+
},
|
|
479
|
+
filter: (value: string) => value.trim(),
|
|
516
480
|
}]);
|
|
517
|
-
return answers.
|
|
481
|
+
return answers.name;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const workspaceNamePattern = /^(?!-)[A-Za-z0-9._-]+$/;
|
|
485
|
+
|
|
486
|
+
function validateWorkspaceName(value: string): true | string {
|
|
487
|
+
if (!value) return "Workspace name is required.";
|
|
488
|
+
if (!workspaceNamePattern.test(value)) {
|
|
489
|
+
return 'Use only letters, numbers, ".", "_", and "-", and do not start with "-".';
|
|
490
|
+
}
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function assertValidWorkspaceName(value: unknown): void {
|
|
495
|
+
if (typeof value !== "string") throw new Error(`Workspace name must be a string`);
|
|
496
|
+
const valid = validateWorkspaceName(value);
|
|
497
|
+
if (valid !== true) throw new Error(`Invalid workspace name "${value}". ${valid}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function promptWorkspaceOperation(
|
|
501
|
+
workspace: string,
|
|
502
|
+
operations: RuntimeOperationDefinition[],
|
|
503
|
+
): Promise<string> {
|
|
504
|
+
const answers = await inquirer.prompt<{ operation: string }>([{
|
|
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;
|
|
518
515
|
}
|
|
519
516
|
|
|
520
517
|
async function runPackageManagerInstall(
|
|
@@ -598,7 +595,7 @@ function printInitResult(result: InitProjectResult, install: InitInstallResult):
|
|
|
598
595
|
if (install.skipped) {
|
|
599
596
|
console.log(` ${detectInstallCommand(result.packageJsonPath)}`);
|
|
600
597
|
}
|
|
601
|
-
console.log(" rig plan");
|
|
598
|
+
console.log(" rig run plan");
|
|
602
599
|
}
|
|
603
600
|
|
|
604
601
|
function displayProjectDir(projectDir: string): string {
|
|
@@ -667,6 +664,7 @@ async function runProjectOperation(
|
|
|
667
664
|
}
|
|
668
665
|
|
|
669
666
|
await renderOperationResult(operation, result, parsed.hostOptions);
|
|
667
|
+
printInteractiveOutputGap(invocation);
|
|
670
668
|
}
|
|
671
669
|
|
|
672
670
|
async function runDiscoveredProjectOperation(
|
|
@@ -722,6 +720,7 @@ async function runDiscoveredProjectOperation(
|
|
|
722
720
|
console.log(chalk.bold(displayProjectDir(project.projectDir)));
|
|
723
721
|
}
|
|
724
722
|
await renderOperationResult(operation, result, parsed.hostOptions);
|
|
723
|
+
printInteractiveOutputGap(invocation);
|
|
725
724
|
}
|
|
726
725
|
}
|
|
727
726
|
|
|
@@ -792,13 +791,13 @@ async function executeRuntimeOperation(
|
|
|
792
791
|
result: unknown;
|
|
793
792
|
}> {
|
|
794
793
|
const manifest = await readRuntimeOperations(runtime);
|
|
795
|
-
const resolved =
|
|
794
|
+
const resolved = await resolveRequestedRuntimeOperation(invocation, runtime, manifest, requestedOperation);
|
|
796
795
|
if (!resolved) {
|
|
797
796
|
throw new Error(`This project does not define a Rigkit operation named "${requestedOperation}".`);
|
|
798
797
|
}
|
|
799
798
|
const { operation, runOperation } = resolved;
|
|
800
799
|
|
|
801
|
-
const parsed =
|
|
800
|
+
const parsed = await parseOperationArgsWithPrompts(invocation, operation, args);
|
|
802
801
|
enforceHostOnlyBooleanGuards(operation, parsed);
|
|
803
802
|
|
|
804
803
|
const result = await runRuntimeOperation<unknown>(
|
|
@@ -811,6 +810,28 @@ async function executeRuntimeOperation(
|
|
|
811
810
|
return { operation, parsed, result };
|
|
812
811
|
}
|
|
813
812
|
|
|
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
|
+
|
|
814
835
|
function findRuntimeOperation(
|
|
815
836
|
manifest: RuntimeOperationManifest,
|
|
816
837
|
requestedOperation: string,
|
|
@@ -836,7 +857,36 @@ function parseWorkspaceOperationId(value: string): { workspace: string; operatio
|
|
|
836
857
|
};
|
|
837
858
|
}
|
|
838
859
|
|
|
839
|
-
function
|
|
860
|
+
async function parseOperationArgsWithPrompts(
|
|
861
|
+
invocation: CliInvocation,
|
|
862
|
+
operation: RuntimeOperationDefinition,
|
|
863
|
+
args: string[],
|
|
864
|
+
): Promise<ParsedOperationInput> {
|
|
865
|
+
const parsed = parseOperationArgs(operation, args, {
|
|
866
|
+
allowMissingRequired: !wantsJson(invocation) && canPrompt(),
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
if (
|
|
870
|
+
operation.createsWorkspace &&
|
|
871
|
+
parsed.input.name === undefined &&
|
|
872
|
+
!wantsJson(invocation) &&
|
|
873
|
+
canPrompt()
|
|
874
|
+
) {
|
|
875
|
+
parsed.input.name = await promptWorkspaceName();
|
|
876
|
+
}
|
|
877
|
+
if (operation.createsWorkspace && parsed.input.name !== undefined) {
|
|
878
|
+
assertValidWorkspaceName(parsed.input.name);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
enforceRequiredOperationInputs(operation, parsed);
|
|
882
|
+
return parsed;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function parseOperationArgs(
|
|
886
|
+
operation: RuntimeOperationDefinition,
|
|
887
|
+
args: string[],
|
|
888
|
+
options: { allowMissingRequired?: boolean } = {},
|
|
889
|
+
): ParsedOperationInput {
|
|
840
890
|
const cli = inferCliMetadata(operation);
|
|
841
891
|
const input: Record<string, unknown> = {};
|
|
842
892
|
const hostOptions: Record<string, unknown> = {};
|
|
@@ -875,19 +925,27 @@ function parseOperationArgs(operation: RuntimeOperationDefinition, args: string[
|
|
|
875
925
|
assignPositional(positionals, positionalIndex++, arg, input);
|
|
876
926
|
}
|
|
877
927
|
|
|
928
|
+
if (!options.allowMissingRequired) enforceRequiredOperationInputs(operation, { input, hostOptions });
|
|
929
|
+
|
|
930
|
+
return { input, hostOptions };
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function enforceRequiredOperationInputs(
|
|
934
|
+
operation: RuntimeOperationDefinition,
|
|
935
|
+
parsed: ParsedOperationInput,
|
|
936
|
+
): void {
|
|
937
|
+
const cli = inferCliMetadata(operation);
|
|
878
938
|
for (const option of cli.options ?? []) {
|
|
879
|
-
if (option.required && input[option.name] === undefined && hostOptions[option.name] === undefined) {
|
|
939
|
+
if (option.required && parsed.input[option.name] === undefined && parsed.hostOptions[option.name] === undefined) {
|
|
880
940
|
throw new Error(`Operation ${operation.id} requires ${option.flag}`);
|
|
881
941
|
}
|
|
882
942
|
}
|
|
883
943
|
|
|
884
944
|
for (const name of operation.inputSchema?.required ?? []) {
|
|
885
|
-
if (input[name] === undefined) {
|
|
945
|
+
if (parsed.input[name] === undefined) {
|
|
886
946
|
throw new Error(`Operation ${operation.id} requires ${name}`);
|
|
887
947
|
}
|
|
888
948
|
}
|
|
889
|
-
|
|
890
|
-
return { input, hostOptions };
|
|
891
949
|
}
|
|
892
950
|
|
|
893
951
|
function enforceHostOnlyBooleanGuards(
|
|
@@ -1090,7 +1148,7 @@ async function runHelp(invocation: CliInvocation): Promise<void> {
|
|
|
1090
1148
|
commands: [
|
|
1091
1149
|
{ name: "help", description: "Show Rigkit CLI help" },
|
|
1092
1150
|
{ name: "init", description: "Initialize a Rigkit project" },
|
|
1093
|
-
{ name: "
|
|
1151
|
+
{ name: "run", description: "Run a project operation exposed by the runtime" },
|
|
1094
1152
|
{ name: "ls", description: "List project workspaces" },
|
|
1095
1153
|
{ name: "projects", description: "Discover Rigkit projects below the current directory" },
|
|
1096
1154
|
{ name: "doctor", description: "Show Rigkit runtime diagnostics" },
|
|
@@ -1104,12 +1162,12 @@ async function runHelp(invocation: CliInvocation): Promise<void> {
|
|
|
1104
1162
|
`rig ${RIGKIT_CLI_VERSION}`,
|
|
1105
1163
|
"",
|
|
1106
1164
|
"Usage:",
|
|
1107
|
-
" rig [options] <command
|
|
1165
|
+
" rig [options] <command>",
|
|
1108
1166
|
"",
|
|
1109
1167
|
"Commands:",
|
|
1110
1168
|
" help Show Rigkit CLI help",
|
|
1111
1169
|
" init Initialize a Rigkit project",
|
|
1112
|
-
"
|
|
1170
|
+
" run Run a project operation exposed by the runtime",
|
|
1113
1171
|
" ls List project workspaces",
|
|
1114
1172
|
" projects Discover Rigkit projects below the current directory",
|
|
1115
1173
|
" doctor Show Rigkit runtime diagnostics",
|
|
@@ -1214,10 +1272,12 @@ async function runRuntimeOperation<T>(
|
|
|
1214
1272
|
return;
|
|
1215
1273
|
}
|
|
1216
1274
|
if (isHostCapabilityRequestEvent(event)) {
|
|
1217
|
-
|
|
1275
|
+
const suspendPresenter = hostCapabilityNeedsTerminal(event);
|
|
1276
|
+
const logger = createHostCapabilityLogger(event, presenter);
|
|
1277
|
+
if (suspendPresenter) presenter?.pause();
|
|
1218
1278
|
try {
|
|
1219
1279
|
if (sendSession) {
|
|
1220
|
-
await answerHostCapabilityRequestOverSession(sendSession, event);
|
|
1280
|
+
await answerHostCapabilityRequestOverSession(sendSession, event, { logger });
|
|
1221
1281
|
} else if (respond) {
|
|
1222
1282
|
await answerHostCapabilityRequestOverSession((message) => {
|
|
1223
1283
|
if (isRecord(message) && message.type === "response") {
|
|
@@ -1225,12 +1285,12 @@ async function runRuntimeOperation<T>(
|
|
|
1225
1285
|
if (id) return respond(id, "error" in message ? { error: message.error } : { result: message.result });
|
|
1226
1286
|
}
|
|
1227
1287
|
throw new Error(`Session response channel cannot send ${String(isRecord(message) ? message.type : typeof message)}`);
|
|
1228
|
-
}, event);
|
|
1288
|
+
}, event, { logger });
|
|
1229
1289
|
} else {
|
|
1230
|
-
await answerHostCapabilityRequest(runtime, event);
|
|
1290
|
+
await answerHostCapabilityRequest(runtime, event, { logger });
|
|
1231
1291
|
}
|
|
1232
1292
|
} finally {
|
|
1233
|
-
presenter?.resume();
|
|
1293
|
+
if (suspendPresenter) presenter?.resume();
|
|
1234
1294
|
}
|
|
1235
1295
|
return;
|
|
1236
1296
|
}
|
|
@@ -1352,6 +1412,7 @@ type HostCapabilityRequestEvent = {
|
|
|
1352
1412
|
type: "host.capability.request";
|
|
1353
1413
|
requestId?: string;
|
|
1354
1414
|
id?: string;
|
|
1415
|
+
nodePath?: string;
|
|
1355
1416
|
capability: string;
|
|
1356
1417
|
params: unknown;
|
|
1357
1418
|
};
|
|
@@ -1360,6 +1421,15 @@ type HostRequestHandlingOptions = {
|
|
|
1360
1421
|
quietOpen?: boolean;
|
|
1361
1422
|
};
|
|
1362
1423
|
|
|
1424
|
+
type HostCapabilityLogOptions = {
|
|
1425
|
+
stream?: "stdout" | "stderr" | "info";
|
|
1426
|
+
label?: string;
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1429
|
+
type HostCapabilityRequestHandlingOptions = {
|
|
1430
|
+
logger?: (data: string, options?: HostCapabilityLogOptions) => void;
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1363
1433
|
class UnsupportedHostCapabilityError extends Error {
|
|
1364
1434
|
constructor(capability: string) {
|
|
1365
1435
|
super(
|
|
@@ -1407,11 +1477,15 @@ async function answerHostRequestOverSession(
|
|
|
1407
1477
|
}
|
|
1408
1478
|
}
|
|
1409
1479
|
|
|
1410
|
-
async function answerHostCapabilityRequest(
|
|
1480
|
+
async function answerHostCapabilityRequest(
|
|
1481
|
+
runtime: RuntimeClient,
|
|
1482
|
+
event: HostCapabilityRequestEvent,
|
|
1483
|
+
options: HostCapabilityRequestHandlingOptions = {},
|
|
1484
|
+
): Promise<void> {
|
|
1411
1485
|
const requestId = event.requestId ?? event.id;
|
|
1412
1486
|
if (!requestId) throw new Error(`Host capability request is missing requestId`);
|
|
1413
1487
|
try {
|
|
1414
|
-
const handled = await handleHostCapabilityRequest(event.capability, event.params);
|
|
1488
|
+
const handled = await handleHostCapabilityRequest(event.capability, event.params, options);
|
|
1415
1489
|
await runtime.control.hostResponse(requestId, { result: handled.result });
|
|
1416
1490
|
} catch (error) {
|
|
1417
1491
|
await runtime.control.hostResponse(requestId, {
|
|
@@ -1423,11 +1497,12 @@ async function answerHostCapabilityRequest(runtime: RuntimeClient, event: HostCa
|
|
|
1423
1497
|
async function answerHostCapabilityRequestOverSession(
|
|
1424
1498
|
send: (message: unknown) => void | Promise<void>,
|
|
1425
1499
|
event: HostCapabilityRequestEvent,
|
|
1500
|
+
options: HostCapabilityRequestHandlingOptions = {},
|
|
1426
1501
|
): Promise<void> {
|
|
1427
1502
|
const id = event.id ?? event.requestId;
|
|
1428
1503
|
if (!id) throw new Error(`Host capability request is missing id`);
|
|
1429
1504
|
try {
|
|
1430
|
-
const handled = await handleHostCapabilityRequest(event.capability, event.params);
|
|
1505
|
+
const handled = await handleHostCapabilityRequest(event.capability, event.params, options);
|
|
1431
1506
|
await send({ type: "response", id, result: handled.result });
|
|
1432
1507
|
if (handled.closed) reportHostCapabilityClosed(send, id, handled.closed);
|
|
1433
1508
|
} catch (error) {
|
|
@@ -1473,6 +1548,34 @@ function hostRequestNeedsTerminal(event: HostRequestEvent): boolean {
|
|
|
1473
1548
|
}
|
|
1474
1549
|
}
|
|
1475
1550
|
|
|
1551
|
+
function hostCapabilityNeedsTerminal(event: HostCapabilityRequestEvent): boolean {
|
|
1552
|
+
switch (event.capability) {
|
|
1553
|
+
case "cmux.open":
|
|
1554
|
+
return false;
|
|
1555
|
+
default:
|
|
1556
|
+
return true;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function createHostCapabilityLogger(
|
|
1561
|
+
event: HostCapabilityRequestEvent,
|
|
1562
|
+
presenter: RunPresenter | undefined,
|
|
1563
|
+
): (data: string, options?: HostCapabilityLogOptions) => void {
|
|
1564
|
+
return (data, options = {}) => {
|
|
1565
|
+
if (presenter) {
|
|
1566
|
+
presenter.render({
|
|
1567
|
+
type: "log.output",
|
|
1568
|
+
nodePath: event.nodePath ?? "runtime",
|
|
1569
|
+
stream: options.stream ?? "info",
|
|
1570
|
+
label: options.label ?? event.capability,
|
|
1571
|
+
data,
|
|
1572
|
+
});
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
console.error(data);
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1476
1579
|
function isTrustedCaptureHostCommand(params: unknown): boolean {
|
|
1477
1580
|
return process.env.RIGKIT_TRUST_HOST_COMMANDS === "1" &&
|
|
1478
1581
|
isRecord(params) &&
|
|
@@ -1484,12 +1587,18 @@ type HandledHostCapability = {
|
|
|
1484
1587
|
closed?: Promise<void>;
|
|
1485
1588
|
};
|
|
1486
1589
|
|
|
1487
|
-
async function handleHostCapabilityRequest(
|
|
1590
|
+
async function handleHostCapabilityRequest(
|
|
1591
|
+
capability: string,
|
|
1592
|
+
params: unknown,
|
|
1593
|
+
options: HostCapabilityRequestHandlingOptions = {},
|
|
1594
|
+
): Promise<HandledHostCapability> {
|
|
1488
1595
|
const handler = CLI_HOST_CAPABILITY_HANDLERS.get(capability);
|
|
1489
1596
|
if (!handler) {
|
|
1490
1597
|
throw new UnsupportedHostCapabilityError(capability);
|
|
1491
1598
|
}
|
|
1492
|
-
return normalizeHostCapabilityResult(await handler.handle(params
|
|
1599
|
+
return normalizeHostCapabilityResult(await handler.handle(params, {
|
|
1600
|
+
log: (data, logOptions) => options.logger?.(data, logOptions),
|
|
1601
|
+
}));
|
|
1493
1602
|
}
|
|
1494
1603
|
|
|
1495
1604
|
function normalizeHostCapabilityResult(value: unknown): HandledHostCapability {
|
|
@@ -1685,6 +1794,7 @@ function isHostCapabilityRequestEvent(value: unknown): value is HostCapabilityRe
|
|
|
1685
1794
|
return isRecord(value) &&
|
|
1686
1795
|
value.type === "host.capability.request" &&
|
|
1687
1796
|
(typeof value.requestId === "string" || typeof value.id === "string") &&
|
|
1797
|
+
(value.nodePath === undefined || typeof value.nodePath === "string") &&
|
|
1688
1798
|
typeof value.capability === "string";
|
|
1689
1799
|
}
|
|
1690
1800
|
|
|
@@ -1739,6 +1849,11 @@ function printJson(value: unknown): void {
|
|
|
1739
1849
|
console.log(JSON.stringify(value, null, 2));
|
|
1740
1850
|
}
|
|
1741
1851
|
|
|
1852
|
+
function printInteractiveOutputGap(invocation: CliInvocation): void {
|
|
1853
|
+
if (wantsJson(invocation) || !process.stdout.isTTY) return;
|
|
1854
|
+
console.log("");
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1742
1857
|
function printPlan(plan: WorkflowPlan): void {
|
|
1743
1858
|
console.log(`${plan.workflow}: ${plan.cachedNodeCount}/${plan.nodeCount} nodes cached`);
|
|
1744
1859
|
|
|
@@ -1760,15 +1875,35 @@ function printWorkspaces(
|
|
|
1760
1875
|
}
|
|
1761
1876
|
|
|
1762
1877
|
printTable(
|
|
1763
|
-
["name", "workflow", "created"],
|
|
1878
|
+
["name", "workflow", "created", "age"],
|
|
1764
1879
|
workspaces.map((workspace) => [
|
|
1765
1880
|
workspace.name,
|
|
1766
1881
|
workspace.workflow,
|
|
1767
1882
|
workspace.createdAt,
|
|
1883
|
+
formatWorkspaceAge(workspace.createdAt),
|
|
1768
1884
|
]),
|
|
1769
1885
|
);
|
|
1770
1886
|
}
|
|
1771
1887
|
|
|
1888
|
+
function formatWorkspaceAge(createdAt: string): string {
|
|
1889
|
+
const createdTime = Date.parse(createdAt);
|
|
1890
|
+
if (Number.isNaN(createdTime)) return chalk.dim("unknown");
|
|
1891
|
+
|
|
1892
|
+
const ageMs = Math.max(0, Date.now() - createdTime);
|
|
1893
|
+
const minute = 60 * 1000;
|
|
1894
|
+
const hour = 60 * minute;
|
|
1895
|
+
const day = 24 * hour;
|
|
1896
|
+
const label = ageMs < hour
|
|
1897
|
+
? `${Math.max(1, Math.floor(ageMs / minute))}m`
|
|
1898
|
+
: ageMs < day
|
|
1899
|
+
? `${Math.floor(ageMs / hour)}h`
|
|
1900
|
+
: `${Math.floor(ageMs / day)}d`;
|
|
1901
|
+
|
|
1902
|
+
if (ageMs < day) return chalk.green(label);
|
|
1903
|
+
if (ageMs <= 3 * day) return chalk.yellow(label);
|
|
1904
|
+
return chalk.red(label);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1772
1907
|
function printSnapshots(snapshots: SnapshotRecord[]): void {
|
|
1773
1908
|
if (snapshots.length === 0) {
|
|
1774
1909
|
console.log("No snapshots.");
|
package/src/completion.test.ts
CHANGED
|
@@ -11,8 +11,8 @@ describe("CLI completion", () => {
|
|
|
11
11
|
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
12
12
|
const items = await completeRig({
|
|
13
13
|
cwd: projectDir,
|
|
14
|
-
words: ["rig", "ssh", ""],
|
|
15
|
-
currentIndex:
|
|
14
|
+
words: ["rig", "run", "ssh", ""],
|
|
15
|
+
currentIndex: 3,
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
expect(items.map((item) => item.value)).toEqual(["api", "web"]);
|
|
@@ -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", "ssh", "vm-"],
|
|
29
|
-
currentIndex:
|
|
28
|
+
words: ["rig", "run", "ssh", "vm-"],
|
|
29
|
+
currentIndex: 3,
|
|
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", "ssh", ""],
|
|
43
|
-
currentIndex:
|
|
42
|
+
words: ["rig", "-C", "project", "run", "ssh", ""],
|
|
43
|
+
currentIndex: 5,
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
expect(items.map((item) => item.value)).toEqual(["api", "web"]);
|
|
@@ -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)).toContain("api
|
|
58
|
+
expect(roots.map((item) => item.value)).toContain("api");
|
|
59
59
|
expect(roots.map((item) => item.value)).toContain("ssh");
|
|
60
60
|
|
|
61
|
-
const
|
|
61
|
+
const exactWorkspace = await completeRig({
|
|
62
|
+
cwd: projectDir,
|
|
63
|
+
words: ["rig", "run", "api"],
|
|
64
|
+
currentIndex: 2,
|
|
65
|
+
});
|
|
66
|
+
expect(exactWorkspace.map((item) => item.value)).toEqual(["api/remove", "api/open-cmux"]);
|
|
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(["api/remove", "api/open-cmux"]);
|
|
74
|
+
|
|
75
|
+
const slashWorkspace = await completeRig({
|
|
62
76
|
cwd: projectDir,
|
|
63
77
|
words: ["rig", "run", "api/"],
|
|
64
78
|
currentIndex: 2,
|
|
65
79
|
});
|
|
66
|
-
expect(
|
|
80
|
+
expect(slashWorkspace.map((item) => item.value)).toEqual(["api/remove", "api/open-cmux"]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("does not complete runtime operations 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", "s"],
|
|
90
|
+
currentIndex: 1,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(items.map((item) => item.value)).not.toContain("ssh");
|
|
67
94
|
});
|
|
68
95
|
});
|
|
69
96
|
|
|
@@ -72,7 +99,10 @@ 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 ''");
|
|
76
106
|
});
|
|
77
107
|
|
|
78
108
|
test("completes ls targets", async () => {
|
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 = {
|
|
@@ -102,31 +103,10 @@ export async function completeRig(input: CompleteRigInput): Promise<CompletionIt
|
|
|
102
103
|
if (expectsOptionValue(before)) return [];
|
|
103
104
|
|
|
104
105
|
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
106
|
return filterItems(
|
|
127
107
|
current.startsWith("-")
|
|
128
108
|
? GLOBAL_OPTIONS
|
|
129
|
-
: [...COMMANDS, ...
|
|
109
|
+
: [...COMMANDS, ...GLOBAL_OPTIONS],
|
|
130
110
|
current,
|
|
131
111
|
);
|
|
132
112
|
}
|
|
@@ -157,6 +137,9 @@ export async function completeRig(input: CompleteRigInput): Promise<CompletionIt
|
|
|
157
137
|
return filterItems(await safeOperationTargets(resolveProjectDir(words, cwd), current), current);
|
|
158
138
|
}
|
|
159
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
|
+
}
|
|
160
143
|
const operationPositionalCount = countRunOperationPositionals(run.args);
|
|
161
144
|
const positional = operation?.cli?.positionals?.find((item) => item.index === operationPositionalCount);
|
|
162
145
|
if (positional && /workspace|vm/i.test(positional.name)) {
|
|
@@ -178,6 +161,9 @@ export async function completeRig(input: CompleteRigInput): Promise<CompletionIt
|
|
|
178
161
|
export function formatCompletionItems(items: CompletionItem[], shell: CompletionShell): string {
|
|
179
162
|
const lines = items.map((item) => {
|
|
180
163
|
if (shell === "bash") return item.value;
|
|
164
|
+
if (shell === "zsh" && item.noSpace) {
|
|
165
|
+
return `${item.value}\t${item.description ?? ""}\tnospace`;
|
|
166
|
+
}
|
|
181
167
|
return item.description ? `${item.value}\t${item.description}` : item.value;
|
|
182
168
|
});
|
|
183
169
|
return lines.join("\n");
|
|
@@ -217,22 +203,33 @@ complete -c rig -f -a "(__rig_complete)"
|
|
|
217
203
|
`;
|
|
218
204
|
}
|
|
219
205
|
|
|
220
|
-
|
|
206
|
+
return `#compdef rig
|
|
221
207
|
# rig zsh completion
|
|
222
208
|
_rig() {
|
|
223
|
-
local -a raw
|
|
224
|
-
local line value description
|
|
209
|
+
local -a raw values descriptions nospace_values nospace_descriptions
|
|
210
|
+
local line value description rest marker
|
|
225
211
|
raw=("\${(@f)$(command rig __complete --shell zsh --index $((CURRENT - 1)) -- "\${words[@]}" 2>/dev/null)}")
|
|
226
212
|
for line in "\${raw[@]}"; do
|
|
227
213
|
value="\${line%%$'\\t'*}"
|
|
214
|
+
description=""
|
|
215
|
+
marker=""
|
|
228
216
|
if [[ "$line" == *$'\\t'* ]]; then
|
|
229
|
-
|
|
230
|
-
|
|
217
|
+
rest="\${line#*$'\\t'}"
|
|
218
|
+
description="\${rest%%$'\\t'*}"
|
|
219
|
+
if [[ "$rest" == *$'\\t'* ]]; then
|
|
220
|
+
marker="\${rest#*$'\\t'}"
|
|
221
|
+
fi
|
|
222
|
+
fi
|
|
223
|
+
if [[ "$marker" == "nospace" ]]; then
|
|
224
|
+
nospace_values+=("\${value}")
|
|
225
|
+
nospace_descriptions+=("\${description}")
|
|
231
226
|
else
|
|
232
|
-
|
|
227
|
+
values+=("\${value}")
|
|
228
|
+
descriptions+=("\${description}")
|
|
233
229
|
fi
|
|
234
230
|
done
|
|
235
|
-
|
|
231
|
+
(( \${#nospace_values} )) && compadd -S '' -d nospace_descriptions -a nospace_values
|
|
232
|
+
(( \${#values} )) && compadd -d descriptions -a values
|
|
236
233
|
}
|
|
237
234
|
compdef _rig rig
|
|
238
235
|
`;
|
|
@@ -298,21 +295,6 @@ function parseRunCommand(words: string[]): { operation?: string; args: string[]
|
|
|
298
295
|
return { operation: args[0], args: args.slice(1) };
|
|
299
296
|
}
|
|
300
297
|
|
|
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
298
|
function countRunOperationPositionals(args: string[]): number {
|
|
317
299
|
let count = 0;
|
|
318
300
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -392,15 +374,35 @@ async function operationTargets(
|
|
|
392
374
|
return workspaceOperationTargets(manifest, current);
|
|
393
375
|
}
|
|
394
376
|
const workspaces = await readWorkspaces(paths).catch(() => []);
|
|
377
|
+
if (workspaces.some((workspace) => workspace.name === current)) {
|
|
378
|
+
return workspaceOperationTargets(manifest, `${current}/`);
|
|
379
|
+
}
|
|
395
380
|
return manifest.operations.flatMap((operation) => [
|
|
396
381
|
{ value: operation.id, description: operation.description },
|
|
397
382
|
...(operation.aliases ?? []).map((alias) => ({ value: alias, description: operation.description })),
|
|
398
383
|
]).concat(workspaces.map((workspace) => ({
|
|
399
|
-
value:
|
|
384
|
+
value: workspace.name,
|
|
400
385
|
description: `workspace ${workspace.workflow}`,
|
|
386
|
+
noSpace: true,
|
|
401
387
|
})));
|
|
402
388
|
}
|
|
403
389
|
|
|
390
|
+
async function safeWorkspaceOperationTargets(
|
|
391
|
+
paths: { projectDir: string; configPath: string },
|
|
392
|
+
workspace: string,
|
|
393
|
+
): Promise<CompletionItem[]> {
|
|
394
|
+
try {
|
|
395
|
+
const [manifest, workspaces] = await Promise.all([
|
|
396
|
+
readOperations(paths),
|
|
397
|
+
readWorkspaces(paths),
|
|
398
|
+
]);
|
|
399
|
+
if (!workspaces.some((item) => item.name === workspace)) return [];
|
|
400
|
+
return workspaceOperationTargets(manifest, `${workspace}/`);
|
|
401
|
+
} catch {
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
404
406
|
function workspaceOperationTargets(manifest: RuntimeOperationManifest, current: string): CompletionItem[] {
|
|
405
407
|
const slash = current.indexOf("/");
|
|
406
408
|
if (slash < 0) return [];
|
package/src/init.test.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { initProject, normalizeMachineName } from "./init.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
FREESTYLE_PROVIDER_PACKAGE_NAME,
|
|
8
|
+
FREESTYLE_SDK_PACKAGE_NAME,
|
|
9
|
+
FREESTYLE_SDK_PACKAGE_VERSION,
|
|
10
|
+
PROJECT_PACKAGE_NAME,
|
|
11
|
+
} from "./project.ts";
|
|
7
12
|
import { RIGKIT_CLI_VERSION } from "./version.ts";
|
|
8
13
|
|
|
9
14
|
describe("initProject", () => {
|
|
@@ -14,7 +19,6 @@ describe("initProject", () => {
|
|
|
14
19
|
projectDir,
|
|
15
20
|
configPath: join(projectDir, "rig.config.ts"),
|
|
16
21
|
name: "Platform API",
|
|
17
|
-
apiKey: "fs_test_123",
|
|
18
22
|
});
|
|
19
23
|
|
|
20
24
|
expect(result.name).toBe("platform-api");
|
|
@@ -22,24 +26,27 @@ describe("initProject", () => {
|
|
|
22
26
|
expect(existsSync(projectDir)).toBe(true);
|
|
23
27
|
expect(result.created).toEqual({
|
|
24
28
|
config: true,
|
|
25
|
-
env:
|
|
26
|
-
envExample:
|
|
29
|
+
env: false,
|
|
30
|
+
envExample: false,
|
|
27
31
|
gitignore: true,
|
|
28
32
|
packageJson: true,
|
|
29
33
|
});
|
|
30
34
|
|
|
31
35
|
expect(readFileSync(join(projectDir, "rig.config.ts"), "utf8")).toContain('sequence("platform-api"');
|
|
32
36
|
expect(readFileSync(join(projectDir, "rig.config.ts"), "utf8")).toContain("defineConfig({");
|
|
33
|
-
expect(readFileSync(join(projectDir, ".
|
|
34
|
-
expect(readFileSync(join(projectDir, ".
|
|
37
|
+
expect(readFileSync(join(projectDir, "rig.config.ts"), "utf8")).toContain("new VmSpec()");
|
|
38
|
+
expect(readFileSync(join(projectDir, "rig.config.ts"), "utf8")).not.toContain("FREESTYLE_API_KEY");
|
|
39
|
+
expect(existsSync(join(projectDir, ".env"))).toBe(false);
|
|
40
|
+
expect(existsSync(join(projectDir, ".env.example"))).toBe(false);
|
|
35
41
|
expect(readFileSync(join(projectDir, ".gitignore"), "utf8")).toContain(".env\n.rigkit/\n");
|
|
36
42
|
|
|
37
43
|
const pkg = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8"));
|
|
38
44
|
expect(pkg.name).toBe("platform-api");
|
|
39
|
-
expect(pkg.scripts.plan).toBe("rig plan");
|
|
40
|
-
expect(pkg.scripts.apply).toBe("rig apply");
|
|
45
|
+
expect(pkg.scripts.plan).toBe("rig run plan");
|
|
46
|
+
expect(pkg.scripts.apply).toBe("rig run apply");
|
|
41
47
|
expect(pkg.devDependencies[PROJECT_PACKAGE_NAME]).toBe(RIGKIT_CLI_VERSION);
|
|
42
48
|
expect(pkg.devDependencies[FREESTYLE_PROVIDER_PACKAGE_NAME]).toBe(RIGKIT_CLI_VERSION);
|
|
49
|
+
expect(pkg.devDependencies[FREESTYLE_SDK_PACKAGE_NAME]).toBe(FREESTYLE_SDK_PACKAGE_VERSION);
|
|
43
50
|
});
|
|
44
51
|
|
|
45
52
|
test("updates existing project files without replacing package metadata", () => {
|
|
@@ -67,7 +74,7 @@ describe("initProject", () => {
|
|
|
67
74
|
const pkg = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8"));
|
|
68
75
|
expect(pkg.name).toBe("existing");
|
|
69
76
|
expect(pkg.scripts.test).toBe("bun test");
|
|
70
|
-
expect(pkg.scripts.plan).toBe("rig plan");
|
|
77
|
+
expect(pkg.scripts.plan).toBe("rig run plan");
|
|
71
78
|
});
|
|
72
79
|
});
|
|
73
80
|
|
package/src/init.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { RIGKIT_CLI_VERSION } from "./version.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
FREESTYLE_PROVIDER_PACKAGE_NAME,
|
|
6
|
+
FREESTYLE_SDK_PACKAGE_NAME,
|
|
7
|
+
FREESTYLE_SDK_PACKAGE_VERSION,
|
|
8
|
+
PROJECT_PACKAGE_NAME,
|
|
9
|
+
} from "./project.ts";
|
|
5
10
|
|
|
6
11
|
export type InitProjectInput = {
|
|
7
12
|
projectDir: string;
|
|
8
13
|
configPath: string;
|
|
9
14
|
name: string;
|
|
10
|
-
apiKey
|
|
15
|
+
apiKey?: string;
|
|
11
16
|
force?: boolean;
|
|
12
17
|
};
|
|
13
18
|
|
|
@@ -47,11 +52,14 @@ export function initProject(input: InitProjectInput): InitProjectResult {
|
|
|
47
52
|
writeFileSync(input.configPath, starterConfig(name));
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
const apiKey = input.apiKey?.trim();
|
|
50
56
|
const envPath = join(input.projectDir, ".env");
|
|
51
|
-
const env =
|
|
57
|
+
const env = apiKey
|
|
58
|
+
? writeEnvFile(envPath, apiKey)
|
|
59
|
+
: { created: false, updated: false };
|
|
52
60
|
|
|
53
61
|
const envExamplePath = join(input.projectDir, ".env.example");
|
|
54
|
-
const wroteEnvExample = !existsSync(envExamplePath);
|
|
62
|
+
const wroteEnvExample = Boolean(apiKey) && !existsSync(envExamplePath);
|
|
55
63
|
if (wroteEnvExample) {
|
|
56
64
|
writeFileSync(envExamplePath, "FREESTYLE_API_KEY=\n");
|
|
57
65
|
}
|
|
@@ -94,32 +102,46 @@ export function normalizeMachineName(value: string): string {
|
|
|
94
102
|
export function starterConfig(name: string): string {
|
|
95
103
|
const workflowName = JSON.stringify(normalizeMachineName(name));
|
|
96
104
|
|
|
97
|
-
return `import { defineConfig,
|
|
98
|
-
import { freestyle } from "@rigkit/provider-freestyle";
|
|
105
|
+
return `import { defineConfig, sequence } from "@rigkit/sdk";
|
|
106
|
+
import { freestyle, VmBaseImage, VmSpec } from "@rigkit/provider-freestyle";
|
|
99
107
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
const vmIdleTimeoutSeconds = 3600;
|
|
109
|
+
const vmSpec = new VmSpec()
|
|
110
|
+
.baseImage(new VmBaseImage("FROM node:22"))
|
|
111
|
+
.idleTimeoutSeconds(vmIdleTimeoutSeconds);
|
|
112
|
+
|
|
113
|
+
const freestyleProvider = freestyle.provider();
|
|
104
114
|
|
|
105
115
|
const dev = sequence(${workflowName})
|
|
106
|
-
.step("verify-node-22", async ({ providers }) => {
|
|
107
|
-
const vm = await providers.freestyle.vms.create(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
.step("verify-node-22", async ({ providers, step }) => {
|
|
117
|
+
const { vm, vmId } = await providers.freestyle.client.vms.create({
|
|
118
|
+
spec: vmSpec,
|
|
119
|
+
logger: step.log,
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
const result = await vm.exec("node --version");
|
|
123
|
+
if ((result.statusCode ?? 0) !== 0 || !result.stdout.trim().startsWith("v22.")) {
|
|
124
|
+
throw new Error(\`Expected Node.js v22, got: \${result.stdout}\${result.stderr}\`);
|
|
125
|
+
}
|
|
126
|
+
const snapshot = await vm.snapshot();
|
|
127
|
+
return { ctx: { snapshotId: snapshot.snapshotId } };
|
|
128
|
+
} finally {
|
|
129
|
+
await providers.freestyle.client.vms.delete({ vmId });
|
|
111
130
|
}
|
|
112
|
-
return { vm: await vm.snapshotRef() };
|
|
113
131
|
})
|
|
114
132
|
.workspace({
|
|
115
|
-
create: async ({ workflow, providers }) => {
|
|
116
|
-
const
|
|
133
|
+
create: async ({ workflow, providers, step }) => {
|
|
134
|
+
const { vmId } = await providers.freestyle.client.vms.create({
|
|
135
|
+
snapshotId: workflow.ctx.snapshotId,
|
|
136
|
+
idleTimeoutSeconds: vmIdleTimeoutSeconds,
|
|
137
|
+
logger: step.log,
|
|
138
|
+
});
|
|
117
139
|
return {
|
|
118
|
-
vmId
|
|
140
|
+
vmId,
|
|
119
141
|
};
|
|
120
142
|
},
|
|
121
143
|
remove: async ({ providers, workspace }) => {
|
|
122
|
-
await providers.freestyle.vms.delete(workspace.ctx.vmId);
|
|
144
|
+
await providers.freestyle.client.vms.delete({ vmId: workspace.ctx.vmId });
|
|
123
145
|
},
|
|
124
146
|
});
|
|
125
147
|
|
|
@@ -207,7 +229,7 @@ function ensureProjectPackageJson(
|
|
|
207
229
|
}
|
|
208
230
|
|
|
209
231
|
const scripts = pkg.scripts as Record<string, string>;
|
|
210
|
-
for (const [key, value] of Object.entries({ plan: "rig plan", apply: "rig apply" })) {
|
|
232
|
+
for (const [key, value] of Object.entries({ plan: "rig run plan", apply: "rig run apply" })) {
|
|
211
233
|
if (scripts[key] !== value) {
|
|
212
234
|
scripts[key] = value;
|
|
213
235
|
updated = true;
|
|
@@ -218,11 +240,13 @@ function ensureProjectPackageJson(
|
|
|
218
240
|
const devDependencies = isRecord(pkg.devDependencies) ? pkg.devDependencies : {};
|
|
219
241
|
const sdkDependencyChanged =
|
|
220
242
|
devDependencies[PROJECT_PACKAGE_NAME] !== RIGKIT_CLI_VERSION ||
|
|
221
|
-
devDependencies[FREESTYLE_PROVIDER_PACKAGE_NAME] !== RIGKIT_CLI_VERSION
|
|
243
|
+
devDependencies[FREESTYLE_PROVIDER_PACKAGE_NAME] !== RIGKIT_CLI_VERSION ||
|
|
244
|
+
devDependencies[FREESTYLE_SDK_PACKAGE_NAME] !== FREESTYLE_SDK_PACKAGE_VERSION;
|
|
222
245
|
if (sdkDependencyChanged) {
|
|
223
246
|
delete devDependencies["@rigkit/runtime"];
|
|
224
247
|
devDependencies[PROJECT_PACKAGE_NAME] = RIGKIT_CLI_VERSION;
|
|
225
248
|
devDependencies[FREESTYLE_PROVIDER_PACKAGE_NAME] = RIGKIT_CLI_VERSION;
|
|
249
|
+
devDependencies[FREESTYLE_SDK_PACKAGE_NAME] = FREESTYLE_SDK_PACKAGE_VERSION;
|
|
226
250
|
updated = true;
|
|
227
251
|
}
|
|
228
252
|
pkg.devDependencies = sortObject(devDependencies);
|
package/src/project.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
4
4
|
export const DEFAULT_CONFIG_FILE = "rig.config.ts";
|
|
5
5
|
export const PROJECT_PACKAGE_NAME = "@rigkit/sdk";
|
|
6
6
|
export const FREESTYLE_PROVIDER_PACKAGE_NAME = "@rigkit/provider-freestyle";
|
|
7
|
+
export const FREESTYLE_SDK_PACKAGE_NAME = "freestyle";
|
|
8
|
+
export const FREESTYLE_SDK_PACKAGE_VERSION = "^0.1.51";
|
|
7
9
|
|
|
8
10
|
export type ConfigPathOptions = {
|
|
9
11
|
project?: string;
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const RIGKIT_CLI_VERSION = "0.2.
|
|
1
|
+
export const RIGKIT_CLI_VERSION = "0.2.5";
|