@rigkit/cli 0.2.9 → 0.2.10

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,103 @@ 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
+ | "projects"
32
+ | "doctor"
33
+ | "version"
34
+ | "completion";
35
+
36
+ type CompletionContext = {
37
+ cwd: string;
38
+ words: string[];
39
+ currentIndex: number;
40
+ current: string;
41
+ before: string[];
42
+ command?: CommandName;
43
+ commandIndex?: number;
44
+ argsBefore: string[];
45
+ unknownRootPositionals: string[];
46
+ };
47
+
48
+ type ValueCompletionKind =
49
+ | "directories"
50
+ | "config-files"
51
+ | "filesystem"
52
+ | "package-managers";
53
+
54
+ type OptionDefinition = {
55
+ flags: string[];
56
+ completions?: Array<{ value: string; noSpace?: boolean }>;
57
+ description: string;
58
+ group: string;
59
+ takesValue?: boolean;
60
+ valueKind?: ValueCompletionKind;
61
+ operation?: RuntimeOperationDefinition;
62
+ runtimeOption?: RuntimeOperationCliOption;
63
+ };
64
+
65
+ type RuntimeOperationManifest = {
66
+ operations: RuntimeOperationDefinition[];
67
+ workspaceOperations?: RuntimeOperationDefinition[];
68
+ };
69
+
70
+ type RuntimeOperationDefinition = {
71
+ id: string;
72
+ aliases?: string[];
73
+ title?: string;
74
+ description?: string;
75
+ createsWorkspace?: boolean;
76
+ cli?: {
77
+ positionals?: Array<{ name: string; index: number }>;
78
+ options?: RuntimeOperationCliOption[];
79
+ };
80
+ inputSchema?: {
81
+ properties?: Record<string, JsonSchemaProperty>;
82
+ required?: string[];
83
+ };
84
+ };
85
+
86
+ type RuntimeOperationCliOption = {
87
+ name: string;
88
+ flag: string;
89
+ aliases?: string[];
90
+ required?: boolean;
91
+ runtime?: boolean;
92
+ type?: "string" | "boolean" | "number";
93
+ };
94
+
95
+ type JsonSchemaProperty = {
96
+ type?: string;
97
+ description?: string;
98
+ default?: unknown;
99
+ enum?: unknown[];
100
+ };
101
+
102
+ type RuntimeWorkspaceCompletion = {
103
+ name: string;
104
+ workflow: string;
105
+ createdAt: string;
106
+ updatedAt: string;
107
+ };
108
+
109
+ type RuntimeCacheCompletionEntry = {
110
+ scope: "local" | "global";
111
+ workflow: string;
112
+ nodePath: string;
113
+ nodeName: string;
114
+ invalidated: boolean;
115
+ createdAt: string;
116
+ };
117
+
21
118
  const GROUP_COMMANDS = "Commands";
22
119
  const GROUP_SUBCOMMANDS = "Subcommands";
23
120
  const GROUP_FLAGS = "Flags";
@@ -28,6 +125,7 @@ const GROUP_OPERATIONS = "Operations";
28
125
  const GROUP_VALUES = "Values";
29
126
  const GROUP_PATHS = "Paths";
30
127
  const GROUP_SHELLS = "Shells";
128
+ const GROUP_CACHE = "Cache entries";
31
129
 
