@rigkit/cli 0.2.9 → 0.2.11

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/src/completion.ts CHANGED
@@ -18,6 +18,104 @@ type CompleteRigInput = {
18
18
  cwd?: string;
19
19
  };
20
20
 
21
+ type CommandName =
22
+ | "help"
23
+ | "init"
24
+ | "plan"
25
+ | "apply"
26
+ | "create"
27
+ | "rm"
28
+ | "run"
29
+ | "ls"
30
+ | "cache"
31
+ | "providers"
32
+ | "projects"
33
+ | "doctor"
34
+ | "version"
35
+ | "completion";
36
+
37
+ type CompletionContext = {
38
+ cwd: string;
39
+ words: string[];
40
+ currentIndex: number;
41
+ current: string;
42
+ before: string[];
43
+ command?: CommandName;
44
+ commandIndex?: number;
45
+ argsBefore: string[];
46
+ unknownRootPositionals: string[];
47
+ };
48
+
49
+ type ValueCompletionKind =
50
+ | "directories"
51
+ | "config-files"
52
+ | "filesystem"
53
+ | "package-managers";
54
+
55
+ type OptionDefinition = {
56
+ flags: string[];
57
+ completions?: Array<{ value: string; noSpace?: boolean }>;
58
+ description: string;
59
+ group: string;
60
+ takesValue?: boolean;
61
+ valueKind?: ValueCompletionKind;
62
+ operation?: RuntimeOperationDefinition;
63
+ runtimeOption?: RuntimeOperationCliOption;
64
+ };
65
+
66
+ type RuntimeOperationManifest = {
67
+ operations: RuntimeOperationDefinition[];
68
+ workspaceOperations?: RuntimeOperationDefinition[];
69
+ };
70
+
71
+ type RuntimeOperationDefinition = {
72
+ id: string;
73
+ aliases?: string[];
74
+ title?: string;
75
+ description?: string;
76
+ createsWorkspace?: boolean;
77
+ cli?: {
78
+ positionals?: Array<{ name: string; index: number }>;
79
+ options?: RuntimeOperationCliOption[];
80
+ };
81
+ inputSchema?: {
82
+ properties?: Record<string, JsonSchemaProperty>;
83
+ required?: string[];
84
+ };
85
+ };
86
+
87
+ type RuntimeOperationCliOption = {
88
+ name: string;
89
+ flag: string;
90
+ aliases?: string[];
91
+ required?: boolean;
92
+ runtime?: boolean;
93
+ type?: "string" | "boolean" | "number";
94
+ };
95
+
96
+ type JsonSchemaProperty = {
97
+ type?: string;
98
+ description?: string;
99
+ default?: unknown;
100
+ enum?: unknown[];
101
+ };
102
+
103
+ type RuntimeWorkspaceCompletion = {
104
+ name: string;
105
+ workflow: string;
106
+ createdAt: string;
107
+ updatedAt: string;
108
+ };
109
+
110
+ type RuntimeCacheCompletionEntry = {
111
+ scope: "local" | "global";
112
+ workflow: string;
113
+ nodePath: string;
114
+ nodeName: string;
115
+ invalidated: boolean;
116
+ createdAt: string;
117
+ };
118
+
21
119
  const GROUP_COMMANDS = "Commands";
22
120
  const GROUP_SUBCOMMANDS = "Subcommands";
23
121
  const GROUP_FLAGS = "Flags";
@@ -28,6 +126,8 @@ const GROUP_OPERATIONS = "Operations";
28
126
  const GROUP_VALUES = "Values";
29
127
  const GROUP_PATHS = "Paths";
30
128
  const GROUP_SHELLS = "Shells";
129
+ const GROUP_CACHE = "Cache entries";
130
+ const GROUP_PROVIDERS = "Providers";
31
131
 
