@canonical/summon 0.1.0
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 +439 -0
- package/generators/example/hello/index.ts +132 -0
- package/generators/example/hello/templates/README.md.ejs +20 -0
- package/generators/example/hello/templates/index.ts.ejs +9 -0
- package/generators/example/webapp/index.ts +509 -0
- package/generators/example/webapp/templates/ARCHITECTURE.md.ejs +180 -0
- package/generators/example/webapp/templates/App.tsx.ejs +86 -0
- package/generators/example/webapp/templates/README.md.ejs +154 -0
- package/generators/example/webapp/templates/app.test.ts.ejs +63 -0
- package/generators/example/webapp/templates/app.ts.ejs +132 -0
- package/generators/example/webapp/templates/feature.ts.ejs +264 -0
- package/generators/example/webapp/templates/index.html.ejs +20 -0
- package/generators/example/webapp/templates/main.tsx.ejs +43 -0
- package/generators/example/webapp/templates/styles.css.ejs +135 -0
- package/generators/init/index.ts +124 -0
- package/generators/init/templates/generator.ts.ejs +85 -0
- package/generators/init/templates/template-index.ts.ejs +9 -0
- package/generators/init/templates/template-test.ts.ejs +8 -0
- package/package.json +64 -0
- package/src/__tests__/combinators.test.ts +895 -0
- package/src/__tests__/dry-run.test.ts +927 -0
- package/src/__tests__/effect.test.ts +816 -0
- package/src/__tests__/interpreter.test.ts +673 -0
- package/src/__tests__/primitives.test.ts +970 -0
- package/src/__tests__/task.test.ts +929 -0
- package/src/__tests__/template.test.ts +666 -0
- package/src/cli-format.ts +165 -0
- package/src/cli-types.ts +53 -0
- package/src/cli.tsx +1322 -0
- package/src/combinators.ts +294 -0
- package/src/completion.ts +488 -0
- package/src/components/App.tsx +960 -0
- package/src/components/ExecutionProgress.tsx +205 -0
- package/src/components/FileTreePreview.tsx +97 -0
- package/src/components/PromptSequence.tsx +483 -0
- package/src/components/Spinner.tsx +36 -0
- package/src/components/index.ts +16 -0
- package/src/dry-run.ts +434 -0
- package/src/effect.ts +224 -0
- package/src/index.ts +266 -0
- package/src/interpreter.ts +463 -0
- package/src/primitives.ts +442 -0
- package/src/task.ts +245 -0
- package/src/template.ts +537 -0
- package/src/types.ts +453 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell Autocompletion Support for Summon CLI
|
|
3
|
+
*
|
|
4
|
+
* Uses omelette to provide TAB completion for:
|
|
5
|
+
* - Generator names (navigating the command tree)
|
|
6
|
+
* - Generator arguments (based on prompt definitions)
|
|
7
|
+
* - File/folder paths for path-related prompts
|
|
8
|
+
*
|
|
9
|
+
* Supports Bash, Zsh, and Fish shells.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import omelette from "omelette";
|
|
15
|
+
import type { GeneratorDefinition, PromptDefinition } from "./types.js";
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/** Represents a node in the generator tree (simplified for completion) */
|
|
22
|
+
export interface CompletionNode {
|
|
23
|
+
name: string;
|
|
24
|
+
indexPath?: string;
|
|
25
|
+
children: Map<string, CompletionNode>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Flattened generator info for completion */
|
|
29
|
+
interface GeneratorInfo {
|
|
30
|
+
path: string[];
|
|
31
|
+
prompts: PromptDefinition[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Path Detection
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Detect if a prompt expects a file/folder path based on its name or message.
|
|
40
|
+
*/
|
|
41
|
+
export const isPathPrompt = (prompt: PromptDefinition): boolean => {
|
|
42
|
+
const pathIndicators = [
|
|
43
|
+
"path",
|
|
44
|
+
"dir",
|
|
45
|
+
"directory",
|
|
46
|
+
"file",
|
|
47
|
+
"folder",
|
|
48
|
+
"location",
|
|
49
|
+
];
|
|
50
|
+
const nameLower = prompt.name.toLowerCase();
|
|
51
|
+
const messageLower = prompt.message.toLowerCase();
|
|
52
|
+
|
|
53
|
+
return pathIndicators.some(
|
|
54
|
+
(ind) => nameLower.includes(ind) || messageLower.includes(ind),
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// Filesystem Completion
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get filesystem path completions for a partial path.
|
|
64
|
+
*/
|
|
65
|
+
export const getPathCompletions = (partial: string): string[] => {
|
|
66
|
+
try {
|
|
67
|
+
// Handle empty or relative paths
|
|
68
|
+
const searchPath = partial || ".";
|
|
69
|
+
const dir = path.dirname(searchPath);
|
|
70
|
+
const prefix = path.basename(searchPath);
|
|
71
|
+
|
|
72
|
+
// Try to read the directory
|
|
73
|
+
const dirToRead = partial.endsWith("/") ? searchPath : dir;
|
|
74
|
+
const entries = fs.readdirSync(dirToRead, { withFileTypes: true });
|
|
75
|
+
|
|
76
|
+
// Filter entries that match the prefix
|
|
77
|
+
const matches = entries
|
|
78
|
+
.filter((entry) => {
|
|
79
|
+
// If partial ends with /, show all entries
|
|
80
|
+
if (partial.endsWith("/")) return true;
|
|
81
|
+
// Otherwise filter by prefix
|
|
82
|
+
return entry.name.startsWith(prefix);
|
|
83
|
+
})
|
|
84
|
+
.map((entry) => {
|
|
85
|
+
const name = entry.name;
|
|
86
|
+
const basePath = partial.endsWith("/") ? searchPath : dir;
|
|
87
|
+
const fullPath = basePath === "." ? name : path.join(basePath, name);
|
|
88
|
+
|
|
89
|
+
// Append / for directories to enable continued completion
|
|
90
|
+
return entry.isDirectory() ? `${fullPath}/` : fullPath;
|
|
91
|
+
})
|
|
92
|
+
.slice(0, 50); // Limit results
|
|
93
|
+
|
|
94
|
+
return matches;
|
|
95
|
+
} catch {
|
|
96
|
+
// Directory doesn't exist or can't be read
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// Argument Completion
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Convert camelCase to kebab-case for CLI flags.
|
|
107
|
+
*/
|
|
108
|
+
const toKebabCase = (str: string): string =>
|
|
109
|
+
str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get completions for generator arguments based on prompt definitions.
|
|
113
|
+
*
|
|
114
|
+
* @param prompts - The prompt definitions for the generator
|
|
115
|
+
* @param line - The current command line
|
|
116
|
+
* @param before - The word before the cursor
|
|
117
|
+
* @param showAll - If true, show all flags even if line doesn't contain --
|
|
118
|
+
*/
|
|
119
|
+
export const getArgumentCompletions = (
|
|
120
|
+
prompts: PromptDefinition[],
|
|
121
|
+
line: string,
|
|
122
|
+
before: string,
|
|
123
|
+
showAll = false,
|
|
124
|
+
): string[] => {
|
|
125
|
+
const completions: string[] = [];
|
|
126
|
+
|
|
127
|
+
// Check if we're completing a flag value (e.g., --type=<TAB> or --type <TAB>)
|
|
128
|
+
const flagValueMatch = line.match(/--([a-z-]+)(?:=|\s+)([^\s]*)$/i);
|
|
129
|
+
if (flagValueMatch) {
|
|
130
|
+
const flagName = flagValueMatch[1];
|
|
131
|
+
const partial = flagValueMatch[2] || "";
|
|
132
|
+
|
|
133
|
+
// Find the corresponding prompt
|
|
134
|
+
const prompt = prompts.find(
|
|
135
|
+
(p) => toKebabCase(p.name) === flagName || p.name === flagName,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (prompt) {
|
|
139
|
+
// For select/multiselect, return choices
|
|
140
|
+
if (
|
|
141
|
+
(prompt.type === "select" || prompt.type === "multiselect") &&
|
|
142
|
+
prompt.choices
|
|
143
|
+
) {
|
|
144
|
+
return prompt.choices
|
|
145
|
+
.map((c) => c.value)
|
|
146
|
+
.filter((v) => v.startsWith(partial));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// For text prompts that look like paths, return path completions
|
|
150
|
+
if (prompt.type === "text" && isPathPrompt(prompt)) {
|
|
151
|
+
return getPathCompletions(partial);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if we're completing a flag name (--<TAB>) or showing all flags
|
|
157
|
+
const flagNameMatch = line.match(/--([a-z-]*)$/i);
|
|
158
|
+
const shouldShowFlags =
|
|
159
|
+
showAll ||
|
|
160
|
+
flagNameMatch ||
|
|
161
|
+
before === "--" ||
|
|
162
|
+
before.startsWith("--") ||
|
|
163
|
+
before.startsWith("-");
|
|
164
|
+
|
|
165
|
+
if (shouldShowFlags) {
|
|
166
|
+
// Extract partial from the flag name match, or from 'before' if it starts with --
|
|
167
|
+
let partial = flagNameMatch?.[1] || "";
|
|
168
|
+
if (!partial && before.startsWith("--")) {
|
|
169
|
+
partial = before.slice(2);
|
|
170
|
+
} else if (!partial && before.startsWith("-")) {
|
|
171
|
+
partial = before.slice(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const prompt of prompts) {
|
|
175
|
+
const kebabName = toKebabCase(prompt.name);
|
|
176
|
+
|
|
177
|
+
if (prompt.type === "confirm") {
|
|
178
|
+
// For confirm prompts, show --no-X if default is true, --X if default is false
|
|
179
|
+
if (prompt.default === true) {
|
|
180
|
+
const flag = `no-${kebabName}`;
|
|
181
|
+
if (flag.startsWith(partial)) {
|
|
182
|
+
completions.push(`--${flag}`);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
if (kebabName.startsWith(partial)) {
|
|
186
|
+
completions.push(`--${kebabName}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
if (kebabName.startsWith(partial)) {
|
|
191
|
+
completions.push(`--${kebabName}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Add built-in flags
|
|
197
|
+
const builtinFlags = [
|
|
198
|
+
"--dry-run",
|
|
199
|
+
"--yes",
|
|
200
|
+
"--verbose",
|
|
201
|
+
"--no-preview",
|
|
202
|
+
"--no-generated-stamp",
|
|
203
|
+
"--help",
|
|
204
|
+
];
|
|
205
|
+
for (const flag of builtinFlags) {
|
|
206
|
+
if (flag.startsWith(`--${partial}`)) {
|
|
207
|
+
completions.push(flag);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return completions;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// =============================================================================
|
|
216
|
+
// Tree Building
|
|
217
|
+
// =============================================================================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Generator loader function type - passed in to avoid circular dependencies.
|
|
221
|
+
*/
|
|
222
|
+
export type GeneratorLoader = (
|
|
223
|
+
indexPath: string,
|
|
224
|
+
) => Promise<GeneratorDefinition>;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get completions for a specific position in the command.
|
|
228
|
+
*/
|
|
229
|
+
const getCompletionsAtPosition = (
|
|
230
|
+
node: CompletionNode,
|
|
231
|
+
args: string[],
|
|
232
|
+
generators: Map<string, GeneratorInfo>,
|
|
233
|
+
): string[] => {
|
|
234
|
+
// Navigate to the correct position in the tree
|
|
235
|
+
let current = node;
|
|
236
|
+
const pathSoFar: string[] = [];
|
|
237
|
+
|
|
238
|
+
for (const arg of args) {
|
|
239
|
+
// Skip flags and empty args
|
|
240
|
+
if (arg.startsWith("-") || !arg) continue;
|
|
241
|
+
|
|
242
|
+
const child = current.children.get(arg);
|
|
243
|
+
if (child) {
|
|
244
|
+
current = child;
|
|
245
|
+
pathSoFar.push(arg);
|
|
246
|
+
} else {
|
|
247
|
+
// Partial match - don't add to path, keep current node
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Check if we have subcommands available
|
|
253
|
+
const subcommands = [...current.children.keys()];
|
|
254
|
+
|
|
255
|
+
// Check if we're at a generator
|
|
256
|
+
const generatorKey = pathSoFar.join("/");
|
|
257
|
+
const generatorInfo = generators.get(generatorKey);
|
|
258
|
+
|
|
259
|
+
// Get the last arg for context
|
|
260
|
+
const lastArg = args[args.length - 1] || "";
|
|
261
|
+
const line = args.join(" ");
|
|
262
|
+
|
|
263
|
+
// If we're at a generator
|
|
264
|
+
if (generatorInfo) {
|
|
265
|
+
// If the last arg starts with -, provide flag completions
|
|
266
|
+
if (lastArg.startsWith("-")) {
|
|
267
|
+
return getArgumentCompletions(generatorInfo.prompts, line, lastArg);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check if this generator has a positional prompt
|
|
271
|
+
const positionalPrompt = generatorInfo.prompts.find((p) => p.positional);
|
|
272
|
+
|
|
273
|
+
// If there are no subcommands and this is a leaf generator
|
|
274
|
+
if (subcommands.length === 0) {
|
|
275
|
+
// Count non-flag args after the generator path
|
|
276
|
+
const nonFlagArgs = args.filter((a) => !a.startsWith("-"));
|
|
277
|
+
const argsAfterGenerator = nonFlagArgs.length - pathSoFar.length;
|
|
278
|
+
|
|
279
|
+
// If we have a positional prompt and haven't provided the positional arg yet
|
|
280
|
+
if (positionalPrompt && argsAfterGenerator === 0) {
|
|
281
|
+
// If the positional is a path, provide path completions
|
|
282
|
+
if (isPathPrompt(positionalPrompt)) {
|
|
283
|
+
return getPathCompletions(lastArg || "");
|
|
284
|
+
}
|
|
285
|
+
// Otherwise show flags (they can type the positional or use flags)
|
|
286
|
+
return getArgumentCompletions(generatorInfo.prompts, line, "", true);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// If positional already provided or no positional, show flags
|
|
290
|
+
return getArgumentCompletions(generatorInfo.prompts, line, "", true);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// If there are both subcommands and this is a generator,
|
|
294
|
+
// show subcommands (user might want to go deeper)
|
|
295
|
+
return subcommands;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Not at a generator, show subcommands
|
|
299
|
+
// Only filter if the last arg is a partial (not in pathSoFar)
|
|
300
|
+
const lastPathSeg = pathSoFar[pathSoFar.length - 1];
|
|
301
|
+
const isPartialArg =
|
|
302
|
+
lastArg && !lastArg.startsWith("-") && lastArg !== lastPathSeg;
|
|
303
|
+
|
|
304
|
+
if (isPartialArg) {
|
|
305
|
+
return subcommands.filter((cmd) => cmd.startsWith(lastArg));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return subcommands;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Load all generators and cache their prompts.
|
|
313
|
+
*/
|
|
314
|
+
const loadAllGenerators = async (
|
|
315
|
+
node: CompletionNode,
|
|
316
|
+
loadGenerator: GeneratorLoader,
|
|
317
|
+
currentPath: string[] = [],
|
|
318
|
+
cache: Map<string, GeneratorInfo> = new Map(),
|
|
319
|
+
): Promise<Map<string, GeneratorInfo>> => {
|
|
320
|
+
for (const [name, child] of node.children) {
|
|
321
|
+
const childPath = [...currentPath, name];
|
|
322
|
+
const pathKey = childPath.join("/");
|
|
323
|
+
|
|
324
|
+
if (child.indexPath) {
|
|
325
|
+
try {
|
|
326
|
+
const generator = await loadGenerator(child.indexPath);
|
|
327
|
+
cache.set(pathKey, {
|
|
328
|
+
path: childPath,
|
|
329
|
+
prompts: generator.prompts,
|
|
330
|
+
});
|
|
331
|
+
} catch {
|
|
332
|
+
// Failed to load generator, skip
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (child.children.size > 0) {
|
|
337
|
+
await loadAllGenerators(child, loadGenerator, childPath, cache);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return cache;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// =============================================================================
|
|
345
|
+
// Completion Initialization
|
|
346
|
+
// =============================================================================
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Initialize shell completion for the summon CLI.
|
|
350
|
+
*
|
|
351
|
+
* This should be called early in the CLI lifecycle, before commander parsing,
|
|
352
|
+
* because omelette needs to intercept completion requests.
|
|
353
|
+
*
|
|
354
|
+
* @param generatorTree - The root of the generator tree
|
|
355
|
+
* @param loadGenerator - Function to load a generator from its path
|
|
356
|
+
* @returns The omelette instance (for setup/cleanup methods)
|
|
357
|
+
*/
|
|
358
|
+
export const initCompletion = async (
|
|
359
|
+
generatorTree: CompletionNode,
|
|
360
|
+
loadGenerator: GeneratorLoader,
|
|
361
|
+
): Promise<omelette.Instance> => {
|
|
362
|
+
// Load all generators to get their prompts
|
|
363
|
+
const generators = await loadAllGenerators(generatorTree, loadGenerator);
|
|
364
|
+
|
|
365
|
+
// Create the completion instance
|
|
366
|
+
// We use a template with multiple segments to handle deep hierarchies
|
|
367
|
+
// The "complete" event fires for all positions
|
|
368
|
+
const complete = omelette("summon <arg1> <arg2> <arg3> <arg4> <arg5>");
|
|
369
|
+
|
|
370
|
+
// Handle completion dynamically for all positions
|
|
371
|
+
// The "complete" event receives the fragment name (arg1, arg2, etc.) and context
|
|
372
|
+
complete.on("complete", (fragment, { line, reply, before }) => {
|
|
373
|
+
// Parse the current line to determine what to complete
|
|
374
|
+
// Remove 'summon' and filter out internal flags like --generators
|
|
375
|
+
let args = line.split(/\s+/).filter(Boolean).slice(1);
|
|
376
|
+
|
|
377
|
+
// Filter out internal CLI flags that shouldn't be part of completion context
|
|
378
|
+
const internalFlags = [
|
|
379
|
+
"--generators",
|
|
380
|
+
"-g",
|
|
381
|
+
"--compbash",
|
|
382
|
+
"--compzsh",
|
|
383
|
+
"--compfish",
|
|
384
|
+
"--compgen",
|
|
385
|
+
];
|
|
386
|
+
args = args.filter((arg, idx) => {
|
|
387
|
+
// Skip internal flags and their values
|
|
388
|
+
if (internalFlags.includes(arg)) return false;
|
|
389
|
+
// Skip the value after an internal flag
|
|
390
|
+
if (idx > 0 && internalFlags.includes(args[idx - 1])) return false;
|
|
391
|
+
return true;
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Debug logging (only when SUMMON_DEBUG is set)
|
|
395
|
+
if (process.env.SUMMON_DEBUG) {
|
|
396
|
+
console.error("[completion] fragment:", fragment);
|
|
397
|
+
console.error("[completion] line:", line);
|
|
398
|
+
console.error("[completion] before:", before);
|
|
399
|
+
console.error("[completion] args (filtered):", args);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// If before is empty, we're at the start of a new argument
|
|
403
|
+
// If not, we're completing a partial argument
|
|
404
|
+
const completions = getCompletionsAtPosition(
|
|
405
|
+
generatorTree,
|
|
406
|
+
args,
|
|
407
|
+
generators,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
if (process.env.SUMMON_DEBUG) {
|
|
411
|
+
console.error("[completion] completions:", completions);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
reply(completions);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Initialize omelette (this intercepts --completion args)
|
|
418
|
+
complete.init();
|
|
419
|
+
|
|
420
|
+
return complete;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Check if the current invocation is a completion request.
|
|
425
|
+
* This allows us to skip normal CLI processing for completion.
|
|
426
|
+
*/
|
|
427
|
+
export const isCompletionRequest = (): boolean => {
|
|
428
|
+
const args = process.argv;
|
|
429
|
+
return (
|
|
430
|
+
args.includes("--completion") ||
|
|
431
|
+
args.includes("--completion-fish") ||
|
|
432
|
+
args.includes("--compzsh") ||
|
|
433
|
+
args.includes("--compbash") ||
|
|
434
|
+
args.includes("--compfish") ||
|
|
435
|
+
// Omelette internal completion trigger
|
|
436
|
+
args.some((arg) => arg.startsWith("--compgen"))
|
|
437
|
+
);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Check if this is a setup or cleanup request.
|
|
442
|
+
*/
|
|
443
|
+
export const isSetupRequest = (): boolean => {
|
|
444
|
+
const args = process.argv;
|
|
445
|
+
return (
|
|
446
|
+
args.includes("--setup-completion") || args.includes("--cleanup-completion")
|
|
447
|
+
);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Handle setup/cleanup requests.
|
|
452
|
+
*/
|
|
453
|
+
export const handleSetupRequest = (complete: omelette.Instance): void => {
|
|
454
|
+
const args = process.argv;
|
|
455
|
+
|
|
456
|
+
if (args.includes("--setup-completion")) {
|
|
457
|
+
try {
|
|
458
|
+
complete.setupShellInitFile();
|
|
459
|
+
console.log("Shell completion installed successfully!");
|
|
460
|
+
console.log(
|
|
461
|
+
"Please restart your shell or run: source ~/.zshrc (or ~/.bashrc)",
|
|
462
|
+
);
|
|
463
|
+
} catch (err) {
|
|
464
|
+
console.error("Failed to install completion:", (err as Error).message);
|
|
465
|
+
console.error("\nManual installation:");
|
|
466
|
+
console.error(" Zsh: echo '. <(summon --completion)' >> ~/.zshrc");
|
|
467
|
+
console.error(
|
|
468
|
+
" Bash: summon --completion >> ~/.summon-completion.sh && echo 'source ~/.summon-completion.sh' >> ~/.bash_profile",
|
|
469
|
+
);
|
|
470
|
+
console.error(
|
|
471
|
+
" Fish: echo 'summon --completion-fish | source' >> ~/.config/fish/config.fish",
|
|
472
|
+
);
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
process.exit(0);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (args.includes("--cleanup-completion")) {
|
|
479
|
+
try {
|
|
480
|
+
complete.cleanupShellInitFile();
|
|
481
|
+
console.log("Shell completion removed successfully!");
|
|
482
|
+
} catch (err) {
|
|
483
|
+
console.error("Failed to remove completion:", (err as Error).message);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
process.exit(0);
|
|
487
|
+
}
|
|
488
|
+
};
|