32
130
  const COMMANDS: CompletionItem[] = withGroup(GROUP_COMMANDS, [
33
131
  { value: "help", description: "show CLI help" },
@@ -45,239 +143,587 @@ const COMMANDS: CompletionItem[] = withGroup(GROUP_COMMANDS, [
45
143
  { value: "completion", description: "generate shell completion" },
46
144
  ]);
47
145
 
48
- const COMMAND_ALIASES = new Map<string, string>();
146
+ const COMMAND_NAMES = new Set(COMMANDS.map((command) => command.value as CommandName));
147
+
148
+ const JSON_OPTION = option(["--json"], "print JSON");
149
+ const HELP_OPTION = option(["--help"], "show help");
150
+
151
+ const GLOBAL_OPTIONS: OptionDefinition[] = [
152
+ option(["-chdir", "--chdir"], "working directory", {
153
+ group: GROUP_GLOBAL,
154
+ takesValue: true,
155
+ valueKind: "directories",
156
+ completions: [
157
+ { value: "-chdir=", noSpace: true },
158
+ { value: "--chdir=", noSpace: true },
159
+ ],
160
+ }),
161
+ option(["-config", "--config"], "config file", {
162
+ group: GROUP_GLOBAL,
163
+ takesValue: true,
164
+ valueKind: "config-files",
165
+ completions: [
166
+ { value: "-config=", noSpace: true },
167
+ { value: "--config=", noSpace: true },
168
+ ],
169
+ }),
170
+ option(["-state", "--state"], "state database path", {
171
+ group: GROUP_GLOBAL,
172
+ takesValue: true,
173
+ valueKind: "filesystem",
174
+ completions: [
175
+ { value: "-state=", noSpace: true },
176
+ { value: "--state=", noSpace: true },
177
+ ],
178
+ }),
179
+ option(["-json", "--json"], "print JSON", { group: GROUP_GLOBAL }),
180
+ option(["-help", "--help"], "show help", { group: GROUP_GLOBAL }),
181
+ option(["-version", "--version", "-v"], "show version", { group: GROUP_GLOBAL }),
182
+ ];
183
+
184
+ const COMMAND_OPTIONS: Record<CommandName, OptionDefinition[]> = {
185
+ init: [
186
+ option(["--name"], "project and workflow name", { takesValue: true }),
187
+ option(["--api-key"], "Freestyle API key", { takesValue: true }),
188
+ option(["--package-manager"], "npm, bun, pnpm, or skip", {
189
+ takesValue: true,
190
+ valueKind: "package-managers",
191
+ }),
192
+ option(["--force"], "overwrite existing config"),
193
+ JSON_OPTION,
194
+ HELP_OPTION,
195
+ ],
196
+ plan: [
197
+ option(["--all"], "run against every discovered project"),
198
+ option(["--discover"], "discover projects below the selected directory"),
199
+ JSON_OPTION,
200
+ HELP_OPTION,
201
+ ],
202
+ apply: [
203
+ option(["--all"], "run against every discovered project"),
204
+ option(["--discover"], "discover projects below the selected directory"),
205
+ JSON_OPTION,
206
+ HELP_OPTION,
207
+ ],
208
+ create: [
209
+ JSON_OPTION,
210
+ HELP_OPTION,
211
+ ],
212
+ rm: [
213
+ option(["-y", "--yes"], "skip confirmation"),
214
+ option(["--all"], "remove every workspace"),
215
+ JSON_OPTION,
216
+ HELP_OPTION,
217
+ ],
218
+ run: [
219
+ JSON_OPTION,
220
+ HELP_OPTION,
221
+ ],
222
+ ls: [
223
+ JSON_OPTION,
224
+ HELP_OPTION,
225
+ ],
226
+ cache: [
227
+ HELP_OPTION,
228
+ ],
229
+ projects: [
230
+ JSON_OPTION,
231
+ HELP_OPTION,
232
+ ],
233
+ doctor: [
234
+ option(["--cli"], "show CLI diagnostics only"),
235
+ JSON_OPTION,
236
+ HELP_OPTION,
237
+ ],
238
+ version: [
239
+ JSON_OPTION,
240
+ HELP_OPTION,
241
+ ],
242
+ help: [
243
+ JSON_OPTION,
244
+ HELP_OPTION,
245
+ ],
246
+ completion: [
247
+ HELP_OPTION,
248
+ ],
249
+ };
49
250
 
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" },
251
+ const CORE_OPERATION_OPTIONS: Partial<Record<CommandName, OptionDefinition[]>> = {
252
+ plan: [
253
+ option(["--workflow"], "workflow name", { takesValue: true }),
254
+ ],
255
+ apply: [
256
+ option(["--workflow"], "workflow name", { takesValue: true }),
257
+ option(["--dry-run"], "plan without applying changes"),
258
+ ],
259
+ create: [
260
+ option(["--workflow"], "workflow name", { takesValue: true }),
261
+ option(["--name"], "workspace name", { takesValue: true }),
262
+ ],
263
+ };
264
+
265
+ const LIST_TARGETS: CompletionItem[] = withGroup(GROUP_TARGETS, [
266
+ { value: "workspaces", description: "list workspaces" },
267
+ { value: "snapshots", description: "list snapshots" },
268
+ { value: "config", description: "show project config" },
269
+ ]);
270
+
271
+ const CACHE_SUBCOMMANDS: CompletionItem[] = withGroup(GROUP_SUBCOMMANDS, [
272
+ { value: "ls", description: "list cache entries" },
273
+ { value: "clear", description: "clear cache entries" },
274
+ { value: "invalidate", description: "mark cached task outputs stale" },
57
275
  ]);
58
276
 
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
- ]),
277
+ const CACHE_SUBCOMMAND_OPTIONS: Record<string, OptionDefinition[]> = {
88
278
  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
- ]),
279
+ JSON_OPTION,
280
+ HELP_OPTION,
281
+ ],
282
+ clear: [
283
+ option(["--local"], "clear local cache entries"),
284
+ option(["--global"], "clear global cache fragments"),
285
+ option(["--all"], "clear every global fragment with --global"),
286
+ JSON_OPTION,
287
+ HELP_OPTION,
288
+ ],
289
+ invalidate: [
290
+ option(["--all"], "invalidate every cached task"),
291
+ option(["-y", "--yes"], "skip confirmation"),
292
+ JSON_OPTION,
293
+ HELP_OPTION,
97
294
  ],
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
295
  };
111
296
 
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" },
121
- ]),
122
- };
297
+ const COMPLETION_SHELLS: CompletionItem[] = withGroup(GROUP_SHELLS, [
298
+ { value: "bash", description: "Bash completion" },
299
+ { value: "fish", description: "fish completion" },
300
+ { value: "zsh", description: "zsh completion" },
301
+ ]);
302
+
303
+ const PROJECT_OPERATION_COMMANDS = new Set<CommandName>(["plan", "apply", "create"]);
123
304
 
124
305
  function withGroup(group: string, items: Omit<CompletionItem, "group">[]): CompletionItem[] {
125
306
  return items.map((item) => ({ ...item, group }));
126
307
  }
127
308
 
