@camunda8/cli 2.8.0-alpha.5 → 2.8.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1145 -73
- package/dist/command-dispatch.js +1 -1
- package/dist/command-dispatch.js.map +1 -1
- package/dist/commands/completion.d.ts +6 -32
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +8 -687
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/mcp-proxy.d.ts +0 -17
- package/dist/commands/mcp-proxy.d.ts.map +1 -1
- package/dist/commands/mcp-proxy.js +3 -104
- package/dist/commands/mcp-proxy.js.map +1 -1
- package/dist/commands/open.d.ts +3 -44
- package/dist/commands/open.d.ts.map +1 -1
- package/dist/commands/open.js +5 -81
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/plugins.d.ts +2 -8
- package/dist/commands/plugins.d.ts.map +1 -1
- package/dist/commands/plugins.js +2 -28
- package/dist/commands/plugins.js.map +1 -1
- package/dist/commands/search.d.ts +2 -39
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +2 -83
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/watch.d.ts +2 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +3 -2
- package/dist/commands/watch.js.map +1 -1
- package/dist/completion.d.ts +36 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +689 -0
- package/dist/completion.js.map +1 -0
- package/dist/{commands/deployments.d.ts → deployments.d.ts} +1 -1
- package/dist/deployments.d.ts.map +1 -0
- package/dist/{commands/deployments.js → deployments.js} +8 -8
- package/dist/deployments.js.map +1 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/{commands/help.js → help.js} +4 -4
- package/dist/help.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp-proxy-helpers.d.ts +23 -0
- package/dist/mcp-proxy-helpers.d.ts.map +1 -0
- package/dist/mcp-proxy-helpers.js +109 -0
- package/dist/mcp-proxy-helpers.js.map +1 -0
- package/dist/open-helpers.d.ts +52 -0
- package/dist/open-helpers.d.ts.map +1 -0
- package/dist/open-helpers.js +88 -0
- package/dist/open-helpers.js.map +1 -0
- package/dist/plugin-version.d.ts +15 -0
- package/dist/plugin-version.d.ts.map +1 -0
- package/dist/plugin-version.js +37 -0
- package/dist/plugin-version.js.map +1 -0
- package/dist/search-helpers.d.ts +46 -0
- package/dist/search-helpers.d.ts.map +1 -0
- package/dist/search-helpers.js +90 -0
- package/dist/search-helpers.js.map +1 -0
- package/dist/watch-constants.d.ts +7 -0
- package/dist/watch-constants.d.ts.map +1 -0
- package/dist/watch-constants.js +7 -0
- package/dist/watch-constants.js.map +1 -0
- package/package.json +9 -5
- package/dist/commands/deployments.d.ts.map +0 -1
- package/dist/commands/deployments.js.map +0 -1
- package/dist/commands/help.d.ts.map +0 -1
- package/dist/commands/help.js.map +0 -1
- /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
|