32
132
  const COMMANDS: CompletionItem[] = withGroup(GROUP_COMMANDS, [
33
133
  { value: "help", description: "show CLI help" },
@@ -39,245 +139,656 @@ const COMMANDS: CompletionItem[] = withGroup(GROUP_COMMANDS, [
39
139
  { value: "run", description: "run a workspace operation" },
40
140
  { value: "ls", description: "list project workspaces" },
41
141
  { value: "cache", description: "inspect and clear Rigkit cache" },
142
+ { value: "providers", description: "manage provider-owned local state" },
42
143
  { value: "projects", description: "discover Rigkit projects" },
43
144
  { value: "doctor", description: "show runtime diagnostics" },
44
145
  { value: "version", description: "show CLI version" },
45
146
  { value: "completion", description: "generate shell completion" },
46
147
  ]);
47
148
 
48
- const COMMAND_ALIASES = new Map<string, string>();
149
+ const COMMAND_NAMES = new Set(COMMANDS.map((command) => command.value as CommandName));
150
+
151
+ const JSON_OPTION = option(["--json"], "print JSON");
152
+ const HELP_OPTION = option(["--help"], "show help");
153
+
154
+ const GLOBAL_OPTIONS: OptionDefinition[] = [
155
+ option(["--chdir", "-chdir"], "working directory", {
156
+ group: GROUP_GLOBAL,
157
+ takesValue: true,
158
+ valueKind: "directories",
159
+ completions: [
160
+ { value: "--chdir=", noSpace: true },
161
+ ],
162
+ }),
163
+ option(["--config", "-config"], "config file", {
164
+ group: GROUP_GLOBAL,
165
+ takesValue: true,
166
+ valueKind: "config-files",
167
+ completions: [
168
+ { value: "--config=", noSpace: true },
169
+ ],
170
+ }),
171
+ option(["--state", "-state"], "state database path", {
172
+ group: GROUP_GLOBAL,
173
+ takesValue: true,
174
+ valueKind: "filesystem",
175
+ completions: [
176
+ { value: "--state=", noSpace: true },
177
+ ],
178
+ }),
179
+ option(["--json", "-json"], "print JSON", {
180
+ group: GROUP_GLOBAL,
181
+ completions: [{ value: "--json" }],
182
+ }),
183
+ option(["--help", "-help"], "show help", {
184
+ group: GROUP_GLOBAL,
185
+ completions: [{ value: "--help" }],
186
+ }),
187
+ option(["--version", "-version", "-v"], "show version", {
188
+ group: GROUP_GLOBAL,
189
+ completions: [{ value: "--version" }, { value: "-v" }],
190
+ }),
191
+ ];
192
+
193
+ const COMMAND_OPTIONS: Record<CommandName, OptionDefinition[]> = {
194
+ init: [
195
+ option(["--name"], "project and workflow name", { takesValue: true }),
196
+ option(["--api-key"], "Freestyle API key", { takesValue: true }),
197
+ option(["--package-manager"], "npm, bun, pnpm, or skip", {
198
+ takesValue: true,
199
+ valueKind: "package-managers",
200
+ }),
201
+ option(["--force"], "overwrite existing config"),
202
+ JSON_OPTION,
203
+ HELP_OPTION,
204
+ ],
205
+ plan: [
206
+ option(["--all"], "run against every discovered project"),
207
+ option(["--discover"], "discover projects below the selected directory"),
208
+ JSON_OPTION,
209
+ HELP_OPTION,
210
+ ],
211
+ apply: [
212
+ option(["--all"], "run against every discovered project"),
213
+ option(["--discover"], "discover projects below the selected directory"),
214
+ JSON_OPTION,
215
+ HELP_OPTION,
216
+ ],
217
+ create: [
218
+ JSON_OPTION,
219
+ HELP_OPTION,
220
+ ],
221
+ rm: [
222
+ option(["-y", "--yes"], "skip confirmation"),
223
+ option(["--all"], "remove every workspace"),
224
+ JSON_OPTION,
225
+ HELP_OPTION,
226
+ ],
227
+ run: [
228
+ JSON_OPTION,
229
+ HELP_OPTION,
230
+ ],
231
+ ls: [
232
+ JSON_OPTION,
233
+ HELP_OPTION,
234
+ ],
235
+ cache: [
236
+ HELP_OPTION,
237
+ ],
238
+ providers: [
239
+ HELP_OPTION,
240
+ ],
241
+ projects: [
242
+ JSON_OPTION,
243
+ HELP_OPTION,
244
+ ],
245
+ doctor: [
246
+ option(["--cli"], "show CLI diagnostics only"),
247
+ JSON_OPTION,
248
+ HELP_OPTION,
249
+ ],
250
+ version: [
251
+ JSON_OPTION,
252
+ HELP_OPTION,
253
+ ],
254
+ help: [
255
+ JSON_OPTION,
256
+ HELP_OPTION,
257
+ ],
258
+ completion: [
259
+ HELP_OPTION,
260
+ ],
261
+ };
49
262
 
50
- const GLOBAL_OPTIONS: CompletionItem[] = withGroup(GROUP_GLOBAL, [
51
- { value: "-chdir=", description: "working directory", noSpace: true },
52
- { value: "-config=", description: "config file", noSpace: true },
53
- { value: "-state=", description: "state database path", noSpace: true },
54
- { value: "-json", description: "print JSON" },
55
- { value: "-help", description: "show help" },
56
- { value: "-version", description: "show version" },
263
+ const CORE_OPERATION_OPTIONS: Partial<Record<CommandName, OptionDefinition[]>> = {
264
+ plan: [
265
+ option(["--workflow"], "workflow name", { takesValue: true }),
266
+ ],
267
+ apply: [
268
+ option(["--workflow"], "workflow name", { takesValue: true }),
269
+ option(["--dry-run"], "plan without applying changes"),
270
+ ],
271
+ create: [
272
+ option(["--workflow"], "workflow name", { takesValue: true }),
273
+ option(["--name"], "workspace name", { takesValue: true }),
274
+ ],
275
+ };
276
+
277
+ const LIST_TARGETS: CompletionItem[] = withGroup(GROUP_TARGETS, [
278
+ { value: "workspaces", description: "list workspaces" },
279
+ { value: "snapshots", description: "list snapshots" },
280
+ { value: "config", description: "show project config" },
57
281
  ]);
58
282
 
59
- const COMMAND_OPTIONS: Record<string, CompletionItem[]> = {
60
- init: withGroup(GROUP_FLAGS, [
61
- { value: "--name", description: "project and workflow name" },
62
- { value: "--api-key", description: "Freestyle API key" },
63
- { value: "--package-manager", description: "npm, bun, pnpm, or skip" },
64
- { value: "--force", description: "overwrite existing config" },
65
- { value: "--json", description: "print JSON" },
66
- ]),
67
- plan: withGroup(GROUP_FLAGS, [
68
- { value: "--all", description: "run against every discovered project" },
69
- { value: "--discover", description: "discover projects below the selected directory" },
70
- { value: "--json", description: "print JSON" },
71
- ]),
72
- apply: withGroup(GROUP_FLAGS, [
73
- { value: "--all", description: "run against every discovered project" },
74
- { value: "--discover", description: "discover projects below the selected directory" },
75
- { value: "--json", description: "print JSON" },
76
- ]),
77
- create: withGroup(GROUP_FLAGS, [
78
- { value: "--json", description: "print JSON" },
79
- ]),
80
- rm: withGroup(GROUP_FLAGS, [
81
- { value: "-y", description: "skip confirmation" },
82
- { value: "--yes", description: "skip confirmation" },
83
- { value: "--json", description: "print JSON" },
84
- ]),
85
- run: withGroup(GROUP_FLAGS, [
86
- { value: "--json", description: "print JSON" },
87
- ]),
283
+ const CACHE_SUBCOMMANDS: CompletionItem[] = withGroup(GROUP_SUBCOMMANDS, [
284
+ { value: "ls", description: "list cache entries" },
285
+ { value: "clear", description: "clear cache entries" },
286
+ { value: "invalidate", description: "mark cached task outputs stale" },
287
+ ]);
288
+
289
+ const CACHE_SUBCOMMAND_OPTIONS: Record<string, OptionDefinition[]> = {
88
290
  ls: [
89
- ...withGroup(GROUP_TARGETS, [
90
- { value: "workspaces", description: "list workspaces" },
91
- { value: "snapshots", description: "list snapshots" },
92
- { value: "config", description: "show project config" },
93
- ]),
94
- ...withGroup(GROUP_FLAGS, [
95
- { value: "--json", description: "print JSON" },
96
- ]),
291
+ JSON_OPTION,
292
+ HELP_OPTION,
293
+ ],
294
+ clear: [
295
+ option(["--local"], "clear local cache entries"),
296
+ option(["--global"], "clear global cache fragments"),
297
+ option(["--all"], "clear every global fragment with --global"),
298
+ JSON_OPTION,
299
+ HELP_OPTION,
300
+ ],
301
+ invalidate: [
302
+ option(["--all"], "invalidate every cached task"),
303
+ option(["-y", "--yes"], "skip confirmation"),
304
+ JSON_OPTION,
305
+ HELP_OPTION,
97
306
  ],
98
- projects: withGroup(GROUP_FLAGS, [
99
- { value: "--json", description: "print JSON" },
100
- ]),
101
- cache: withGroup(GROUP_SUBCOMMANDS, [
102
- { value: "ls", description: "list cache entries" },
103
- { value: "clear", description: "clear cache entries" },
104
- ]),
105
- completion: withGroup(GROUP_SHELLS, [
106
- { value: "bash", description: "Bash completion" },
107
- { value: "fish", description: "fish completion" },
108
- { value: "zsh", description: "zsh completion" },
109
- ]),
110
307
  };
111
308
 
112
- const CACHE_SUBCOMMAND_OPTIONS: Record<string, CompletionItem[]> = {
113
- ls: withGroup(GROUP_FLAGS, [
114
- { value: "--json", description: "print JSON" },
115
- ]),
116
- clear: withGroup(GROUP_FLAGS, [
117
- { value: "--local", description: "clear local cache entries" },
118
- { value: "--global", description: "clear global cache fragments" },
119
- { value: "--all", description: "clear every global fragment" },
120
- { value: "--json", description: "print JSON" },
309
+ const PROVIDER_TARGETS: CompletionItem[] = withGroup(GROUP_PROVIDERS, [
310
+ { value: "freestyle", description: "Freestyle provider state" },
311
+ ]);
312
+
313
+ const PROVIDER_SUBCOMMANDS: Record<string, CompletionItem[]> = {
314
+ freestyle: withGroup(GROUP_SUBCOMMANDS, [
315
+ { value: "clear", description: "clear Freestyle provider local auth and identity state" },
121
316
  ]),
122
317
  };
123
318
 
319
+ const PROVIDER_TARGET_OPTIONS: Record<string, OptionDefinition[]> = {
320
+ freestyle: [
321
+ HELP_OPTION,
322
+ ],
323
+ };
324
+
325
+ const PROVIDER_SUBCOMMAND_OPTIONS: Record<string, Record<string, OptionDefinition[]>> = {
326
+ freestyle: {
327
+ clear: [
328
+ JSON_OPTION,
329
+ HELP_OPTION,
330
+ ],
331
+ },
332
+ };
333
+
334
+ const COMPLETION_SHELLS: CompletionItem[] = withGroup(GROUP_SHELLS, [
335
+ { value: "bash", description: "Bash completion" },
336
+ { value: "fish", description: "fish completion" },
337
+ { value: "zsh", description: "zsh completion" },
338
+ ]);
339
+
340
+ const PROJECT_OPERATION_COMMANDS = new Set<CommandName>(["plan", "apply", "create"]);
341
+
124
342
  function withGroup(group: string, items: Omit<CompletionItem, "group">[]): CompletionItem[] {
125
343
  return items.map((item) => ({ ...item, group }));
126
344
  }
127
345
 
128
- const PROJECT_OPERATION_COMMANDS = new Set(["plan", "apply", "create"]);
346
+ function option(
347
+ flags: string[],
348
+ description: string,
349
+ input: Partial<Omit<OptionDefinition, "flags" | "description">> = {},
350
+ ): OptionDefinition {
351
+ return {
352
+ flags,
353
+ description,
354
+ group: input.group ?? GROUP_FLAGS,
355
+ takesValue: input.takesValue,
356
+ valueKind: input.valueKind,
357
+ completions: input.completions,
358
+ operation: input.operation,
359
+ runtimeOption: input.runtimeOption,
360
+ };
361
+ }
129
362
 
130
- const OPTIONS_WITH_VALUES = new Set([
131
- "-chdir",
132
- "--chdir",
133
- "-config",
134
- "--config",
135
- "-state",
136
- "--state",
137
- "--name",
138
- "--api-key",
139
- "--package-manager",
140
- ]);
363
+ export async function completeRig(input: CompleteRigInput): Promise<CompletionItem[]> {
364
+ const context = completionContext(input);
141
365
 
142
- type RuntimeOperationManifest = {
143
- operations: RuntimeOperationDefinition[];
144
- workspaceOperations?: RuntimeOperationDefinition[];
145
- };
366
+ const valueRequest = await optionValueRequest(context);
367
+ if (valueRequest) {
368
+ return await completeOptionValue(valueRequest);
369
+ }
146
370
 
147
- type RuntimeOperationDefinition = {
148
- id: string;
149
- aliases?: string[];
150
- description?: string;
151
- cli?: {
152
- positionals?: Array<{ name: string; index: number }>;
153
- options?: Array<{ name: string; flag: string; aliases?: string[]; runtime?: boolean; type?: string }>;
154
- };
155
- };
371
+ if (!context.command) {
372
+ if (context.unknownRootPositionals.length > 0) return [];
373
+ return filterItems(
374
+ context.current.startsWith("-")
375
+ ? optionItems(GLOBAL_OPTIONS)
376
+ : [...COMMANDS, ...optionItems(GLOBAL_OPTIONS)],
377
+ context.current,
378
+ );
379
+ }
156
380
 
157
- type RuntimeWorkspaceCompletion = {
158
- name: string;
159
- workflow: string;
160
- createdAt: string;
161
- updatedAt: string;
162
- };
381
+ return await completeCommand(context);
382
+ }
163
383
 
164
- export async function completeRig(input: CompleteRigInput): Promise<CompletionItem[]> {
384
+ function completionContext(input: CompleteRigInput): CompletionContext {
165
385
  const cwd = input.cwd ?? process.cwd();
166
386
  const words = input.words.length > 0 ? input.words : ["rig"];
167
387
  const currentIndex = input.currentIndex ?? Math.max(0, words.length - 1);
168
388
  const current = words[currentIndex] ?? "";
169
389
  const before = words.slice(1, currentIndex);
170
- const command = findCommand(before);
390
+ const unknownRootPositionals: string[] = [];
391
+ let command: CommandName | undefined;
392
+ let commandIndex: number | undefined;
393
+
394
+ for (let index = 0; index < before.length; index += 1) {
395
+ const word = before[index]!;
396
+ const globalOption = findOption(GLOBAL_OPTIONS, word);
397
+ if (globalOption?.takesValue && !hasInlineValue(word)) {
398
+ index += 1;
399
+ continue;
400
+ }
401
+ if (isOptionToken(word)) continue;
171
402
 
172
- const inlineOption = parseInlineValueOption(current);
173
- if (inlineOption) {
174
- return await completeOptionValue({
175
- option: inlineOption.option,
176
- current: inlineOption.value,
177
- cwd,
178
- words,
179
- inlinePrefix: inlineOption.prefix,
180
- });
403
+ if (isCommandName(word)) {
404
+ command = word;
405
+ commandIndex = index;
406
+ break;
407
+ }
408
+
409
+ unknownRootPositionals.push(word);
181
410
  }
182
411
 
183
- const valueOption = optionExpectingValue(before);
184
- if (valueOption) {
185
- return await completeOptionValue({
186
- option: valueOption,
187
- current,
188
- cwd,
189
- words,
190
- });
412
+ return {
413
+ cwd,
414
+ words,
415
+ currentIndex,
416
+ current,
417
+ before,
418
+ command,
419
+ commandIndex,
420
+ argsBefore: commandIndex === undefined ? [] : before.slice(commandIndex + 1),
421
+ unknownRootPositionals,
422
+ };
423
+ }
424
+
425
+ async function optionValueRequest(context: CompletionContext): Promise<{
426
+ option: OptionDefinition;
427
+ current: string;
428
+ cwd: string;
429
+ words: string[];
430
+ inlinePrefix?: string;
431
+ } | undefined> {
432
+ const inlineOption = parseInlineValueOption(context.current);
433
+ if (inlineOption) {
434
+ const definition = await resolveOptionDefinition(context, inlineOption.option);
435
+ if (definition?.takesValue || definition?.runtimeOption?.type === "boolean") {
436
+ return {
437
+ option: definition,
438
+ current: inlineOption.value,
439
+ cwd: context.cwd,
440
+ words: context.words,
441
+ inlinePrefix: inlineOption.prefix,
442
+ };
443
+ }
191
444
  }
192
445
 
193
- if (!command) {
194
- return filterItems(
195
- current.startsWith("-")
196
- ? GLOBAL_OPTIONS
197
- : [...COMMANDS, ...GLOBAL_OPTIONS],
198
- current,
199
- );
446
+ const previous = context.before.at(-1);
447
+ if (!previous) return undefined;
448
+ if (hasInlineValue(previous)) return undefined;
449
+
450
+ const definition = await resolveOptionDefinition(context, previous);
451
+ if (!definition?.takesValue) return undefined;
452
+ return {
453
+ option: definition,
454
+ current: context.current,
455
+ cwd: context.cwd,
456
+ words: context.words,
457
+ };
458
+ }
459
+
460
+ async function resolveOptionDefinition(
461
+ context: CompletionContext,
462
+ flag: string,
463
+ ): Promise<OptionDefinition | undefined> {
464
+ const globalOption = findOption(GLOBAL_OPTIONS, flag);
465
+ if (globalOption) return globalOption;
466
+ if (!context.command) return undefined;
467
+
468
+ const commandOptions = await optionsForCommandContext(context);
469
+ return findOption(commandOptions, flag);
470
+ }
471
+
472
+ async function optionsForCommandContext(context: CompletionContext): Promise<OptionDefinition[]> {
473
+ if (!context.command) return GLOBAL_OPTIONS;
474
+
475
+ if (PROJECT_OPERATION_COMMANDS.has(context.command)) {
476
+ const operation = await safeResolveRuntimeOperation(resolveProjectDir(context.words, context.cwd), context.command);
477
+ return mergeOptions([
478
+ ...operationOptions(operation),
479
+ ...(CORE_OPERATION_OPTIONS[context.command] ?? []),
480
+ ...COMMAND_OPTIONS[context.command],
481
+ ]);
200
482
  }
201
483
 
202
- if (current.startsWith("-")) {
203
- if (command === "rm") {
204
- const remove = parseRemoveCommand(before);
205
- if (remove.workspace) {
206
- const operation = await safeResolveWorkspaceOperation(resolveProjectDir(words, cwd), "remove");
207
- return filterItems([
208
- ...(operation?.cli?.options ?? []).flatMap((option) => [
209
- { value: option.flag, description: option.name, group: GROUP_FLAGS },
210
- ...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name, group: GROUP_FLAGS })),
211
- ]),
212
- ...COMMAND_OPTIONS.rm,
213
- ...GLOBAL_OPTIONS,
214
- ], current);
215
- }
216
- }
217
- if (command === "run") {
218
- const run = parseWorkspaceRunCommand(before);
219
- if (run.workspace && run.operation) {
220
- const operation = await safeResolveWorkspaceOperation(resolveProjectDir(words, cwd), run.operation);
221
- return filterItems([
222
- ...(operation?.cli?.options ?? []).flatMap((option) => [
223
- { value: option.flag, description: option.name, group: GROUP_FLAGS },
224
- ...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name, group: GROUP_FLAGS })),
225
- ]),
226
- ...COMMAND_OPTIONS.run,
227
- ...GLOBAL_OPTIONS,
228
- ], current);
229
- }
484
+ if (context.command === "run") {
485
+ const run = parseRunArgs(context);
486
+ if (run.workspace && run.operation) {
487
+ const operation = await safeResolveWorkspaceOperation(resolveProjectDir(context.words, context.cwd), run.operation);
488
+ return mergeOptions([
489
+ ...operationOptions(operation),
490
+ ...COMMAND_OPTIONS.run,
491
+ ]);
230
492
  }
231
- if (PROJECT_OPERATION_COMMANDS.has(command)) {
232
- const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), command);
233
- return filterItems([
234
- ...(operation?.cli?.options ?? []).flatMap((option) => [
235
- { value: option.flag, description: option.name },
236
- ...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name })),
237
- ]),
238
- ...(COMMAND_OPTIONS[command] ?? []),
239
- ...GLOBAL_OPTIONS,
240
- ], current);
493
+ }
494
+
495
+ if (context.command === "rm") {
496
+ const remove = parseRmArgs(context);
497
+ if (remove.workspace) {
498
+ const operation = await safeResolveWorkspaceOperation(resolveProjectDir(context.words, context.cwd), "remove");
499
+ return mergeOptions([
500
+ ...operationOptions(operation),
501
+ ...COMMAND_OPTIONS.rm,
502
+ ]);
241
503
  }
242
- if (command === "cache") {
243
- return filterItems([
244
- ...cacheOptionTargets(before),
245
- ...GLOBAL_OPTIONS,
246
- ], current);
504
+ }
505
+
506
+ if (context.command === "cache") {
507
+ const cache = parseCacheArgs(context);
508
+ if (cache.subcommand) return CACHE_SUBCOMMAND_OPTIONS[cache.subcommand] ?? [HELP_OPTION];
509
+ }
510
+
511
+ if (context.command === "providers") {
512
+ const providers = parseProvidersArgs(context);
513
+ if (providers.provider && providers.subcommand) {
514
+ return PROVIDER_SUBCOMMAND_OPTIONS[providers.provider]?.[providers.subcommand] ?? [HELP_OPTION];
247
515
  }
248
- return filterItems([...(COMMAND_OPTIONS[command] ?? []), ...GLOBAL_OPTIONS], current);
516
+ if (providers.provider) return PROVIDER_TARGET_OPTIONS[providers.provider] ?? [HELP_OPTION];
517
+ }
518
+
519
+ return COMMAND_OPTIONS[context.command] ?? [];
520
+ }
521
+
522
+ async function completeOptionValue(input: {
523
+ option: OptionDefinition;
524
+ current: string;
525
+ cwd: string;
526
+ words: string[];
527
+ inlinePrefix?: string;
528
+ }): Promise<CompletionItem[]> {
529
+ let items: CompletionItem[];
530
+ switch (input.option.valueKind) {
531
+ case "directories":
532
+ items = completeDirectories(input.cwd, input.current);
533
+ break;
534
+ case "config-files":
535
+ items = completeConfigPaths(projectBaseDir(input.words, input.cwd), input.current);
536
+ break;
537
+ case "filesystem":
538
+ items = completeFilesystemPaths(input.cwd, input.current);
539
+ break;
540
+ case "package-managers":
541
+ items = filterItems([
542
+ { value: "npm", group: GROUP_VALUES },
543
+ { value: "bun", group: GROUP_VALUES },
544
+ { value: "pnpm", group: GROUP_VALUES },
545
+ { value: "skip", group: GROUP_VALUES },
546
+ ], input.current);
547
+ break;
548
+ default:
549
+ items = await completeRuntimeOptionValue(input.option, input.current, input.words, input.cwd);
550
+ break;
551
+ }
552
+
553
+ if (!input.inlinePrefix) return items;
554
+ return items.map((item) => ({
555
+ ...item,
556
+ value: `${input.inlinePrefix}${item.value}`,
557
+ }));
558
+ }
559
+
560
+ async function completeRuntimeOptionValue(
561
+ option: OptionDefinition,
562
+ current: string,
563
+ words: string[],
564
+ cwd: string,
565
+ ): Promise<CompletionItem[]> {
566
+ const runtimeOption = option.runtimeOption;
567
+ const operation = option.operation;
568
+
569
+ if (runtimeOption?.type === "boolean") {
570
+ return filterItems([
571
+ { value: "true", group: GROUP_VALUES },
572
+ { value: "false", group: GROUP_VALUES },
573
+ ], current);
574
+ }
575
+
576
+ const schema = operation && runtimeOption ? operation.inputSchema?.properties?.[runtimeOption.name] : undefined;
577
+ const enumItems = enumCompletionItems(schema);
578
+ if (enumItems.length > 0) return filterItems(enumItems, current);
579
+
580
+ if (runtimeOption?.name === "workflow" || option.flags.includes("--workflow")) {
581
+ return filterItems(await safeWorkflowTargets(resolveProjectDir(words, cwd)), current);
582
+ }
583
+
584
+ return [];
585
+ }
586
+
587
+ async function completeCommand(context: CompletionContext): Promise<CompletionItem[]> {
588
+ switch (context.command) {
589
+ case "plan":
590
+ case "apply":
591
+ case "create":
592
+ return await completeProjectOperationCommand(context);
593
+ case "run":
594
+ return await completeRunCommand(context);
595
+ case "rm":
596
+ return await completeRmCommand(context);
597
+ case "ls":
598
+ return completeLsCommand(context);
599
+ case "cache":
600
+ return await completeCacheCommand(context);
601
+ case "providers":
602
+ return completeProvidersCommand(context);
603
+ case "completion":
604
+ return completeCompletionCommand(context);
605
+ case "init":
606
+ case "projects":
607
+ case "doctor":
608
+ case "version":
609
+ case "help":
610
+ return completeOptionsOnlyCommand(context, COMMAND_OPTIONS[context.command]);
611
+ }
612
+ return [];
613
+ }
614
+
615
+ async function completeProjectOperationCommand(context: CompletionContext): Promise<CompletionItem[]> {
616
+ const operation = await safeResolveRuntimeOperation(resolveProjectDir(context.words, context.cwd), context.command!);
617
+ const options = mergeOptions([
618
+ ...operationOptions(operation),
619
+ ...(CORE_OPERATION_OPTIONS[context.command!] ?? []),
620
+ ...COMMAND_OPTIONS[context.command!],
621
+ ]);
622
+ const positionals = positionalsFrom(context.argsBefore, options);
623
+
624
+ if (context.current.startsWith("-") || context.current === "") {
625
+ const positionalItems = operation ? operationPositionalValueItems(operation, positionals.length, context.current) : [];
626
+ return filterItems([...positionalItems, ...optionItems(options)], context.current);
627
+ }
628
+
629
+ if (!operation) return [];
630
+ return filterItems(operationPositionalValueItems(operation, positionals.length, context.current), context.current);
631
+ }
632
+
633
+ async function completeRunCommand(context: CompletionContext): Promise<CompletionItem[]> {
634
+ const paths = resolveProjectDir(context.words, context.cwd);
635
+ const baseOptions = COMMAND_OPTIONS.run;
636
+ const run = parseRunArgs(context);
637
+
638
+ if (!run.workspace) {
639
+ return completeMixed({
640
+ primary: await safeWorkspaceTargets(paths),
641
+ options: baseOptions,
642
+ current: context.current,
643
+ });
644
+ }
645
+
646
+ if (!run.operation) {
647
+ return completeMixed({
648
+ primary: await safeWorkspaceOperationTargets(paths),
649
+ options: baseOptions,
650
+ current: context.current,
651
+ });
652
+ }
653
+
654
+ const operation = await safeResolveWorkspaceOperation(paths, run.operation);
655
+ const options = mergeOptions([
656
+ ...operationOptions(operation),
657
+ ...baseOptions,
658
+ ]);
659
+ const positionals = positionalsFrom(run.args, options);
660
+
661
+ if (context.current.startsWith("-") || context.current === "") {
662
+ const positionalItems = operation ? operationPositionalValueItems(operation, positionals.length, context.current) : [];
663
+ return filterItems([...positionalItems, ...optionItems(options)], context.current);
664
+ }
665
+
666
+ if (!operation) return [];
667
+ return filterItems(operationPositionalValueItems(operation, positionals.length, context.current), context.current);
668
+ }
669
+
670
+ async function completeRmCommand(context: CompletionContext): Promise<CompletionItem[]> {
671
+ const paths = resolveProjectDir(context.words, context.cwd);
672
+ const remove = parseRmArgs(context);
673
+ const operation = remove.workspace ? await safeResolveWorkspaceOperation(paths, "remove") : undefined;
674
+ const options = mergeOptions([
675
+ ...operationOptions(operation),
676
+ ...COMMAND_OPTIONS.rm,
677
+ ]);
678
+
679
+ if (!remove.workspace) {
680
+ return completeMixed({
681
+ primary: await safeWorkspaceTargets(paths),
682
+ options,
683
+ current: context.current,
684
+ });
249
685
  }
250
686
 
251
- const positionalCount = countPositionals(before, command);
687
+ if (context.current.startsWith("-") || context.current === "") {
688
+ return filterItems(optionItems(options), context.current);
689
+ }
690
+
691
+ return [];
692
+ }
252
693
 
253
- if (command === "run") {
254
- const run = parseWorkspaceRunCommand(before);
255
- if (!run.workspace) return filterItems(await workspaceTargets(resolveProjectDir(words, cwd)), current);
256
- if (!run.operation) return filterItems(await safeWorkspaceOperationTargets(resolveProjectDir(words, cwd)), current);
694
+ function completeLsCommand(context: CompletionContext): CompletionItem[] {
695
+ const options = COMMAND_OPTIONS.ls;
696
+ const targets = positionalsFrom(context.argsBefore, options);
697
+ if (targets.length === 0) {
698
+ return completeMixed({
699
+ primary: LIST_TARGETS,
700
+ options,
701
+ current: context.current,
702
+ });
257
703
  }
258
704
 
259
- if (command === "rm") {
260
- const remove = parseRemoveCommand(before);
261
- if (!remove.workspace) return filterItems(await workspaceTargets(resolveProjectDir(words, cwd)), current);
705
+ if (context.current.startsWith("-") || context.current === "") {
706
+ return filterItems(optionItems(options), context.current);
262
707
  }
263
708
 
264
- if (command === "completion" && positionalCount === 0) {
265
- return filterItems(COMMAND_OPTIONS.completion, current);
709
+ return [];
710
+ }
711
+
712
+ async function completeCacheCommand(context: CompletionContext): Promise<CompletionItem[]> {
713
+ const cache = parseCacheArgs(context);
714
+ if (!cache.subcommand) {
715
+ if (context.current.startsWith("-")) return filterItems(optionItems(COMMAND_OPTIONS.cache), context.current);
716
+ return filterItems(CACHE_SUBCOMMANDS, context.current);
266
717
  }
267
718
 
268
- if (command === "ls" && positionalCount === 0) {
269
- return filterItems(COMMAND_OPTIONS.ls, current);
719
+ const options = CACHE_SUBCOMMAND_OPTIONS[cache.subcommand] ?? [HELP_OPTION];
720
+ if (cache.subcommand !== "invalidate") {
721
+ return completeOptionsOnlyCommand(context, options);
270
722
  }
271
723
 
272
- if (command === "cache") {
273
- const cache = parseCacheCommand(before);
274
- if (!cache.subcommand) return filterItems(COMMAND_OPTIONS.cache ?? [], current);
275
- return filterItems(cacheOptionTargets(before), current);
724
+ const stepArgs = positionalsFrom(cache.args, options);
725
+ if (stepArgs.length === 0) {
726
+ return completeMixed({
727
+ primary: await safeCacheInvalidateTargets(resolveProjectDir(context.words, context.cwd)),
728
+ options,
729
+ current: context.current,
730
+ });
731
+ }
732
+
733
+ if (context.current.startsWith("-") || context.current === "") {
734
+ return filterItems(optionItems(options), context.current);
276
735
  }
277
736
 
278
737
  return [];
279
738
  }
280
739
 
740
+ function completeProvidersCommand(context: CompletionContext): CompletionItem[] {
741
+ const providers = parseProvidersArgs(context);
742
+ if (!providers.provider) {
743
+ if (context.current.startsWith("-")) return filterItems(optionItems(COMMAND_OPTIONS.providers), context.current);
744
+ return filterItems(PROVIDER_TARGETS, context.current);
745
+ }
746
+
747
+ if (!providers.subcommand) {
748
+ const options = PROVIDER_TARGET_OPTIONS[providers.provider] ?? [HELP_OPTION];
749
+ const subcommands = PROVIDER_SUBCOMMANDS[providers.provider] ?? [];
750
+ if (context.current.startsWith("-")) return filterItems(optionItems(options), context.current);
751
+ return filterItems(subcommands, context.current);
752
+ }
753
+
754
+ const options = PROVIDER_SUBCOMMAND_OPTIONS[providers.provider]?.[providers.subcommand] ?? [HELP_OPTION];
755
+ return completeOptionsOnlyCommand(context, options);
756
+ }
757
+
758
+ function completeCompletionCommand(context: CompletionContext): CompletionItem[] {
759
+ const shells = positionalsFrom(context.argsBefore, COMMAND_OPTIONS.completion);
760
+ if (shells.length === 0) {
761
+ return completeMixed({
762
+ primary: COMPLETION_SHELLS,
763
+ options: COMMAND_OPTIONS.completion,
764
+ current: context.current,
765
+ });
766
+ }
767
+
768
+ if (context.current.startsWith("-") || context.current === "") {
769
+ return filterItems(optionItems(COMMAND_OPTIONS.completion), context.current);
770
+ }
771
+
772
+ return [];
773
+ }
774
+
775
+ function completeOptionsOnlyCommand(context: CompletionContext, options: OptionDefinition[]): CompletionItem[] {
776
+ if (context.current.startsWith("-") || context.current === "") {
777
+ return filterItems(optionItems(options), context.current);
778
+ }
779
+ return [];
780
+ }
781
+
782
+ function completeMixed(input: {
783
+ primary: CompletionItem[];
784
+ options: OptionDefinition[];
785
+ current: string;
786
+ }): CompletionItem[] {
787
+ if (input.current.startsWith("-")) return filterItems(optionItems(input.options), input.current);
788
+ if (input.current === "") return filterItems([...input.primary, ...optionItems(input.options)], input.current);
789
+ return filterItems(input.primary, input.current);
790
+ }
791
+
281
792
  export function formatCompletionItems(items: CompletionItem[], shell: CompletionShell): string {
282
793
  const lines = items.map((item) => {
283
794
  if (shell === "bash") return item.value;
@@ -287,7 +798,6 @@ export function formatCompletionItems(items: CompletionItem[], shell: Completion
287
798
  const group = item.group ?? "";
288
799
  return `${item.value}\t${description}\t${marker}\t${group}`;
289
800
  }
290
- // fish: legacy two-column format works fine; descriptions render dim by default
291
801
  return item.description ? `${item.value}\t${item.description}` : item.value;
292
802
  });
293
803
  return lines.join("\n");
@@ -310,6 +820,9 @@ _rig_completion() {
310
820
  local completions
311
821
  completions="$(command rig __complete --shell bash --index "$COMP_CWORD" -- "\${COMP_WORDS[@]}" 2>/dev/null)"
312
822
  COMPREPLY=($(compgen -W "$completions" -- "\${COMP_WORDS[COMP_CWORD]}"))
823
+ if [[ "\${#COMPREPLY[@]}" -eq 1 && ( "\${COMPREPLY[0]}" == */ || "\${COMPREPLY[0]}" == *= ) ]]; then
824
+ compopt -o nospace 2>/dev/null || true
825
+ fi
313
826
  }
314
827
  complete -F _rig_completion rig
315
828
  `;
@@ -327,11 +840,8 @@ complete -c rig -f -a "(__rig_complete)"
327
840
  `;
328
841
  }
329
842
 
330
- return `#compdef rig
331
- # rig zsh completion — auto-generated by \`rig completion zsh\`.
332
- # Visual defaults are scoped to :completion:*:rig:* so they don't override your
333
- # global completion theme. Group headers render bold blue; descriptions inherit
334
- # your usual style.
843
+ return `#compdef rig
844
+ # rig zsh completion generated by \`rig completion zsh\`.
335
845
 
336
846
  () {
337
847
  zstyle ':completion:*:rig:*:descriptions' format $'\\e[1;34m%d\\e[0m'
@@ -381,166 +891,179 @@ compdef _rig rig
381
891
  `;
382
892
  }
383
893
 
384
- function findCommand(words: string[]): string | undefined {
385
- for (let index = 0; index < words.length; index += 1) {
386
- const word = words[index]!;
387
- if (OPTIONS_WITH_VALUES.has(word)) {
388
- index += 1;
389
- continue;
390
- }
391
- if (word.startsWith("--") && word.includes("=")) continue;
392
- if (word.startsWith("-")) continue;
894
+ function parseInlineValueOption(current: string): { option: string; value: string; prefix: string } | undefined {
895
+ const index = current.indexOf("=");
896
+ if (index < 0) return undefined;
897
+ return {
898
+ option: current.slice(0, index),
899
+ value: current.slice(index + 1),
900
+ prefix: current.slice(0, index + 1),
901
+ };
902
+ }
393
903
 
394
- const canonical = COMMAND_ALIASES.get(word) ?? word;
395
- if (COMMANDS.some((command) => command.value === canonical)) return canonical;
396
- }
397
- return undefined;
904
+ function parseRunArgs(context: CompletionContext): { workspace?: string; operation?: string; args: string[] } {
905
+ const basePositionals = positionalTokensFrom(context.argsBefore, COMMAND_OPTIONS.run);
906
+ const workspace = basePositionals[0]?.value;
907
+ const operation = basePositionals[1]?.value;
908
+ const operationTokenIndex = basePositionals[1]?.index;
909
+ return {
910
+ workspace,
911
+ operation,
912
+ args: operationTokenIndex === undefined ? [] : context.argsBefore.slice(operationTokenIndex + 1),
913
+ };
398
914
  }
399
915
 
400
- function countPositionals(words: string[], command: string): number {
401
- let foundCommand = false;
402
- let count = 0;
916
+ function parseRmArgs(context: CompletionContext): { workspace?: string } {
917
+ const positionals = positionalsFrom(context.argsBefore, COMMAND_OPTIONS.rm);
918
+ return { workspace: positionals[0] };
919
+ }
403
920
 
404
- for (let index = 0; index < words.length; index += 1) {
405
- const word = words[index]!;
406
- if (OPTIONS_WITH_VALUES.has(word)) {
407
- index += 1;
408
- continue;
409
- }
410
- if (word.startsWith("--") && word.includes("=")) continue;
411
- if (word.startsWith("-")) continue;
921
+ function parseCacheArgs(context: CompletionContext): { subcommand?: string; args: string[] } {
922
+ const positionals = positionalsFrom(context.argsBefore, COMMAND_OPTIONS.cache);
923
+ return {
924
+ subcommand: positionals[0],
925
+ args: positionals.slice(1),
926
+ };
927
+ }
412
928
 
413
- const canonical = COMMAND_ALIASES.get(word) ?? word;
414
- if (!foundCommand && canonical === command) {
415
- foundCommand = true;
416
- continue;
417
- }
418
- if (foundCommand) count += 1;
419
- }
929
+ function parseProvidersArgs(context: CompletionContext): { provider?: string; subcommand?: string; args: string[] } {
930
+ const positionals = positionalsFrom(context.argsBefore, COMMAND_OPTIONS.providers);
931
+ return {
932
+ provider: positionals[0],
933
+ subcommand: positionals[1],
934
+ args: positionals.slice(2),
935
+ };
936
+ }
420
937
 
421
- return count;
938
+ function positionalsFrom(tokens: string[], options: OptionDefinition[]): string[] {
939
+ return positionalTokensFrom(tokens, options).map((token) => token.value);
422
940
  }
423
941
 
424
- function parseWorkspaceRunCommand(words: string[]): { workspace?: string; operation?: string; args: string[] } {
425
- let foundRun = false;
426
- const args: string[] = [];
427
- for (let index = 0; index < words.length; index += 1) {
428
- const word = words[index]!;
429
- if (OPTIONS_WITH_VALUES.has(word)) {
430
- index += 1;
431
- continue;
942
+ function positionalTokensFrom(tokens: string[], options: OptionDefinition[]): Array<{ value: string; index: number }> {
943
+ const positionalTokens: Array<{ value: string; index: number }> = [];
944
+
945
+ for (let index = 0; index < tokens.length; index += 1) {
946
+ const word = tokens[index]!;
947
+ if (word === "--") {
948
+ positionalTokens.push(...tokens.slice(index + 1).map((value, offset) => ({ value, index: index + 1 + offset })));
949
+ break;
432
950
  }
433
- if (word.startsWith("--") && word.includes("=")) continue;
434
- if (word.startsWith("-")) continue;
435
- if (!foundRun) {
436
- if (word === "run") foundRun = true;
951
+
952
+ const option = findOption(options, word) ?? findOption(GLOBAL_OPTIONS, word);
953
+ if (option && isOptionToken(word)) {
954
+ if (option.takesValue && !hasInlineValue(word)) index += 1;
437
955
  continue;
438
956
  }
439
- args.push(word);
957
+
958
+ if (isOptionToken(word)) continue;
959
+ positionalTokens.push({ value: word, index });
440
960
  }
441
- return { workspace: args[0], operation: args[1], args: args.slice(2) };
961
+
962
+ return positionalTokens;
442
963
  }
443
964
 
444
- function parseRemoveCommand(words: string[]): { workspace?: string; args: string[] } {
445
- let foundRemove = false;
446
- const args: string[] = [];
447
- for (let index = 0; index < words.length; index += 1) {
448
- const word = words[index]!;
449
- if (OPTIONS_WITH_VALUES.has(word)) {
450
- index += 1;
451
- continue;
452
- }
453
- if (word.includes("=") && OPTIONS_WITH_VALUES.has(word.slice(0, word.indexOf("=")))) continue;
454
- if (word.startsWith("-")) continue;
455
- if (!foundRemove) {
456
- if (word === "rm") foundRemove = true;
457
- continue;
458
- }
459
- args.push(word);
460
- }
461
- return { workspace: args[0], args: args.slice(1) };
965
+ function operationOptions(operation: RuntimeOperationDefinition | undefined): OptionDefinition[] {
966
+ if (!operation) return [];
967
+ return inferOperationOptions(operation).map((runtimeOption) =>
968
+ option([runtimeOption.flag, ...(runtimeOption.aliases ?? [])], optionDescription(operation, runtimeOption), {
969
+ takesValue: runtimeOption.type !== "boolean",
970
+ operation,
971
+ runtimeOption,
972
+ })
973
+ );
462
974
  }
463
975
 
464
- function parseCacheCommand(words: string[]): { subcommand?: string; args: string[] } {
465
- let foundCache = false;
466
- const args: string[] = [];
467
- for (let index = 0; index < words.length; index += 1) {
468
- const word = words[index]!;
469
- if (OPTIONS_WITH_VALUES.has(word)) {
470
- index += 1;
471
- continue;
472
- }
473
- if (word.startsWith("--") && word.includes("=")) continue;
474
- if (word.startsWith("-")) continue;
475
- if (!foundCache) {
476
- if (word === "cache") foundCache = true;
477
- continue;
478
- }
479
- args.push(word);
480
- }
481
- return { subcommand: args[0], args: args.slice(1) };
976
+ function inferOperationOptions(operation: RuntimeOperationDefinition): RuntimeOperationCliOption[] {
977
+ const properties = operation.inputSchema?.properties ?? {};
978
+ const runtimeOptions = operation.cli?.options ?? Object.entries(properties).map(([name, schema]) => ({
979
+ name,
980
+ flag: `--${dashCase(name)}`,
981
+ required: operation.inputSchema?.required?.includes(name),
982
+ type: schema.type === "boolean" ? "boolean" : schema.type === "number" ? "number" : "string",
983
+ } satisfies RuntimeOperationCliOption));
984
+
985
+ return runtimeOptions.map((runtimeOption) => ({
986
+ ...runtimeOption,
987
+ type: runtimeOption.type ?? schemaType(properties[runtimeOption.name]) ?? "string",
988
+ }));
482
989
  }
483
990
 
484
- function cacheOptionTargets(words: string[]): CompletionItem[] {
485
- const subcommand = parseCacheCommand(words).subcommand;
486
- return subcommand ? CACHE_SUBCOMMAND_OPTIONS[subcommand] ?? [] : [];
991
+ function operationPositionalValueItems(
992
+ operation: RuntimeOperationDefinition,
993
+ positionalIndex: number,
994
+ current: string,
995
+ ): CompletionItem[] {
996
+ const positionals = operation.cli?.positionals ?? [];
997
+ const positional = positionals.find((item) => item.index === positionalIndex);
998
+ if (!positional) return [];
999
+
1000
+ const schema = operation.inputSchema?.properties?.[positional.name];
1001
+ const enumItems = enumCompletionItems(schema);
1002
+ return filterItems(enumItems, current);
487
1003
  }
488
1004
 
489
- function optionExpectingValue(words: string[]): string | undefined {
490
- const previous = words.at(-1);
491
- return previous && OPTIONS_WITH_VALUES.has(previous) ? previous : undefined;
1005
+ function optionDescription(operation: RuntimeOperationDefinition, option: RuntimeOperationCliOption): string {
1006
+ const schemaDescription = operation.inputSchema?.properties?.[option.name]?.description;
1007
+ if (schemaDescription) return schemaDescription;
1008
+ return option.required ? `${option.name} (required)` : option.name;
492
1009
  }
493
1010
 
494
- async function completeOptionValue(input: {
495
- option: string;
496
- current: string;
497
- cwd: string;
498
- words: string[];
499
- inlinePrefix?: string;
500
- }): Promise<CompletionItem[]> {
501
- let items: CompletionItem[];
502
- switch (input.option) {
503
- case "-chdir":
504
- case "--chdir":
505
- items = completeDirectories(input.cwd, input.current);
506
- break;
507
- case "-config":
508
- case "--config":
509
- items = completeConfigPaths(projectBaseDir(input.words, input.cwd), input.current);
510
- break;
511
- case "--package-manager":
512
- items = filterItems([
513
- { value: "npm", group: GROUP_VALUES },
514
- { value: "bun", group: GROUP_VALUES },
515
- { value: "pnpm", group: GROUP_VALUES },
516
- { value: "skip", group: GROUP_VALUES },
517
- ], input.current);
518
- break;
519
- case "-state":
520
- case "--state":
521
- items = completeFilesystemPaths(input.cwd, input.current);
522
- break;
523
- default:
524
- items = [];
1011
+ function schemaType(schema: JsonSchemaProperty | undefined): RuntimeOperationCliOption["type"] | undefined {
1012
+ if (schema?.type === "boolean" || schema?.type === "number" || schema?.type === "string") return schema.type;
1013
+ return undefined;
1014
+ }
1015
+
1016
+ function enumCompletionItems(schema: JsonSchemaProperty | undefined): CompletionItem[] {
1017
+ const values = schema?.enum ?? [];
1018
+ return values
1019
+ .filter((value): value is string => typeof value === "string")
1020
+ .map((value) => ({ value, group: GROUP_VALUES }));
1021
+ }
1022
+
1023
+ function optionItems(options: OptionDefinition[]): CompletionItem[] {
1024
+ return dedupeItems(options.flatMap((option) =>
1025
+ (option.completions ?? option.flags.map((value): { value: string; noSpace?: boolean } => ({ value }))).map((completion) => ({
1026
+ value: completion.value,
1027
+ description: option.description,
1028
+ noSpace: completion.noSpace,
1029
+ group: option.group,
1030
+ }))
1031
+ ));
1032
+ }
1033
+
1034
+ function mergeOptions(options: OptionDefinition[]): OptionDefinition[] {
1035
+ const seen = new Set<string>();
1036
+ const merged: OptionDefinition[] = [];
1037
+ for (const option of options) {
1038
+ const key = option.flags.join("\0");
1039
+ if (option.flags.some((flag) => seen.has(flag))) continue;
1040
+ for (const flag of option.flags) seen.add(flag);
1041
+ seen.add(key);
1042
+ merged.push(option);
525
1043
  }
1044
+ return merged;
1045
+ }
526
1046
 
527
- if (!input.inlinePrefix) return items;
528
- return items.map((item) => ({
529
- ...item,
530
- value: `${input.inlinePrefix}${item.value}`,
531
- }));
1047
+ function findOption(options: OptionDefinition[], word: string): OptionDefinition | undefined {
1048
+ const flag = optionFlag(word);
1049
+ return options.find((option) => option.flags.includes(flag));
532
1050
  }
533
1051
 
534
- function parseInlineValueOption(current: string): { option: string; value: string; prefix: string } | undefined {
535
- const index = current.indexOf("=");
536
- if (index < 0) return undefined;
537
- const option = current.slice(0, index);
538
- if (!OPTIONS_WITH_VALUES.has(option)) return undefined;
539
- return {
540
- option,
541
- value: current.slice(index + 1),
542
- prefix: current.slice(0, index + 1),
543
- };
1052
+ function optionFlag(word: string): string {
1053
+ const index = word.indexOf("=");
1054
+ return index < 0 ? word : word.slice(0, index);
1055
+ }
1056
+
1057
+ function hasInlineValue(word: string): boolean {
1058
+ return word.includes("=");
1059
+ }
1060
+
1061
+ function isOptionToken(word: string): boolean {
1062
+ return word.startsWith("-") && word !== "-";
1063
+ }
1064
+
1065
+ function isCommandName(value: string): value is CommandName {
1066
+ return COMMAND_NAMES.has(value as CommandName);
544
1067
  }
545
1068
 
546
1069
  function resolveProjectDir(words: string[], cwd: string): { projectDir: string; configPath: string } {
@@ -662,7 +1185,7 @@ function completePathEntries(
662
1185
  if (options.fileFilter && !options.fileFilter(entry.name)) return [];
663
1186
  return [{
664
1187
  value: `${dirPart}${entry.name}`,
665
- description: "config",
1188
+ description: options.fileFilter ? "config" : "file",
666
1189
  group: GROUP_PATHS,
667
1190
  }];
668
1191
  })
@@ -704,17 +1227,19 @@ function splitCompletionPath(baseDir: string, current: string): {
704
1227
  };
705
1228
  }
706
1229
 
707
- async function workspaceTargets(
1230
+ async function safeWorkspaceTargets(
708
1231
  paths: { projectDir: string; configPath: string },
709
1232
  ): Promise<CompletionItem[]> {
710
- const workspaces = await readWorkspaces(paths);
711
- const items = workspaces.map((workspace) => ({
712
- value: workspace.name,
713
- description: workspaceDescription(workspace),
714
- group: GROUP_WORKSPACES,
715
- }));
716
-
717
- return dedupeItems(items);
1233
+ try {
1234
+ const workspaces = await readWorkspaces(paths);
1235
+ return dedupeItems(workspaces.map((workspace) => ({
1236
+ value: workspace.name,
1237
+ description: workspaceDescription(workspace),
1238
+ group: GROUP_WORKSPACES,
1239
+ })));
1240
+ } catch {
1241
+ return [];
1242
+ }
718
1243
  }
719
1244
 
720
1245
  async function readWorkspaces(paths: { projectDir: string; configPath: string }): Promise<RuntimeWorkspaceCompletion[]> {
@@ -728,6 +1253,22 @@ async function readWorkspaces(paths: { projectDir: string; configPath: string })
728
1253
  }));
729
1254
  }
730
1255
 
1256
+ async function safeWorkflowTargets(
1257
+ paths: { projectDir: string; configPath: string },
1258
+ ): Promise<CompletionItem[]> {
1259
+ try {
1260
+ const runtime = await getOrStartRuntime(paths);
1261
+ const { workflows } = await runtime.control.workflows();
1262
+ return workflows.map((workflow) => ({
1263
+ value: workflow.name,
1264
+ description: "workflow",
1265
+ group: GROUP_VALUES,
1266
+ }));
1267
+ } catch {
1268
+ return [];
1269
+ }
1270
+ }
1271
+
731
1272
  async function safeWorkspaceOperationTargets(
732
1273
  paths: { projectDir: string; configPath: string },
733
1274
  ): Promise<CompletionItem[]> {
@@ -740,18 +1281,36 @@ async function safeWorkspaceOperationTargets(
740
1281
  }
741
1282
 
742
1283
  function workspaceOperationTargets(manifest: RuntimeOperationManifest): CompletionItem[] {
743
- return (manifest.workspaceOperations ?? []).flatMap((operation) => [
1284
+ return dedupeItems((manifest.workspaceOperations ?? []).flatMap((operation) => [
744
1285
  {
745
1286
  value: operation.id,
746
- description: operation.description ?? "workspace operation",
1287
+ description: operation.description || "workspace operation",
747
1288
  group: GROUP_OPERATIONS,
748
1289
  },
749
1290
  ...(operation.aliases ?? []).map((alias) => ({
750
1291
  value: alias,
751
- description: operation.description ?? "workspace operation",
1292
+ description: operation.description || "workspace operation",
752
1293
  group: GROUP_OPERATIONS,
753
1294
  })),
754
- ]);
1295
+ ]));
1296
+ }
1297
+
1298
+ async function safeCacheInvalidateTargets(
1299
+ paths: { projectDir: string; configPath: string },
1300
+ ): Promise<CompletionItem[]> {
1301
+ try {
1302
+ const runtime = await getOrStartRuntime(paths);
1303
+ const cache = await runtime.control.cache() as unknown as { entries: readonly RuntimeCacheCompletionEntry[] };
1304
+ return dedupeItems(cache.entries
1305
+ .filter((entry) => entry.scope === "local" && !entry.invalidated)
1306
+ .map((entry) => ({
1307
+ value: entry.nodePath || entry.nodeName,
1308
+ description: entry.workflow ? `workflow ${entry.workflow}` : "cached task",
1309
+ group: GROUP_CACHE,
1310
+ })));
1311
+ } catch {
1312
+ return [];
1313
+ }
755
1314
  }
756
1315
 
757
1316
  async function resolveRuntimeOperation(
@@ -842,3 +1401,7 @@ export function formatWorkspaceAge(createdAt: string, nowMs = Date.now()): strin
842
1401
 
843
1402
  return `${Math.floor(elapsedMonths / 12)}y ago`;
844
1403
  }
1404
+
1405
+ function dashCase(value: string): string {
1406
+ return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
1407
+ }