128
- const PROJECT_OPERATION_COMMANDS = new Set(["plan", "apply", "create"]);
309
+ function option(
310
+ flags: string[],
311
+ description: string,
312
+ input: Partial<Omit<OptionDefinition, "flags" | "description">> = {},
313
+ ): OptionDefinition {
314
+ return {
315
+ flags,
316
+ description,
317
+ group: input.group ?? GROUP_FLAGS,
318
+ takesValue: input.takesValue,
319
+ valueKind: input.valueKind,
320
+ completions: input.completions,
321
+ operation: input.operation,
322
+ runtimeOption: input.runtimeOption,
323
+ };
324
+ }
129
325
 
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
- ]);
326
+ export async function completeRig(input: CompleteRigInput): Promise<CompletionItem[]> {
327
+ const context = completionContext(input);
141
328
 
142
- type RuntimeOperationManifest = {
143
- operations: RuntimeOperationDefinition[];
144
- workspaceOperations?: RuntimeOperationDefinition[];
145
- };
329
+ const valueRequest = await optionValueRequest(context);
330
+ if (valueRequest) {
331
+ return await completeOptionValue(valueRequest);
332
+ }
146
333
 
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
- };
334
+ if (!context.command) {
335
+ if (context.unknownRootPositionals.length > 0) return [];
336
+ return filterItems(
337
+ context.current.startsWith("-")
338
+ ? optionItems(GLOBAL_OPTIONS)
339
+ : [...COMMANDS, ...optionItems(GLOBAL_OPTIONS)],
340
+ context.current,
341
+ );
342
+ }
156
343
 
157
- type RuntimeWorkspaceCompletion = {
158
- name: string;
159
- workflow: string;
160
- createdAt: string;
161
- updatedAt: string;
162
- };
344
+ return await completeCommand(context);
345
+ }
163
346
 
