@camunda8/cli 2.8.0-alpha.4 → 2.8.0-alpha.6

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.
Files changed (70) hide show
  1. package/README.md +1145 -73
  2. package/dist/command-dispatch.js +1 -1
  3. package/dist/command-dispatch.js.map +1 -1
  4. package/dist/commands/completion.d.ts +6 -32
  5. package/dist/commands/completion.d.ts.map +1 -1
  6. package/dist/commands/completion.js +8 -687
  7. package/dist/commands/completion.js.map +1 -1
  8. package/dist/commands/mcp-proxy.d.ts +0 -17
  9. package/dist/commands/mcp-proxy.d.ts.map +1 -1
  10. package/dist/commands/mcp-proxy.js +3 -104
  11. package/dist/commands/mcp-proxy.js.map +1 -1
  12. package/dist/commands/open.d.ts +3 -44
  13. package/dist/commands/open.d.ts.map +1 -1
  14. package/dist/commands/open.js +5 -81
  15. package/dist/commands/open.js.map +1 -1
  16. package/dist/commands/plugins.d.ts +2 -8
  17. package/dist/commands/plugins.d.ts.map +1 -1
  18. package/dist/commands/plugins.js +2 -28
  19. package/dist/commands/plugins.js.map +1 -1
  20. package/dist/commands/run.d.ts +8 -8
  21. package/dist/commands/run.d.ts.map +1 -1
  22. package/dist/commands/run.js +60 -60
  23. package/dist/commands/run.js.map +1 -1
  24. package/dist/commands/search.d.ts +2 -39
  25. package/dist/commands/search.d.ts.map +1 -1
  26. package/dist/commands/search.js +2 -83
  27. package/dist/commands/search.js.map +1 -1
  28. package/dist/commands/watch.d.ts +2 -1
  29. package/dist/commands/watch.d.ts.map +1 -1
  30. package/dist/commands/watch.js +3 -2
  31. package/dist/commands/watch.js.map +1 -1
  32. package/dist/completion.d.ts +36 -0
  33. package/dist/completion.d.ts.map +1 -0
  34. package/dist/completion.js +689 -0
  35. package/dist/completion.js.map +1 -0
  36. package/dist/{commands/deployments.d.ts → deployments.d.ts} +1 -1
  37. package/dist/deployments.d.ts.map +1 -0
  38. package/dist/{commands/deployments.js → deployments.js} +8 -8
  39. package/dist/deployments.js.map +1 -0
  40. package/dist/help.d.ts.map +1 -0
  41. package/dist/{commands/help.js → help.js} +4 -4
  42. package/dist/help.js.map +1 -0
  43. package/dist/index.js +2 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/mcp-proxy-helpers.d.ts +23 -0
  46. package/dist/mcp-proxy-helpers.d.ts.map +1 -0
  47. package/dist/mcp-proxy-helpers.js +109 -0
  48. package/dist/mcp-proxy-helpers.js.map +1 -0
  49. package/dist/open-helpers.d.ts +52 -0
  50. package/dist/open-helpers.d.ts.map +1 -0
  51. package/dist/open-helpers.js +88 -0
  52. package/dist/open-helpers.js.map +1 -0
  53. package/dist/plugin-version.d.ts +15 -0
  54. package/dist/plugin-version.d.ts.map +1 -0
  55. package/dist/plugin-version.js +37 -0
  56. package/dist/plugin-version.js.map +1 -0
  57. package/dist/search-helpers.d.ts +46 -0
  58. package/dist/search-helpers.d.ts.map +1 -0
  59. package/dist/search-helpers.js +90 -0
  60. package/dist/search-helpers.js.map +1 -0
  61. package/dist/watch-constants.d.ts +7 -0
  62. package/dist/watch-constants.d.ts.map +1 -0
  63. package/dist/watch-constants.js +7 -0
  64. package/dist/watch-constants.js.map +1 -0
  65. package/package.json +9 -5
  66. package/dist/commands/deployments.d.ts.map +0 -1
  67. package/dist/commands/deployments.js.map +0 -1
  68. package/dist/commands/help.d.ts.map +0 -1
  69. package/dist/commands/help.js.map +0 -1
  70. /package/dist/{commands/help.d.ts → help.d.ts} +0 -0
