@rigkit/cli 0.2.4 → 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 +126 -45
- package/src/completion.test.ts +39 -9
- package/src/completion.ts +47 -45
- package/src/init.test.ts +3 -3
- package/src/init.ts +1 -1
- 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/
|
|
29
|
-
"@rigkit/
|
|
30
|
-
"@rigkit/
|
|
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")
|
|
@@ -499,6 +468,52 @@ async function promptPackageManager(defaultValue: PackageManager): Promise<Packa
|
|
|
499
468
|
return answers.packageManager;
|
|
500
469
|
}
|
|
501
470
|
|
|
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(),
|
|
480
|
+
}]);
|
|
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;
|
|
515
|
+
}
|
|
516
|
+
|
|
502
517
|
async function runPackageManagerInstall(
|
|
503
518
|
projectDir: string,
|
|
504
519
|
packageManager: PackageManager,
|
|
@@ -580,7 +595,7 @@ function printInitResult(result: InitProjectResult, install: InitInstallResult):
|
|
|
580
595
|
if (install.skipped) {
|
|
581
596
|
console.log(` ${detectInstallCommand(result.packageJsonPath)}`);
|
|
582
597
|
}
|
|
583
|
-
console.log(" rig plan");
|
|
598
|
+
console.log(" rig run plan");
|
|
584
599
|
}
|
|
585
600
|
|
|
586
601
|
function displayProjectDir(projectDir: string): string {
|
|
@@ -649,6 +664,7 @@ async function runProjectOperation(
|
|
|
649
664
|
}
|
|
650
665
|
|
|
651
666
|
await renderOperationResult(operation, result, parsed.hostOptions);
|
|
667
|
+
printInteractiveOutputGap(invocation);
|
|
652
668
|
}
|
|
653
669
|
|
|
654
670
|
async function runDiscoveredProjectOperation(
|
|
@@ -704,6 +720,7 @@ async function runDiscoveredProjectOperation(
|
|
|
704
720
|
console.log(chalk.bold(displayProjectDir(project.projectDir)));
|
|
705
721
|
}
|
|
706
722
|
await renderOperationResult(operation, result, parsed.hostOptions);
|
|
723
|
+
printInteractiveOutputGap(invocation);
|
|
707
724
|
}
|
|
708
725
|
}
|
|
709
726
|
|
|
@@ -774,13 +791,13 @@ async function executeRuntimeOperation(
|
|
|
774
791
|
result: unknown;
|
|
775
792
|
}> {
|
|
776
793
|
const manifest = await readRuntimeOperations(runtime);
|
|
777
|
-
const resolved =
|
|
794
|
+
const resolved = await resolveRequestedRuntimeOperation(invocation, runtime, manifest, requestedOperation);
|
|
778
795
|
if (!resolved) {
|
|
779
796
|
throw new Error(`This project does not define a Rigkit operation named "${requestedOperation}".`);
|
|
780
797
|
}
|
|
781
798
|
const { operation, runOperation } = resolved;
|
|
782
799
|
|
|
783
|
-
const parsed =
|
|
800
|
+
const parsed = await parseOperationArgsWithPrompts(invocation, operation, args);
|
|
784
801
|
enforceHostOnlyBooleanGuards(operation, parsed);
|
|
785
802
|
|
|
786
803
|
const result = await runRuntimeOperation<unknown>(
|
|
@@ -793,6 +810,28 @@ async function executeRuntimeOperation(
|
|
|
793
810
|
return { operation, parsed, result };
|
|
794
811
|
}
|
|
795
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
|
+
|
|
796
835
|
function findRuntimeOperation(
|
|
797
836
|
manifest: RuntimeOperationManifest,
|
|
798
837
|
requestedOperation: string,
|
|
@@ -818,7 +857,36 @@ function parseWorkspaceOperationId(value: string): { workspace: string; operatio
|
|
|
818
857
|
};
|
|
819
858
|
}
|
|
820
859
|
|
|
821
|
-
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 {
|
|
822
890
|
const cli = inferCliMetadata(operation);
|
|
823
891
|
const input: Record<string, unknown> = {};
|
|
824
892
|
const hostOptions: Record<string, unknown> = {};
|
|
@@ -857,19 +925,27 @@ function parseOperationArgs(operation: RuntimeOperationDefinition, args: string[
|
|
|
857
925
|
assignPositional(positionals, positionalIndex++, arg, input);
|
|
858
926
|
}
|
|
859
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);
|
|
860
938
|
for (const option of cli.options ?? []) {
|
|
861
|
-
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) {
|
|
862
940
|
throw new Error(`Operation ${operation.id} requires ${option.flag}`);
|
|
863
941
|
}
|
|
864
942
|
}
|
|
865
943
|
|
|
866
944
|
for (const name of operation.inputSchema?.required ?? []) {
|
|
867
|
-
if (input[name] === undefined) {
|
|
945
|
+
if (parsed.input[name] === undefined) {
|
|
868
946
|
throw new Error(`Operation ${operation.id} requires ${name}`);
|
|
869
947
|
}
|
|
870
948
|
}
|
|
871
|
-
|
|
872
|
-
return { input, hostOptions };
|
|
873
949
|
}
|
|
874
950
|
|
|
875
951
|
function enforceHostOnlyBooleanGuards(
|
|
@@ -1072,7 +1148,7 @@ async function runHelp(invocation: CliInvocation): Promise<void> {
|
|
|
1072
1148
|
commands: [
|
|
1073
1149
|
{ name: "help", description: "Show Rigkit CLI help" },
|
|
1074
1150
|
{ name: "init", description: "Initialize a Rigkit project" },
|
|
1075
|
-
{ name: "
|
|
1151
|
+
{ name: "run", description: "Run a project operation exposed by the runtime" },
|
|
1076
1152
|
{ name: "ls", description: "List project workspaces" },
|
|
1077
1153
|
{ name: "projects", description: "Discover Rigkit projects below the current directory" },
|
|
1078
1154
|
{ name: "doctor", description: "Show Rigkit runtime diagnostics" },
|
|
@@ -1086,12 +1162,12 @@ async function runHelp(invocation: CliInvocation): Promise<void> {
|
|
|
1086
1162
|
`rig ${RIGKIT_CLI_VERSION}`,
|
|
1087
1163
|
"",
|
|
1088
1164
|
"Usage:",
|
|
1089
|
-
" rig [options] <command
|
|
1165
|
+
" rig [options] <command>",
|
|
1090
1166
|
"",
|
|
1091
1167
|
"Commands:",
|
|
1092
1168
|
" help Show Rigkit CLI help",
|
|
1093
1169
|
" init Initialize a Rigkit project",
|
|
1094
|
-
"
|
|
1170
|
+
" run Run a project operation exposed by the runtime",
|
|
1095
1171
|
" ls List project workspaces",
|
|
1096
1172
|
" projects Discover Rigkit projects below the current directory",
|
|
1097
1173
|
" doctor Show Rigkit runtime diagnostics",
|
|
@@ -1773,6 +1849,11 @@ function printJson(value: unknown): void {
|
|
|
1773
1849
|
console.log(JSON.stringify(value, null, 2));
|
|
1774
1850
|
}
|
|
1775
1851
|
|
|
1852
|
+
function printInteractiveOutputGap(invocation: CliInvocation): void {
|
|
1853
|
+
if (wantsJson(invocation) || !process.stdout.isTTY) return;
|
|
1854
|
+
console.log("");
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1776
1857
|
function printPlan(plan: WorkflowPlan): void {
|
|
1777
1858
|
console.log(`${plan.workflow}: ${plan.cachedNodeCount}/${plan.nodeCount} nodes cached`);
|
|
1778
1859
|
|
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
|
@@ -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 plan");
|
|
46
|
-
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");
|
|
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 plan");
|
|
77
|
+
expect(pkg.scripts.plan).toBe("rig run 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 plan", apply: "rig apply" })) {
|
|
232
|
+
for (const [key, value] of Object.entries({ plan: "rig run plan", apply: "rig run 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.5";
|