164
- export async function completeRig(input: CompleteRigInput): Promise<CompletionItem[]> {
347
+ function completionContext(input: CompleteRigInput): CompletionContext {
165
348
  const cwd = input.cwd ?? process.cwd();
166
349
  const words = input.words.length > 0 ? input.words : ["rig"];
167
350
  const currentIndex = input.currentIndex ?? Math.max(0, words.length - 1);
168
351
  const current = words[currentIndex] ?? "";
169
352
  const before = words.slice(1, currentIndex);
170
- const command = findCommand(before);
353
+ const unknownRootPositionals: string[] = [];
354
+ let command: CommandName | undefined;
355
+ let commandIndex: number | undefined;
356
+
357
+ for (let index = 0; index < before.length; index += 1) {
358
+ const word = before[index]!;
359
+ const globalOption = findOption(GLOBAL_OPTIONS, word);
360
+ if (globalOption?.takesValue && !hasInlineValue(word)) {
361
+ index += 1;
362
+ continue;
363
+ }
364
+ if (isOptionToken(word)) continue;
365
+
366
+ if (isCommandName(word)) {
367
+ command = word;
368
+ commandIndex = index;
369
+ break;
370
+ }
171
371
 
172
- const inlineOption = parseInlineValueOption(current);
372
+ unknownRootPositionals.push(word);
373
+ }
374
+
375
+ return {
376
+ cwd,
377
+ words,
378
+ currentIndex,
379
+ current,
380
+ before,
381
+ command,
382
+ commandIndex,
383
+ argsBefore: commandIndex === undefined ? [] : before.slice(commandIndex + 1),
384
+ unknownRootPositionals,
385
+ };
386
+ }
387
+
388
+ async function optionValueRequest(context: CompletionContext): Promise<{
389
+ option: OptionDefinition;
390
+ current: string;
391
+ cwd: string;
392
+ words: string[];
393
+ inlinePrefix?: string;
394
+ } | undefined> {
395
+ const inlineOption = parseInlineValueOption(context.current);
173
396
  if (inlineOption) {
174
- return await completeOptionValue({
175
- option: inlineOption.option,
176
- current: inlineOption.value,
177
- cwd,
178
- words,
179
- inlinePrefix: inlineOption.prefix,
397
+ const definition = await resolveOptionDefinition(context, inlineOption.option);
398
+ if (definition?.takesValue || definition?.runtimeOption?.type === "boolean") {
399
+ return {
400
+ option: definition,
401
+ current: inlineOption.value,
402
+ cwd: context.cwd,
403
+ words: context.words,
404
+ inlinePrefix: inlineOption.prefix,
405
+ };
406
+ }
407
+ }
408
+
409
+ const previous = context.before.at(-1);
410
+ if (!previous) return undefined;
411
+ if (hasInlineValue(previous)) return undefined;
412
+
413
+ const definition = await resolveOptionDefinition(context, previous);
414
+ if (!definition?.takesValue) return undefined;
415
+ return {
416
+ option: definition,
417
+ current: context.current,
418
+ cwd: context.cwd,
419
+ words: context.words,
420
+ };
421
+ }
422
+
423
+ async function resolveOptionDefinition(
424
+ context: CompletionContext,
425
+ flag: string,
426
+ ): Promise<OptionDefinition | undefined> {
427
+ const globalOption = findOption(GLOBAL_OPTIONS, flag);
428
+ if (globalOption) return globalOption;
429
+ if (!context.command) return undefined;
430
+
431
+ const commandOptions = await optionsForCommandContext(context);
432
+ return findOption(commandOptions, flag);
433
+ }
434
+
435
+ async function optionsForCommandContext(context: CompletionContext): Promise<OptionDefinition[]> {
436
+ if (!context.command) return GLOBAL_OPTIONS;
437
+
438
+ if (PROJECT_OPERATION_COMMANDS.has(context.command)) {
439
+ const operation = await safeResolveRuntimeOperation(resolveProjectDir(context.words, context.cwd), context.command);
440
+ return mergeOptions([
441
+ ...operationOptions(operation),
442
+ ...(CORE_OPERATION_OPTIONS[context.command] ?? []),
443
+ ...COMMAND_OPTIONS[context.command],
444
+ ]);
445
+ }
446
+
447
+ if (context.command === "run") {
448
+ const run = parseRunArgs(context);
449
+ if (run.workspace && run.operation) {
450
+ const operation = await safeResolveWorkspaceOperation(resolveProjectDir(context.words, context.cwd), run.operation);
451
+ return mergeOptions([
452
+ ...operationOptions(operation),
453
+ ...COMMAND_OPTIONS.run,
454
+ ]);
455
+ }
456
+ }
457
+
458
+ if (context.command === "rm") {
459
+ const remove = parseRmArgs(context);
460
+ if (remove.workspace) {
461
+ const operation = await safeResolveWorkspaceOperation(resolveProjectDir(context.words, context.cwd), "remove");
462
+ return mergeOptions([
463
+ ...operationOptions(operation),
464
+ ...COMMAND_OPTIONS.rm,
465
+ ]);
466
+ }
467
+ }
468
+
469
+ if (context.command === "cache") {
470
+ const cache = parseCacheArgs(context);
471
+ if (cache.subcommand) return CACHE_SUBCOMMAND_OPTIONS[cache.subcommand] ?? [HELP_OPTION];
472
+ }
473
+
474
+ return COMMAND_OPTIONS[context.command] ?? [];
475
+ }
476
+
477
+ async function completeOptionValue(input: {
478
+ option: OptionDefinition;
479
+ current: string;
480
+ cwd: string;
481
+ words: string[];
482
+ inlinePrefix?: string;
483
+ }): Promise<CompletionItem[]> {
484
+ let items: CompletionItem[];
485
+ switch (input.option.valueKind) {
486
+ case "directories":
487
+ items = completeDirectories(input.cwd, input.current);
488
+ break;
489
+ case "config-files":
490
+ items = completeConfigPaths(projectBaseDir(input.words, input.cwd), input.current);
491
+ break;
492
+ case "filesystem":
493
+ items = completeFilesystemPaths(input.cwd, input.current);
494
+ break;
495
+ case "package-managers":
496
+ items = filterItems([
497
+ { value: "npm", group: GROUP_VALUES },
498
+ { value: "bun", group: GROUP_VALUES },
499
+ { value: "pnpm", group: GROUP_VALUES },
500
+ { value: "skip", group: GROUP_VALUES },
501
+ ], input.current);
502
+ break;
503
+ default:
504
+ items = await completeRuntimeOptionValue(input.option, input.current, input.words, input.cwd);
505
+ break;
506
+ }
507
+
508
+ if (!input.inlinePrefix) return items;
509
+ return items.map((item) => ({
510
+ ...item,
511
+ value: `${input.inlinePrefix}${item.value}`,
512
+ }));
513
+ }
514
+
515
+ async function completeRuntimeOptionValue(
516
+ option: OptionDefinition,
517
+ current: string,
518
+ words: string[],
519
+ cwd: string,
520
+ ): Promise<CompletionItem[]> {
521
+ const runtimeOption = option.runtimeOption;
522
+ const operation = option.operation;
523
+
524
+ if (runtimeOption?.type === "boolean") {
525
+ return filterItems([
526
+ { value: "true", group: GROUP_VALUES },
527
+ { value: "false", group: GROUP_VALUES },
528
+ ], current);
529
+ }
530
+
531
+ const schema = operation && runtimeOption ? operation.inputSchema?.properties?.[runtimeOption.name] : undefined;
532
+ const enumItems = enumCompletionItems(schema);
533
+ if (enumItems.length > 0) return filterItems(enumItems, current);
534
+
535
+ if (runtimeOption?.name === "workflow" || option.flags.includes("--workflow")) {
536
+ return filterItems(await safeWorkflowTargets(resolveProjectDir(words, cwd)), current);
537
+ }
538
+
539
+ return [];
540
+ }
541
+
542
+ async function completeCommand(context: CompletionContext): Promise<CompletionItem[]> {
543
+ switch (context.command) {
544
+ case "plan":
545
+ case "apply":
546
+ case "create":
547
+ return await completeProjectOperationCommand(context);
548
+ case "run":
549
+ return await completeRunCommand(context);
550
+ case "rm":
551
+ return await completeRmCommand(context);
552
+ case "ls":
553
+ return completeLsCommand(context);
554
+ case "cache":
555
+ return await completeCacheCommand(context);
556
+ case "completion":
557
+ return completeCompletionCommand(context);
558
+ case "init":
559
+ case "projects":
560
+ case "doctor":
561
+ case "version":
562
+ case "help":
563
+ return completeOptionsOnlyCommand(context, COMMAND_OPTIONS[context.command]);
564
+ }
565
+ return [];
566
+ }
567
+
568
+ async function completeProjectOperationCommand(context: CompletionContext): Promise<CompletionItem[]> {
569
+ const operation = await safeResolveRuntimeOperation(resolveProjectDir(context.words, context.cwd), context.command!);
570
+ const options = mergeOptions([
571
+ ...operationOptions(operation),
572
+ ...(CORE_OPERATION_OPTIONS[context.command!] ?? []),
573
+ ...COMMAND_OPTIONS[context.command!],
574
+ ]);
575
+ const positionals = positionalsFrom(context.argsBefore, options);
576
+
577
+ if (context.current.startsWith("-") || context.current === "") {
578
+ const positionalItems = operation ? operationPositionalValueItems(operation, positionals.length, context.current) : [];
579
+ return filterItems([...positionalItems, ...optionItems(options)], context.current);
580
+ }
581
+
582
+ if (!operation) return [];
583
+ return filterItems(operationPositionalValueItems(operation, positionals.length, context.current), context.current);
584
+ }
585
+
586
+ async function completeRunCommand(context: CompletionContext): Promise<CompletionItem[]> {
587
+ const paths = resolveProjectDir(context.words, context.cwd);
588
+ const baseOptions = COMMAND_OPTIONS.run;
589
+ const run = parseRunArgs(context);
590
+
591
+ if (!run.workspace) {
592
+ return completeMixed({
593
+ primary: await safeWorkspaceTargets(paths),
594
+ options: baseOptions,
595
+ current: context.current,
180
596
  });
181
597
  }
182
598
 
183
- const valueOption = optionExpectingValue(before);
184
- if (valueOption) {
185
- return await completeOptionValue({
186
- option: valueOption,
187
- current,
188
- cwd,
189
- words,
599
+ if (!run.operation) {
600
+ return completeMixed({
601
+ primary: await safeWorkspaceOperationTargets(paths),
602
+ options: baseOptions,
603
+ current: context.current,
190
604
  });
191
605
  }
192
606
 
193
- if (!command) {
194
- return filterItems(
195
- current.startsWith("-")
196
- ? GLOBAL_OPTIONS
197
- : [...COMMANDS, ...GLOBAL_OPTIONS],
198
- current,
199
- );
607
+ const operation = await safeResolveWorkspaceOperation(paths, run.operation);
608
+ const options = mergeOptions([
609
+ ...operationOptions(operation),
610
+ ...baseOptions,
611
+ ]);
612
+ const positionals = positionalsFrom(run.args, options);
613
+
614
+ if (context.current.startsWith("-") || context.current === "") {
615
+ const positionalItems = operation ? operationPositionalValueItems(operation, positionals.length, context.current) : [];
616
+ return filterItems([...positionalItems, ...optionItems(options)], context.current);
200
617
  }
201
618
 
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
- }
230
- }
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);
241
- }
242
- if (command === "cache") {
243
- return filterItems([
244
- ...cacheOptionTargets(before),
245
- ...GLOBAL_OPTIONS,
246
- ], current);
247
- }
248
- return filterItems([...(COMMAND_OPTIONS[command] ?? []), ...GLOBAL_OPTIONS], current);
619
+ if (!operation) return [];
620
+ return filterItems(operationPositionalValueItems(operation, positionals.length, context.current), context.current);
621
+ }
622
+
623
+ async function completeRmCommand(context: CompletionContext): Promise<CompletionItem[]> {
624
+ const paths = resolveProjectDir(context.words, context.cwd);
625
+ const remove = parseRmArgs(context);
626
+ const operation = remove.workspace ? await safeResolveWorkspaceOperation(paths, "remove") : undefined;
627
+ const options = mergeOptions([
628
+ ...operationOptions(operation),
629
+ ...COMMAND_OPTIONS.rm,
630
+ ]);
631
+
632
+ if (!remove.workspace) {
633
+ return completeMixed({
634
+ primary: await safeWorkspaceTargets(paths),
635
+ options,
636
+ current: context.current,
637
+ });
638
+ }
639
+
640
+ if (context.current.startsWith("-") || context.current === "") {
641
+ return filterItems(optionItems(options), context.current);
249
642
  }
250
643
 
251
- const positionalCount = countPositionals(before, command);
644
+ return [];
645
+ }
646
+
647
+ function completeLsCommand(context: CompletionContext): CompletionItem[] {
648
+ const options = COMMAND_OPTIONS.ls;
649
+ const targets = positionalsFrom(context.argsBefore, options);
650
+ if (targets.length === 0) {
651
+ return completeMixed({
652
+ primary: LIST_TARGETS,
653
+ options,
654
+ current: context.current,
655
+ });
656
+ }
252
657
 
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);
658
+ if (context.current.startsWith("-") || context.current === "") {
659
+ return filterItems(optionItems(options), context.current);
257
660
  }
258
661
 
259
- if (command === "rm") {
260
- const remove = parseRemoveCommand(before);
261
- if (!remove.workspace) return filterItems(await workspaceTargets(resolveProjectDir(words, cwd)), current);
662
+ return [];
663
+ }
664
+
665
+ async function completeCacheCommand(context: CompletionContext): Promise<CompletionItem[]> {
666
+ const cache = parseCacheArgs(context);
667
+ if (!cache.subcommand) {
668
+ if (context.current.startsWith("-")) return filterItems(optionItems(COMMAND_OPTIONS.cache), context.current);
669
+ return filterItems(CACHE_SUBCOMMANDS, context.current);
262
670
  }
263
671
 
264
- if (command === "completion" && positionalCount === 0) {
265
- return filterItems(COMMAND_OPTIONS.completion, current);
672
+ const options = CACHE_SUBCOMMAND_OPTIONS[cache.subcommand] ?? [HELP_OPTION];
673
+ if (cache.subcommand !== "invalidate") {
674
+ return completeOptionsOnlyCommand(context, options);
266
675
  }
267
676
 
268
- if (command === "ls" && positionalCount === 0) {
269
- return filterItems(COMMAND_OPTIONS.ls, current);
677
+ const stepArgs = positionalsFrom(cache.args, options);
678
+ if (stepArgs.length === 0) {
679
+ return completeMixed({
680
+ primary: await safeCacheInvalidateTargets(resolveProjectDir(context.words, context.cwd)),
681
+ options,
682
+ current: context.current,
683
+ });
270
684
  }
271
685
 
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);
686
+ if (context.current.startsWith("-") || context.current === "") {
687
+ return filterItems(optionItems(options), context.current);
276
688
  }
277
689
 
278
690
  return [];
279
691
  }
280
692
 
693
+ function completeCompletionCommand(context: CompletionContext): CompletionItem[] {
694
+ const shells = positionalsFrom(context.argsBefore, COMMAND_OPTIONS.completion);
695
+ if (shells.length === 0) {
696
+ return completeMixed({
697
+ primary: COMPLETION_SHELLS,
698
+ options: COMMAND_OPTIONS.completion,
699
+ current: context.current,
700
+ });
701
+ }
702
+
703
+ if (context.current.startsWith("-") || context.current === "") {
704
+ return filterItems(optionItems(COMMAND_OPTIONS.completion), context.current);
705
+ }
706
+
707
+ return [];
708
+ }
709
+
710
+ function completeOptionsOnlyCommand(context: CompletionContext, options: OptionDefinition[]): CompletionItem[] {
711
+ if (context.current.startsWith("-") || context.current === "") {
712
+ return filterItems(optionItems(options), context.current);
713
+ }
714
+ return [];
715
+ }
716
+
717
+ function completeMixed(input: {
718
+ primary: CompletionItem[];
719
+ options: OptionDefinition[];
720
+ current: string;
721
+ }): CompletionItem[] {
722
+ if (input.current.startsWith("-")) return filterItems(optionItems(input.options), input.current);
723
+ if (input.current === "") return filterItems([...input.primary, ...optionItems(input.options)], input.current);
724
+ return filterItems(input.primary, input.current);
725
+ }
726
+
281
727
  export function formatCompletionItems(items: CompletionItem[], shell: CompletionShell): string {
282
728
  const lines = items.map((item) => {
283
729
  if (shell === "bash") return item.value;
@@ -287,7 +733,6 @@ export function formatCompletionItems(items: CompletionItem[], shell: Completion
287
733
  const group = item.group ?? "";
288
734
  return `${item.value}\t${description}\t${marker}\t${group}`;
289
735
  }
290
- // fish: legacy two-column format works fine; descriptions render dim by default
291
736
  return item.description ? `${item.value}\t${item.description}` : item.value;
292
737
  });
293
738
  return lines.join("\n");
@@ -310,6 +755,9 @@ _rig_completion() {
310
755
  local completions
311
756
  completions="$(command rig __complete --shell bash --index "$COMP_CWORD" -- "\${COMP_WORDS[@]}" 2>/dev/null)"
312
757
  COMPREPLY=($(compgen -W "$completions" -- "\${COMP_WORDS[COMP_CWORD]}"))
758
+ if [[ "\${#COMPREPLY[@]}" -eq 1 && ( "\${COMPREPLY[0]}" == */ || "\${COMPREPLY[0]}" == *= ) ]]; then
759
+ compopt -o nospace 2>/dev/null || true
760
+ fi
313
761
  }
314
762
  complete -F _rig_completion rig
315
763
  `;
@@ -327,11 +775,8 @@ complete -c rig -f -a "(__rig_complete)"
327
775
  `;
328
776
  }
329
777
 
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.
778
+ return `#compdef rig
779
+ # rig zsh completion generated by \`rig completion zsh\`.
335
780
 
336
781
  () {
337
782
  zstyle ':completion:*:rig:*:descriptions' format $'\\e[1;34m%d\\e[0m'
@@ -381,166 +826,170 @@ compdef _rig rig
381
826
  `;
382
827
  }
383
828
 
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;
829
+ function parseInlineValueOption(current: string): { option: string; value: string; prefix: string } | undefined {
830
+ const index = current.indexOf("=");
831
+ if (index < 0) return undefined;
832
+ return {
833
+ option: current.slice(0, index),
834
+ value: current.slice(index + 1),
835
+ prefix: current.slice(0, index + 1),
836
+ };
837
+ }
393
838
 
394
- const canonical = COMMAND_ALIASES.get(word) ?? word;
395
- if (COMMANDS.some((command) => command.value === canonical)) return canonical;
396
- }
397
- return undefined;
839
+ function parseRunArgs(context: CompletionContext): { workspace?: string; operation?: string; args: string[] } {
840
+ const basePositionals = positionalTokensFrom(context.argsBefore, COMMAND_OPTIONS.run);
841
+ const workspace = basePositionals[0]?.value;
842
+ const operation = basePositionals[1]?.value;
843
+ const operationTokenIndex = basePositionals[1]?.index;
844
+ return {
845
+ workspace,
846
+ operation,
847
+ args: operationTokenIndex === undefined ? [] : context.argsBefore.slice(operationTokenIndex + 1),
848
+ };
398
849
  }
399
850
 
400
- function countPositionals(words: string[], command: string): number {
401
- let foundCommand = false;
402
- let count = 0;
851
+ function parseRmArgs(context: CompletionContext): { workspace?: string } {
852
+ const positionals = positionalsFrom(context.argsBefore, COMMAND_OPTIONS.rm);
853
+ return { workspace: positionals[0] };
854
+ }
403
855
 
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;
856
+ function parseCacheArgs(context: CompletionContext): { subcommand?: string; args: string[] } {
857
+ const positionals = positionalsFrom(context.argsBefore, COMMAND_OPTIONS.cache);
858
+ return {
859
+ subcommand: positionals[0],
860
+ args: positionals.slice(1),
861
+ };
862
+ }
863
+
864
+ function positionalsFrom(tokens: string[], options: OptionDefinition[]): string[] {
865
+ return positionalTokensFrom(tokens, options).map((token) => token.value);
866
+ }
867
+
868
+ function positionalTokensFrom(tokens: string[], options: OptionDefinition[]): Array<{ value: string; index: number }> {
869
+ const positionalTokens: Array<{ value: string; index: number }> = [];
870
+
871
+ for (let index = 0; index < tokens.length; index += 1) {
872
+ const word = tokens[index]!;
873
+ if (word === "--") {
874
+ positionalTokens.push(...tokens.slice(index + 1).map((value, offset) => ({ value, index: index + 1 + offset })));
875
+ break;
409
876
  }
410
- if (word.startsWith("--") && word.includes("=")) continue;
411
- if (word.startsWith("-")) continue;
412
877
 
413
- const canonical = COMMAND_ALIASES.get(word) ?? word;
414
- if (!foundCommand && canonical === command) {
415
- foundCommand = true;
878
+ const option = findOption(options, word) ?? findOption(GLOBAL_OPTIONS, word);
879
+ if (option && isOptionToken(word)) {
880
+ if (option.takesValue && !hasInlineValue(word)) index += 1;
416
881
  continue;
417
882
  }
418
- if (foundCommand) count += 1;
883
+
884
+ if (isOptionToken(word)) continue;
885
+ positionalTokens.push({ value: word, index });
419
886
  }
420
887
 
421
- return count;
888
+ return positionalTokens;
422
889
  }
423
890
 
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;
432
- }
433
- if (word.startsWith("--") && word.includes("=")) continue;
434
- if (word.startsWith("-")) continue;
435
- if (!foundRun) {
436
- if (word === "run") foundRun = true;
437
- continue;
438
- }
439
- args.push(word);
440
- }
441
- return { workspace: args[0], operation: args[1], args: args.slice(2) };
891
+ function operationOptions(operation: RuntimeOperationDefinition | undefined): OptionDefinition[] {
892
+ if (!operation) return [];
893
+ return inferOperationOptions(operation).map((runtimeOption) =>
894
+ option([runtimeOption.flag, ...(runtimeOption.aliases ?? [])], optionDescription(operation, runtimeOption), {
895
+ takesValue: runtimeOption.type !== "boolean",
896
+ operation,
897
+ runtimeOption,
898
+ })
899
+ );
442
900
  }
443
901
 
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) };
902
+ function inferOperationOptions(operation: RuntimeOperationDefinition): RuntimeOperationCliOption[] {
903
+ const properties = operation.inputSchema?.properties ?? {};
904
+ const runtimeOptions = operation.cli?.options ?? Object.entries(properties).map(([name, schema]) => ({
905
+ name,
906
+ flag: `--${dashCase(name)}`,
907
+ required: operation.inputSchema?.required?.includes(name),
908
+ type: schema.type === "boolean" ? "boolean" : schema.type === "number" ? "number" : "string",
909
+ } satisfies RuntimeOperationCliOption));
910
+
911
+ return runtimeOptions.map((runtimeOption) => ({
912
+ ...runtimeOption,
913
+ type: runtimeOption.type ?? schemaType(properties[runtimeOption.name]) ?? "string",
914
+ }));
462
915
  }