@@ -0,0 +1,689 @@
1
+ /**
2
+ * Shell completion commands — derived from COMMAND_REGISTRY + plugins.
3
+ */
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { homedir, platform } from "node:os";
6
+ import { join } from "node:path";
7
+ import { COMMAND_REGISTRY, GLOBAL_FLAGS, RESOURCE_ALIASES, } from "./command-registry.js";
8
+ import { getUserDataDir } from "./config.js";
9
+ import { getLogger } from "./logger.js";
10
+ import { getPluginCommandsInfo, } from "./plugin-loader.js";
11
+ import { c8ctl } from "./runtime.js";
12
+ // ─── Typed helpers (same pattern as help.ts) ─────────────────────────────────
13
+ /** Typed entries for COMMAND_REGISTRY — avoids per-loop casts. */
14
+ function registryEntries() {
15
+ return Object.entries(COMMAND_REGISTRY);
16
+ }
17
+ /** Typed entries for a flag record — avoids per-loop casts. */
18
+ function flagEntries(flags) {
19
+ return Object.entries(flags);
20
+ }
21
+ // ─── Derived completion data ─────────────────────────────────────────────────
22
+ /** Reverse map: canonical resource → all names that resolve to it (including itself). */
23
+ function buildCanonicalToAliases() {
24
+ const map = new Map();
25
+ // Seed with every known canonical name from the registry
26
+ for (const [, def] of registryEntries()) {
27
+ for (const r of def.resources) {
28
+ const canonical = RESOURCE_ALIASES[r] ?? r;
29
+ if (!map.has(canonical)) {
30
+ map.set(canonical, [canonical]);
31
+ }
32
+ }
33
+ }
34
+ // Add every alias
35
+ for (const [alias, canonical] of Object.entries(RESOURCE_ALIASES)) {
36
+ if (!map.has(canonical)) {
37
+ map.set(canonical, [canonical]);
38
+ }
39
+ const arr = map.get(canonical) ?? [];
40
+ if (!arr.includes(alias)) {
41
+ arr.push(alias);
42
+ }
43
+ }
44
+ return map;
45
+ }
46
+ /** All accepted forms for a verb's resources (canonical + all aliases). */
47
+ function resourceFormsForVerb(def, canonicalToAliases) {
48
+ const seen = new Set();
49
+ for (const r of def.resources) {
50
+ const canonical = RESOURCE_ALIASES[r] ?? r;
51
+ for (const form of canonicalToAliases.get(canonical) ?? [canonical]) {
52
+ seen.add(form);
53
+ }
54
+ }
55
+ return [...seen];
56
+ }
57
+ function deriveVerbInfos(pluginCommandsInfo) {
58
+ const canonicalToAliases = buildCanonicalToAliases();
59
+ const infos = [];
60
+ for (const [verb, def] of registryEntries()) {
61
+ // Skip mcp-proxy — not a user-facing verb
62
+ if (verb === "mcp-proxy")
63
+ continue;
64
+ const resources = resourceFormsForVerb(def, canonicalToAliases);
65
+ const fileComplete = !def.requiresResource &&
66
+ def.resources.length === 0 &&
67
+ ["deploy", "run", "watch"].includes(verb);
68
+ infos.push({
69
+ verb,
70
+ description: def.description,
71
+ resources,
72
+ aliases: def.aliases ?? [],
73
+ fileComplete,
74
+ });
75
+ }
76
+ // Plugin-provided verbs (e.g. cluster)
77
+ for (const cmd of pluginCommandsInfo) {
78
+ // Skip if already in registry
79
+ if (cmd.commandName in COMMAND_REGISTRY)
80
+ continue;
81
+ infos.push({
82
+ verb: cmd.commandName,
83
+ description: cmd.description ?? "",
84
+ resources: (cmd.subcommands ?? []).map((s) => s.name),
85
+ aliases: [],
86
+ fileComplete: false,
87
+ });
88
+ }
89
+ return infos;
90
+ }
91
+ /** Collect all unique flag names across all commands + global + search flags. */
92
+ function deriveAllFlagNames() {
93
+ const names = new Set();
94
+ for (const name of Object.keys(GLOBAL_FLAGS)) {
95
+ names.add(name);
96
+ }
97
+ for (const [, def] of registryEntries()) {
98
+ for (const [name] of flagEntries(def.flags)) {
99
+ names.add(name);
100
+ }
101
+ if (def.resourceFlags) {
102
+ for (const rf of Object.values(def.resourceFlags)) {
103
+ for (const name of Object.keys(rf)) {
104
+ names.add(name);
105
+ }
106
+ }
107
+ }
108
+ }
109
+ return [...names].map((n) => `--${n}`);
110
+ }
111
+ /** Collect all flags with descriptions and types for rich completions (zsh/fish). */
112
+ function deriveAllFlags() {
113
+ const seen = new Map();
114
+ function addFlags(flags) {
115
+ for (const [name, def] of flagEntries(flags)) {
116
+ if (!seen.has(name)) {
117
+ seen.set(name, {
118
+ description: def.description,
119
+ type: def.type,
120
+ short: def.short,
121
+ });
122
+ }
123
+ }
124
+ }
125
+ addFlags(GLOBAL_FLAGS);
126
+ for (const [, def] of registryEntries()) {
127
+ addFlags(def.flags);
128
+ if (def.resourceFlags) {
129
+ for (const rf of Object.values(def.resourceFlags)) {
130
+ addFlags(rf);
131
+ }
132
+ }
133
+ }
134
+ return [...seen].map(([name, info]) => ({
135
+ name,
136
+ ...info,
137
+ }));
138
+ }
139
+ /** Get resources for the `help` verb: verbs with hasDetailedHelp + special topics. */
140
+ function deriveHelpResources() {
141
+ const items = [];
142
+ for (const [verb, def] of registryEntries()) {
143
+ if (def.hasDetailedHelp) {
144
+ items.push({ name: verb, description: `Show ${verb} command help` });
145
+ }
146
+ }
147
+ // Special topics that aren't verbs but have help pages
148
+ items.push({
149
+ name: "profiles",
150
+ description: "Show profile management help",
151
+ }, {
152
+ name: "profile",
153
+ description: "Alias for profile management help",
154
+ }, {
155
+ name: "plugin",
156
+ description: "Show plugin management help",
157
+ }, {
158
+ name: "plugins",
159
+ description: "Alias for plugin management help",
160
+ });
161
+ // Plugin verbs
162
+ const pluginCmds = getPluginCommandsInfo();
163
+ for (const cmd of pluginCmds) {
164
+ if (!items.some((i) => i.name === cmd.commandName) &&
165
+ !(cmd.commandName in COMMAND_REGISTRY)) {
166
+ items.push({
167
+ name: cmd.commandName,
168
+ description: cmd.description
169
+ ? `Show ${cmd.commandName} command help`
170
+ : `No detailed help; use c8ctl help for general usage`,
171
+ });
172
+ }
173
+ }
174
+ return items;
175
+ }
176
+ // ─── Bash completion ─────────────────────────────────────────────────────────
177
+ function generateBashCompletion() {
178
+ const pluginCmds = getPluginCommandsInfo();
179
+ const verbInfos = deriveVerbInfos(pluginCmds);
180
+ const allFlags = deriveAllFlagNames();
181
+ const helpResources = deriveHelpResources();
182
+ // All verb names (including aliases)
183
+ const allVerbs = new Set();
184
+ for (const v of verbInfos) {
185
+ allVerbs.add(v.verb);
186
+ for (const a of v.aliases)
187
+ allVerbs.add(a);
188
+ }
189
+ const verbsStr = [...allVerbs].join(" ");
190
+ const flagsStr = allFlags.join(" ");
191
+ // Build per-verb resource variables
192
+ const resourceVars = [];
193
+ const caseBranches = [];
194
+ for (const v of verbInfos) {
195
+ if (v.verb === "help") {
196
+ // Help completes to verbs/topics, not resources
197
+ resourceVars.push(` local help_resources="${helpResources.map((r) => r.name).join(" ")}"`);
198
+ caseBranches.push(` help)\n COMPREPLY=( $(compgen -W "\${help_resources}" -- "\${cur}") )\n ;;`);
199
+ continue;
200
+ }
201
+ if (v.fileComplete) {
202
+ // deploy/run/watch complete with files
203
+ caseBranches.push(` ${v.verb})\n COMPREPLY=( $(compgen -f -- "\${cur}") )\n ;;`);
204
+ continue;
205
+ }
206
+ if (v.resources.length === 0)
207
+ continue;
208
+ const varName = `${v.verb.replace(/-/g, "_")}_resources`;
209
+ resourceVars.push(` local ${varName}="${v.resources.join(" ")}"`);
210
+ // Include aliases in the case pattern
211
+ const casePattern = v.aliases.length > 0 ? `${v.verb}|${v.aliases.join("|")}` : v.verb;
212
+ caseBranches.push(` ${casePattern})\n COMPREPLY=( $(compgen -W "\${${varName}}" -- "\${cur}") )\n ;;`);
213
+ }
214
+ return `# c8ctl-completion-version: ${c8ctl.version}
215
+ # c8ctl bash completion
216
+ _c8ctl_completions() {
217
+ local cur prev words cword
218
+
219
+ # Initialize completion variables (standalone, no bash-completion dependency)
220
+ COMPREPLY=()
221
+ cur="\${COMP_WORDS[COMP_CWORD]}"
222
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
223
+ words=("\${COMP_WORDS[@]}")
224
+ cword=\${COMP_CWORD}
225
+
226
+ # Commands (verbs)
227
+ local verbs="${verbsStr}"
228
+
229
+ # Resources by verb
230
+ ${resourceVars.join("\n")}
231
+
232
+ # Global flags
233
+ local flags="${flagsStr}"
234
+
235
+ case \${cword} in
236
+ 1)
237
+ # Complete verbs
238
+ COMPREPLY=( $(compgen -W "\${verbs}" -- "\${cur}") )
239
+ ;;
240
+ 2)
241
+ # Complete resources based on verb
242
+ local verb="\${words[1]}"
243
+ case "\${verb}" in
244
+ ${caseBranches.join("\n")}
245
+ esac
246
+ ;;
247
+ *)
248
+ # Complete flags or files
249
+ if [[ \${cur} == -* ]]; then
250
+ COMPREPLY=( $(compgen -W "\${flags}" -- "\${cur}") )
251
+ else
252
+ COMPREPLY=( $(compgen -f -- "\${cur}") )
253
+ fi
254
+ ;;
255
+ esac
256
+ }
257
+
258
+ complete -F _c8ctl_completions c8ctl
259
+ complete -F _c8ctl_completions c8
260
+ `;
261
+ }
262
+ // ─── Zsh completion ──────────────────────────────────────────────────────────
263
+ function generateZshCompletion() {
264
+ const pluginCmds = getPluginCommandsInfo();
265
+ const verbInfos = deriveVerbInfos(pluginCmds);
266
+ const allFlags = deriveAllFlags();
267
+ const helpResources = deriveHelpResources();
268
+ // Verb entries: 'verb:description'
269
+ const verbEntries = verbInfos.map((v) => {
270
+ const items = [` '${v.verb}:${escZsh(v.description)}'`];
271
+ for (const a of v.aliases) {
272
+ items.push(` '${a}:${escZsh(v.description)}'`);
273
+ }
274
+ return items.join("\n");
275
+ });
276
+ // Flag entries: '--flag[description]:hint:' or '--flag[description]'
277
+ const flagEntryLines = allFlags.map((f) => {
278
+ const desc = escZsh(f.description);
279
+ if (f.short) {
280
+ return ` '-${f.short}[${desc}]'\n '--${f.name}[${desc}]${f.type === "string" ? `:${f.name}:` : ""}'`;
281
+ }
282
+ return ` '--${f.name}[${desc}]${f.type === "string" ? `:${f.name}:` : ""}'`;
283
+ });
284
+ // Per-verb resource case branches
285
+ const resourceCases = [];
286
+ for (const v of verbInfos) {
287
+ if (v.verb === "help") {
288
+ const entries = helpResources.map((r) => ` '${r.name}:${escZsh(r.description)}'`);
289
+ resourceCases.push(` help)\n resources=(\n${entries.join("\n")}\n )\n _describe 'resource' resources\n ;;`);
290
+ continue;
291
+ }
292
+ if (v.fileComplete) {
293
+ const casePattern = v.aliases.length > 0 ? `${v.verb}|${v.aliases.join("|")}` : v.verb;
294
+ resourceCases.push(` ${casePattern})\n _files\n ;;`);
295
+ continue;
296
+ }
297
+ if (v.resources.length === 0)
298
+ continue;
299
+ const entries = v.resources.map((r) => ` '${r}:${escZsh(capitalize(v.verb))} ${resourceDisplayName(r)}'`);
300
+ const casePattern = v.aliases.length > 0 ? `${v.verb}|${v.aliases.join("|")}` : v.verb;
301
+ resourceCases.push(` ${casePattern})\n resources=(\n${entries.join("\n")}\n )\n _describe 'resource' resources\n ;;`);
302
+ }
303
+ return `#compdef c8ctl c8
304
+ # c8ctl-completion-version: ${c8ctl.version}
305
+
306
+ _c8ctl() {
307
+ local -a verbs resources flags
308
+
309
+ verbs=(
310
+ ${verbEntries.join("\n")}
311
+ )
312
+
313
+ flags=(
314
+ ${flagEntryLines.join("\n")}
315
+ )
316
+
317
+ case $CURRENT in
318
+ 2)
319
+ _describe 'command' verbs
320
+ ;;
321
+ 3)
322
+ case "\${words[2]}" in
323
+ ${resourceCases.join("\n")}
324
+ esac
325
+ ;;
326
+ *)
327
+ _arguments \${flags[@]}
328
+ ;;
329
+ esac
330
+ }
331
+
332
+ if (( $+functions[compdef] )); then
333
+ compdef _c8ctl c8ctl c8
334
+ fi
335
+ `;
336
+ }
337
+ // ─── Fish completion ─────────────────────────────────────────────────────────
338
+ function generateFishCompletion() {
339
+ const pluginCmds = getPluginCommandsInfo();
340
+ const verbInfos = deriveVerbInfos(pluginCmds);
341
+ const allFlags = deriveAllFlags();
342
+ const helpResources = deriveHelpResources();
343
+ const lines = [
344
+ "# c8ctl fish completion",
345
+ "",
346
+ "# Remove all existing completions for c8ctl and c8",
347
+ "complete -c c8ctl -e",
348
+ "complete -c c8 -e",
349
+ "",
350
+ ];
351
+ // Global flags
352
+ lines.push("# Global flags");
353
+ for (const f of allFlags) {
354
+ const desc = escFish(f.description);
355
+ const req = f.type === "string" ? " -r" : "";
356
+ if (f.short) {
357
+ lines.push(`complete -c c8ctl -s ${f.short} -l ${f.name} -d '${desc}'${req}`);
358
+ lines.push(`complete -c c8 -s ${f.short} -l ${f.name} -d '${desc}'${req}`);
359
+ }
360
+ else {
361
+ lines.push(`complete -c c8ctl -l ${f.name} -d '${desc}'${req}`);
362
+ lines.push(`complete -c c8 -l ${f.name} -d '${desc}'${req}`);
363
+ }
364
+ }
365
+ lines.push("");
366
+ // Verb completions
367
+ lines.push("# Commands (verbs) - only suggest when no command is given yet");
368
+ for (const v of verbInfos) {
369
+ const desc = escFish(v.description);
370
+ lines.push(`complete -c c8ctl -n '__fish_use_subcommand' -a '${v.verb}' -d '${desc}'`);
371
+ lines.push(`complete -c c8 -n '__fish_use_subcommand' -a '${v.verb}' -d '${desc}'`);
372
+ for (const a of v.aliases) {
373
+ lines.push(`complete -c c8ctl -n '__fish_use_subcommand' -a '${a}' -d '${desc}'`);
374
+ lines.push(`complete -c c8 -n '__fish_use_subcommand' -a '${a}' -d '${desc}'`);
375
+ }
376
+ }
377
+ lines.push("");
378
+ // Per-verb resource completions
379
+ for (const v of verbInfos) {
380
+ if (v.verb === "help") {
381
+ lines.push(`# Resources for 'help' command`);
382
+ for (const r of helpResources) {
383
+ const desc = escFish(r.description);
384
+ lines.push(`complete -c c8ctl -n '__fish_seen_subcommand_from help' -a '${r.name}' -d '${desc}'`);
385
+ lines.push(`complete -c c8 -n '__fish_seen_subcommand_from help' -a '${r.name}' -d '${desc}'`);
386
+ }
387
+ lines.push("");
388
+ continue;
389
+ }
390
+ if (v.fileComplete || v.resources.length === 0)
391
+ continue;
392
+ const seenFrom = [v.verb, ...v.aliases].join(" ");
393
+ lines.push(`# Resources for '${v.verb}' command`);
394
+ for (const r of v.resources) {
395
+ const desc = escFish(`${capitalize(v.verb)} ${resourceDisplayName(r)}`);
396
+ lines.push(`complete -c c8ctl -n '__fish_seen_subcommand_from ${seenFrom}' -a '${r}' -d '${desc}'`);
397
+ lines.push(`complete -c c8 -n '__fish_seen_subcommand_from ${seenFrom}' -a '${r}' -d '${desc}'`);
398
+ }
399
+ lines.push("");
400
+ }
401
+ lines.unshift(`# c8ctl-completion-version: ${c8ctl.version}`);
402
+ return `${lines.join("\n")}\n`;
403
+ }
404
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
405
+ function capitalize(s) {
406
+ return s.charAt(0).toUpperCase() + s.slice(1);
407
+ }
408
+ /** Human-readable name from resource alias. */
409
+ function resourceDisplayName(resource) {
410
+ const canonical = RESOURCE_ALIASES[resource] ?? resource;
411
+ return canonical.replace(/-/g, " ") + (resource !== canonical ? "s" : "");
412
+ }
413
+ function escZsh(s) {
414
+ return s.replace(/'/g, "'\\''");
415
+ }
416
+ function escFish(s) {
417
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
418
+ }
419
+ // ─── Public API ──────────────────────────────────────────────────────────────
420
+ /**
421
+ * Show completion command
422
+ *
423
+ * Throws on missing/unknown shell; framework wrapper adds the
424
+ * `Failed to completion ...` prefix. Do NOT reintroduce `process.exit` —
425
+ * `tests/unit/no-process-exit-in-handlers.test.ts` enforces this.
426
+ */
427
+ export function showCompletion(shell) {
428
+ if (!shell) {
429
+ throw new Error("Shell type required. Usage: c8 completion <bash|zsh|fish>");
430
+ }
431
+ const normalizedShell = shell.toLowerCase();
432
+ switch (normalizedShell) {
433
+ case "bash":
434
+ console.log(generateBashCompletion());
435
+ break;
436
+ case "zsh":
437
+ console.log(generateZshCompletion());
438
+ break;
439
+ case "fish":
440
+ console.log(generateFishCompletion());
441
+ break;
442
+ default:
443
+ throw new Error(`Unknown shell: ${shell}. Supported shells: bash, zsh, fish. Usage: c8 completion <bash|zsh|fish>`);
444
+ }
445
+ }
446
+ // ─── Completion install ──────────────────────────────────────────────────────
447
+ /** Version header prefix used to tag generated completion files. */
448
+ const VERSION_HEADER_PREFIX = "# c8ctl-completion-version: ";
449
+ /** Completions subdirectory under the user data dir. */
450
+ const COMPLETIONS_DIR = "completions";
451
+ /** Shell file extensions. */
452
+ const SHELL_EXTENSIONS = {
453
+ bash: "bash",
454
+ zsh: "zsh",
455
+ fish: "fish",
456
+ };
457
+ /** Detect the user's shell from $SHELL. Returns lowercase shell name or undefined. */
458
+ export function detectShell() {
459
+ const shellPath = process.env.SHELL;
460
+ if (!shellPath)
461
+ return undefined;
462
+ const base = shellPath.split("/").pop();
463
+ if (!base)
464
+ return undefined;
465
+ const name = base.toLowerCase();
466
+ if (name === "bash" || name === "zsh" || name === "fish")
467
+ return name;
468
+ return undefined;
469
+ }
470
+ /** Get the appropriate RC file path for a given shell. */
471
+ export function getShellRcFile(shell) {
472
+ const home = homedir();
473
+ switch (shell) {
474
+ case "bash":
475
+ // macOS uses .bash_profile by default; Linux uses .bashrc
476
+ return platform() === "darwin"
477
+ ? join(home, ".bash_profile")
478
+ : join(home, ".bashrc");
479
+ case "zsh":
480
+ return join(home, ".zshrc");
481
+ case "fish":
482
+ // fish auto-loads from completions dir — no rc edit needed
483
+ return undefined;
484
+ default:
485
+ return undefined;
486
+ }
487
+ }
488
+ /** Get the path where the completion file will be written. */
489
+ export function getCompletionFilePath(shell) {
490
+ const ext = SHELL_EXTENSIONS[shell] ?? shell;
491
+ return join(getUserDataDir(), COMPLETIONS_DIR, `c8ctl.${ext}`);
492
+ }
493
+ /** Get the fish completions dir (fish auto-loads from here). Respects XDG_CONFIG_HOME. */
494
+ function getFishCompletionsDir() {
495
+ const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
496
+ return join(configHome, "fish", "completions");
497
+ }
498
+ /** Generate the source line that goes into the shell RC file.
499
+ * Uses single quotes to prevent shell expansion of the path. */
500
+ function buildSourceLine(completionFilePath) {
501
+ // Reject control characters (newlines, null, etc.) that could inject
502
+ // additional commands into the shell RC file.
503
+ for (const ch of completionFilePath) {
504
+ const code = ch.charCodeAt(0);
505
+ if (code <= 0x1f || code === 0x7f) {
506
+ throw new Error("Completion file path contains control characters.");
507
+ }
508
+ }
509
+ const escaped = completionFilePath.replaceAll("'", "'\\''");
510
+ return `source '${escaped}'`;
511
+ }
512
+ /** Generate the comment+source block for the RC file. */
513
+ function buildRcBlock(completionFilePath) {
514
+ return `\n# c8ctl shell completion\n${buildSourceLine(completionFilePath)}\n`;
515
+ }
516
+ /** Check if the RC file already contains the source line.
517
+ * Checks for both the current single-quoted source line and the legacy
518
+ * double-quoted form, so upgrades are detected without false-positiving
519
+ * on the raw path appearing in unrelated lines. */
520
+ function rcAlreadyConfigured(rcFile, completionFilePath) {
521
+ if (!existsSync(rcFile))
522
+ return false;
523
+ try {
524
+ const content = readFileSync(rcFile, "utf-8");
525
+ // Check for current single-quoted source line
526
+ if (content.includes(buildSourceLine(completionFilePath)))
527
+ return true;
528
+ // Check for legacy double-quoted source line
529
+ const escaped = completionFilePath.replaceAll('"', '\\"');
530
+ if (content.includes(`source "${escaped}"`))
531
+ return true;
532
+ return false;
533
+ }
534
+ catch {
535
+ return false;
536
+ }
537
+ }
538
+ /** Generate the completion script content for a given shell. */
539
+ function generateForShell(shell) {
540
+ switch (shell) {
541
+ case "bash":
542
+ return generateBashCompletion();
543
+ case "zsh":
544
+ return generateZshCompletion();
545
+ case "fish":
546
+ return generateFishCompletion();
547
+ default:
548
+ throw new Error(`Unknown shell: ${shell}`);
549
+ }
550
+ }
551
+ /** Extract the version from a completion file's header lines.
552
+ * Scans the first few lines so the header can appear after
553
+ * shell-required directives like zsh's #compdef. */
554
+ export function extractCompletionVersion(filePath) {
555
+ if (!existsSync(filePath))
556
+ return undefined;
557
+ try {
558
+ const content = readFileSync(filePath, "utf-8");
559
+ const lines = content.split("\n").slice(0, 5);
560
+ for (const line of lines) {
561
+ if (line.startsWith(VERSION_HEADER_PREFIX)) {
562
+ return line.slice(VERSION_HEADER_PREFIX.length).trim();
563
+ }
564
+ }
565
+ return undefined;
566
+ }
567
+ catch {
568
+ return undefined;
569
+ }
570
+ }
571
+ /**
572
+ * Install shell completions: write script to data dir, wire into RC file.
573
+ *
574
+ * Supports --dry-run via the c8ctl runtime flag.
575
+ */
576
+ export function installCompletion(shellOverride) {
577
+ const logger = getLogger();
578
+ const shell = shellOverride?.toLowerCase() ?? detectShell();
579
+ if (!shell) {
580
+ throw new Error("Could not detect shell. Specify with: c8ctl completion install --shell <bash|zsh|fish>");
581
+ }
582
+ if (!["bash", "zsh", "fish"].includes(shell)) {
583
+ throw new Error(`Unsupported shell: ${shell}. Supported shells: bash, zsh, fish`);
584
+ }
585
+ const completionFile = getCompletionFilePath(shell);
586
+ const rcFile = getShellRcFile(shell);
587
+ const rcConfigured = rcFile
588
+ ? rcAlreadyConfigured(rcFile, completionFile)
589
+ : true;
590
+ // Dry-run support
591
+ if (c8ctl.dryRun) {
592
+ const result = {
593
+ dryRun: true,
594
+ detectedShell: shell,
595
+ completionFile,
596
+ };
597
+ if (rcFile) {
598
+ result.rcFile = rcFile;
599
+ result.sourceLine = buildSourceLine(completionFile);
600
+ result.rcAlreadyConfigured = rcConfigured;
601
+ }
602
+ if (shell === "fish") {
603
+ result.fishCompletionsDir = getFishCompletionsDir();
604
+ }
605
+ logger.json(result);
606
+ return;
607
+ }
608
+ // Write the completion file
609
+ try {
610
+ const completionDir = join(getUserDataDir(), COMPLETIONS_DIR);
611
+ mkdirSync(completionDir, { recursive: true });
612
+ const script = generateForShell(shell);
613
+ writeFileSync(completionFile, script, "utf-8");
614
+ logger.info(`Completion script written to ${completionFile}`);
615
+ // Fish: also copy to fish auto-load directory
616
+ if (shell === "fish") {
617
+ const fishDir = getFishCompletionsDir();
618
+ mkdirSync(fishDir, { recursive: true });
619
+ writeFileSync(join(fishDir, "c8ctl.fish"), script, "utf-8");
620
+ logger.info(`Fish completion installed to ${fishDir}/c8ctl.fish`);
621
+ logger.info("Completions will be loaded automatically on next shell start.");
622
+ return;
623
+ }
624
+ // Wire into RC file (bash/zsh)
625
+ if (rcFile) {
626
+ if (rcConfigured) {
627
+ logger.info(`RC file already configured: ${rcFile}`);
628
+ }
629
+ else {
630
+ const block = buildRcBlock(completionFile);
631
+ writeFileSync(rcFile, block, { encoding: "utf-8", flag: "a" });
632
+ logger.info(`Added source line to ${rcFile}`);
633
+ }
634
+ }
635
+ logger.info("Restart your shell or run:");
636
+ logger.info(` ${buildSourceLine(completionFile)}`);
637
+ }
638
+ catch (err) {
639
+ const msg = err instanceof Error ? err.message : String(err);
640
+ logger.info(`Target path: ${completionFile}`);
641
+ if (rcFile) {
642
+ logger.info(`Add this line manually to your shell config:\n ${buildSourceLine(completionFile)}`);
643
+ }
644
+ throw new Error(`Failed to install completions: ${msg}`);
645
+ }
646
+ }
647
+ /**
648
+ * Refresh the installed completion file if the CLI version has changed.
649
+ *
650
+ * Called on every CLI invocation — no-op if completions are not installed
651
+ * or if the embedded version matches the running CLI version.
652
+ * Synchronous write (~1ms for a few KB).
653
+ */
654
+ export function refreshCompletionsIfStale() {
655
+ // Skip in dry-run mode — refresh is a side effect
656
+ if (c8ctl.dryRun)
657
+ return;
658
+ // Use the same source of truth as generateForShell() for version headers
659
+ const currentVersion = c8ctl.version;
660
+ // Check each shell — user may have installed for multiple shells
661
+ for (const shell of ["bash", "zsh", "fish"]) {
662
+ const filePath = getCompletionFilePath(shell);
663
+ const installed = extractCompletionVersion(filePath);
664
+ if (installed === undefined) {
665
+ // No version header — treat existing file as stale, regenerate
666
+ if (!existsSync(filePath))
667
+ continue; // not installed for this shell
668
+ }
669
+ else if (installed === currentVersion) {
670
+ continue; // up to date
671
+ }
672
+ // Stale — regenerate
673
+ try {
674
+ const script = generateForShell(shell);
675
+ writeFileSync(filePath, script, "utf-8");
676
+ // Fish: also update the auto-load copy
677
+ if (shell === "fish") {
678
+ const fishTarget = join(getFishCompletionsDir(), "c8ctl.fish");
679
+ if (existsSync(fishTarget)) {
680
+ writeFileSync(fishTarget, script, "utf-8");
681
+ }
682
+ }
683
+ }
684
+ catch {
685
+ // Best-effort — don't crash if the write fails
686
+ }
687
+ }
688
+ }
689
+ //# sourceMappingURL=completion.js.map