@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 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.4",
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/provider-cmux": "0.2.4",
29
- "@rigkit/engine": "0.2.4",
30
- "@rigkit/runtime-client": "0.2.4"
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("<operation> Run a project operation exposed by the runtime");
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("<operation> Run a project operation exposed by the runtime");
37
+ expect(help.stdout).toContain("run Run a project operation exposed by the runtime");
38
38
  });
39
39
 
40
- test("treats unknown root tokens as project operations", async () => {
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("No Rigkit config found");
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(normalizeOperationArgv(process.argv)).catch(handleCliError);
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|operation>")
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...]", { hidden: true })
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 = findRuntimeOperation(manifest, requestedOperation);
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 = parseOperationArgs(operation, args);
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 parseOperationArgs(operation: RuntimeOperationDefinition, args: string[]): ParsedOperationInput {
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: "<operation>", description: "Run a project operation exposed by the runtime" },
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|operation>",
1165
+ " rig [options] <command>",
1090
1166
  "",
1091
1167
  "Commands:",
1092
1168
  " help Show Rigkit CLI help",
1093
1169
  " init Initialize a Rigkit project",
1094
- " <operation> Run a project operation exposed by the runtime",
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
 
@@ -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: 2,
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: 2,
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: 4,
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 operations = await completeRig({
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(operations.map((item) => item.value)).toEqual(["api/remove", "api/open-cmux"]);
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, ...await safeOperationTargets(resolveProjectDir(words, cwd), current), ...GLOBAL_OPTIONS],
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
- return `#compdef rig
206
+ return `#compdef rig
221
207
  # rig zsh completion
222
208
  _rig() {
223
- local -a raw completions
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
- description="\${line#*$'\\t'}"
230
- completions+=("\${value}:\${description}")
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
- completions+=("\${value}")
227
+ values+=("\${value}")
228
+ descriptions+=("\${description}")
233
229
  fi
234
230
  done
235
- _describe 'rig' completions
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: `${workspace.name}/`,
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.4";
1
+ export const RIGKIT_CLI_VERSION = "0.2.5";