463
916
 
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) };
917
+ function operationPositionalValueItems(
918
+ operation: RuntimeOperationDefinition,
919
+ positionalIndex: number,
920
+ current: string,
921
+ ): CompletionItem[] {
922
+ const positionals = operation.cli?.positionals ?? [];
923
+ const positional = positionals.find((item) => item.index === positionalIndex);
924
+ if (!positional) return [];
925
+
926
+ const schema = operation.inputSchema?.properties?.[positional.name];
927
+ const enumItems = enumCompletionItems(schema);
928
+ return filterItems(enumItems, current);
482
929
  }
483
930
 
484
- function cacheOptionTargets(words: string[]): CompletionItem[] {
485
- const subcommand = parseCacheCommand(words).subcommand;
486
- return subcommand ? CACHE_SUBCOMMAND_OPTIONS[subcommand] ?? [] : [];
931
+ function optionDescription(operation: RuntimeOperationDefinition, option: RuntimeOperationCliOption): string {
932
+ const schemaDescription = operation.inputSchema?.properties?.[option.name]?.description;
933
+ if (schemaDescription) return schemaDescription;
934
+ return option.required ? `${option.name} (required)` : option.name;
487
935
  }
488
936
 
489
- function optionExpectingValue(words: string[]): string | undefined {
490
- const previous = words.at(-1);
491
- return previous && OPTIONS_WITH_VALUES.has(previous) ? previous : undefined;
937
+ function schemaType(schema: JsonSchemaProperty | undefined): RuntimeOperationCliOption["type"] | undefined {
938
+ if (schema?.type === "boolean" || schema?.type === "number" || schema?.type === "string") return schema.type;
939
+ return undefined;
492
940
  }
493
941
 
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 = [];
942
+ function enumCompletionItems(schema: JsonSchemaProperty | undefined): CompletionItem[] {
943
+ const values = schema?.enum ?? [];
944
+ return values
945
+ .filter((value): value is string => typeof value === "string")
946
+ .map((value) => ({ value, group: GROUP_VALUES }));
947
+ }
948
+
949
+ function optionItems(options: OptionDefinition[]): CompletionItem[] {
950
+ return dedupeItems(options.flatMap((option) =>
951
+ (option.completions ?? option.flags.map((value): { value: string; noSpace?: boolean } => ({ value }))).map((completion) => ({
952
+ value: completion.value,
953
+ description: option.description,
954
+ noSpace: completion.noSpace,
955
+ group: option.group,
956
+ }))
957
+ ));
958
+ }
959
+
960
+ function mergeOptions(options: OptionDefinition[]): OptionDefinition[] {
961
+ const seen = new Set<string>();
962
+ const merged: OptionDefinition[] = [];
963
+ for (const option of options) {
964
+ const key = option.flags.join("\0");
965
+ if (option.flags.some((flag) => seen.has(flag))) continue;
966
+ for (const flag of option.flags) seen.add(flag);
967
+ seen.add(key);
968
+ merged.push(option);
525
969
  }
970
+ return merged;
971
+ }
526
972
 
527
- if (!input.inlinePrefix) return items;
528
- return items.map((item) => ({
529
- ...item,
530
- value: `${input.inlinePrefix}${item.value}`,
531
- }));
973
+ function findOption(options: OptionDefinition[], word: string): OptionDefinition | undefined {
974
+ const flag = optionFlag(word);
975
+ return options.find((option) => option.flags.includes(flag));
532
976
  }
533
977
 
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
- };
978
+ function optionFlag(word: string): string {
979
+ const index = word.indexOf("=");
980
+ return index < 0 ? word : word.slice(0, index);
981
+ }
982
+
983
+ function hasInlineValue(word: string): boolean {
984
+ return word.includes("=");
985
+ }
986
+
987
+ function isOptionToken(word: string): boolean {
988
+ return word.startsWith("-") && word !== "-";
989
+ }
990
+
991
+ function isCommandName(value: string): value is CommandName {
992
+ return COMMAND_NAMES.has(value as CommandName);
544
993
  }
545
994
 
546
995
  function resolveProjectDir(words: string[], cwd: string): { projectDir: string; configPath: string } {
@@ -662,7 +1111,7 @@ function completePathEntries(
662
1111
  if (options.fileFilter && !options.fileFilter(entry.name)) return [];
663
1112
  return [{
664
1113
  value: `${dirPart}${entry.name}`,
665
- description: "config",
1114
+ description: options.fileFilter ? "config" : "file",
666
1115
  group: GROUP_PATHS,
667
1116
  }];
668
1117
  })
@@ -704,17 +1153,19 @@ function splitCompletionPath(baseDir: string, current: string): {
704
1153
  };
705
1154
  }
706
1155
 
707
- async function workspaceTargets(
1156
+ async function safeWorkspaceTargets(
708
1157
  paths: { projectDir: string; configPath: string },
709
1158
  ): 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);
1159
+ try {
1160
+ const workspaces = await readWorkspaces(paths);
1161
+ return dedupeItems(workspaces.map((workspace) => ({
1162
+ value: workspace.name,
1163
+ description: workspaceDescription(workspace),
1164
+ group: GROUP_WORKSPACES,
1165
+ })));
1166
+ } catch {
1167
+ return [];
1168
+ }
718
1169
  }
719
1170
 
720
1171
  async function readWorkspaces(paths: { projectDir: string; configPath: string }): Promise<RuntimeWorkspaceCompletion[]> {
@@ -728,6 +1179,22 @@ async function readWorkspaces(paths: { projectDir: string; configPath: string })
728
1179
  }));
729
1180
  }
730
1181
 
1182
+ async function safeWorkflowTargets(
1183
+ paths: { projectDir: string; configPath: string },
1184
+ ): Promise<CompletionItem[]> {
1185
+ try {
1186
+ const runtime = await getOrStartRuntime(paths);
1187
+ const { workflows } = await runtime.control.workflows();
1188
+ return workflows.map((workflow) => ({
1189
+ value: workflow.name,
1190
+ description: "workflow",
1191
+ group: GROUP_VALUES,
1192
+ }));
1193
+ } catch {
1194
+ return [];
1195
+ }
1196
+ }
1197
+
731
1198
  async function safeWorkspaceOperationTargets(
732
1199
  paths: { projectDir: string; configPath: string },
733
1200
  ): Promise<CompletionItem[]> {
@@ -740,18 +1207,36 @@ async function safeWorkspaceOperationTargets(
740
1207
  }
741
1208
 
742
1209
  function workspaceOperationTargets(manifest: RuntimeOperationManifest): CompletionItem[] {
743
- return (manifest.workspaceOperations ?? []).flatMap((operation) => [
1210
+ return dedupeItems((manifest.workspaceOperations ?? []).flatMap((operation) => [
744
1211
  {
745
1212
  value: operation.id,
746
- description: operation.description ?? "workspace operation",
1213
+ description: operation.description || "workspace operation",
747
1214
  group: GROUP_OPERATIONS,
748
1215
  },
749
1216
  ...(operation.aliases ?? []).map((alias) => ({
750
1217
  value: alias,
751
- description: operation.description ?? "workspace operation",
1218
+ description: operation.description || "workspace operation",
752
1219
  group: GROUP_OPERATIONS,
753
1220
  })),
754
- ]);
1221
+ ]));
1222
+ }
1223
+
1224
+ async function safeCacheInvalidateTargets(
1225
+ paths: { projectDir: string; configPath: string },
1226
+ ): Promise<CompletionItem[]> {
1227
+ try {
1228
+ const runtime = await getOrStartRuntime(paths);
1229
+ const cache = await runtime.control.cache() as unknown as { entries: readonly RuntimeCacheCompletionEntry[] };
1230
+ return dedupeItems(cache.entries
1231
+ .filter((entry) => entry.scope === "local" && !entry.invalidated)
1232
+ .map((entry) => ({
1233
+ value: entry.nodePath || entry.nodeName,
1234
+ description: entry.workflow ? `workflow ${entry.workflow}` : "cached task",
1235
+ group: GROUP_CACHE,
1236
+ })));
1237
+ } catch {
1238
+ return [];
1239
+ }
755
1240
  }
756
1241
 
757
1242
  async function resolveRuntimeOperation(
@@ -842,3 +1327,7 @@ export function formatWorkspaceAge(createdAt: string, nowMs = Date.now()): strin
842
1327
 
843
1328
  return `${Math.floor(elapsedMonths / 12)}y ago`;
844
1329
  }
1330
+
1331
+ function dashCase(value: string): string {
1332
+ return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
1333
+ }