Skip to content

Commit 5e2425d

Browse files
committed
cli completions
1 parent 06b8e31 commit 5e2425d

File tree

4 files changed

+215
-69
lines changed

4 files changed

+215
-69
lines changed

bin/cli.ts

Lines changed: 68 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -12,84 +12,84 @@ const shells = ['zsh', 'bash', 'fish', 'powershell'];
1212
const cli = cac('tab');
1313

1414
cli
15-
.command(
16-
'<packageManager> complete',
17-
'Process completion requests from shell'
18-
)
19-
.action(async (packageManager) => {
20-
if (!packageManagers.includes(packageManager)) {
21-
console.error(`Error: Unsupported package manager "${packageManager}"`);
22-
console.error(
23-
`Supported package managers: ${packageManagers.join(', ')}`
24-
);
25-
process.exit(1);
26-
}
15+
.command(
16+
'<packageManager> complete',
17+
'Process completion requests from shell'
18+
)
19+
.action(async (packageManager) => {
20+
if (!packageManagers.includes(packageManager)) {
21+
console.error(`Error: Unsupported package manager "${packageManager}"`);
22+
console.error(
23+
`Supported package managers: ${packageManagers.join(', ')}`
24+
);
25+
process.exit(1);
26+
}
2727

28-
const dashIndex = process.argv.indexOf('--');
29-
if (dashIndex !== -1) {
30-
const completion = new Completion();
31-
setupCompletionForPackageManager(packageManager, completion);
32-
const toComplete = process.argv.slice(dashIndex + 1);
33-
await completion.parse(toComplete);
34-
process.exit(0);
35-
} else {
36-
console.error(`Error: Expected '--' followed by command to complete`);
37-
console.error(
38-
`Example: ${packageManager} exec @bombsh/tab ${packageManager} complete -- command-to-complete`
39-
);
40-
process.exit(1);
41-
}
42-
});
28+
const dashIndex = process.argv.indexOf('--');
29+
if (dashIndex !== -1) {
30+
const completion = new Completion();
31+
setupCompletionForPackageManager(packageManager, completion);
32+
const toComplete = process.argv.slice(dashIndex + 1);
33+
await completion.parse(toComplete);
34+
process.exit(0);
35+
} else {
36+
console.error(`Error: Expected '--' followed by command to complete`);
37+
console.error(
38+
`Example: ${packageManager} exec @bombsh/tab ${packageManager} complete -- command-to-complete`
39+
);
40+
process.exit(1);
41+
}
42+
});
4343

4444
cli
45-
.command(
46-
'<packageManager> <shell>',
47-
'Generate shell completion script for a package manager'
48-
)
49-
.action(async (packageManager, shell) => {
50-
if (shell === 'complete') {
51-
const dashIndex = process.argv.indexOf('--');
52-
if (dashIndex !== -1) {
53-
const completion = new Completion();
54-
setupCompletionForPackageManager(packageManager, completion);
55-
const toComplete = process.argv.slice(dashIndex + 1);
56-
await completion.parse(toComplete);
57-
process.exit(0);
58-
} else {
59-
console.error(`Error: Expected '--' followed by command to complete`);
60-
console.error(
61-
`Example: ${packageManager} exec @bombsh/tab ${packageManager} complete -- command-to-complete`
62-
);
63-
process.exit(1);
64-
}
65-
return;
66-
}
45+
.command(
46+
'<packageManager> <shell>',
47+
'Generate shell completion script for a package manager'
48+
)
49+
.action(async (packageManager, shell) => {
50+
if (shell === 'complete') {
51+
const dashIndex = process.argv.indexOf('--');
52+
if (dashIndex !== -1) {
53+
const completion = new Completion();
54+
setupCompletionForPackageManager(packageManager, completion);
55+
const toComplete = process.argv.slice(dashIndex + 1);
56+
await completion.parse(toComplete);
57+
process.exit(0);
58+
} else {
59+
console.error(`Error: Expected '--' followed by command to complete`);
60+
console.error(
61+
`Example: ${packageManager} exec @bombsh/tab ${packageManager} complete -- command-to-complete`
62+
);
63+
process.exit(1);
64+
}
65+
return;
66+
}
6767

68-
if (!packageManagers.includes(packageManager)) {
69-
console.error(`Error: Unsupported package manager "${packageManager}"`);
70-
console.error(
71-
`Supported package managers: ${packageManagers.join(', ')}`
72-
);
73-
process.exit(1);
74-
}
68+
if (!packageManagers.includes(packageManager)) {
69+
console.error(`Error: Unsupported package manager "${packageManager}"`);
70+
console.error(
71+
`Supported package managers: ${packageManagers.join(', ')}`
72+
);
73+
process.exit(1);
74+
}
7575

76-
if (!shells.includes(shell)) {
77-
console.error(`Error: Unsupported shell "${shell}"`);
78-
console.error(`Supported shells: ${shells.join(', ')}`);
79-
process.exit(1);
80-
}
76+
if (!shells.includes(shell)) {
77+
console.error(`Error: Unsupported shell "${shell}"`);
78+
console.error(`Supported shells: ${shells.join(', ')}`);
79+
process.exit(1);
80+
}
8181

82-
generateCompletionScript(packageManager, shell);
83-
});
82+
generateCompletionScript(packageManager, shell);
83+
});
8484

8585
const completion = tab(cli);
8686

8787
cli.parse();
8888

8989
function generateCompletionScript(packageManager: string, shell: string) {
90-
const name = packageManager;
91-
const executable = process.env.npm_execpath
92-
? `${packageManager} exec @bombsh/tab ${packageManager}`
93-
: `node ${process.argv[1]} ${packageManager}`;
94-
script(shell as any, name, executable);
90+
const name = packageManager;
91+
const executable = process.env.npm_execpath
92+
? `${packageManager} exec @bombsh/tab ${packageManager}`
93+
: `node ${process.argv[1]} ${packageManager}`;
94+
script(shell as any, name, executable);
9595
}

bin/completion-handlers.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,61 @@
11
import { Completion } from '../src/index.js';
2+
import { execSync } from 'child_process';
3+
4+
const DEBUG = false; // for debugging purposes
5+
6+
function debugLog(...args: any[]) {
7+
if (DEBUG) {
8+
console.error('[DEBUG]', ...args);
9+
}
10+
}
11+
12+
async function checkCliHasCompletions(
13+
cliName: string,
14+
packageManager: string
15+
): Promise<boolean> {
16+
try {
17+
debugLog(`Checking if ${cliName} has completions via ${packageManager}`);
18+
const command = `${packageManager} ${cliName} __complete`;
19+
const result = execSync(command, {
20+
encoding: 'utf8',
21+
stdio: ['pipe', 'pipe', 'ignore'],
22+
timeout: 1000, // AMIR: we still havin issues with this, it still hangs if a cli doesn't have completions.
23+
});
24+
const hasCompletions = !!result.trim();
25+
debugLog(`${cliName} supports completions: ${hasCompletions}`);
26+
return hasCompletions;
27+
} catch (error) {
28+
debugLog(`Error checking completions for ${cliName}:`, error);
29+
return false;
30+
}
31+
}
32+
33+
async function getCliCompletions(
34+
cliName: string,
35+
packageManager: string,
36+
args: string[]
37+
): Promise<string[]> {
38+
try {
39+
const completeArgs = args.map((arg) =>
40+
arg.includes(' ') ? `"${arg}"` : arg
41+
);
42+
const completeCommand = `${packageManager} ${cliName} __complete ${completeArgs.join(' ')}`;
43+
debugLog(`Getting completions with command: ${completeCommand}`);
44+
45+
const result = execSync(completeCommand, {
46+
encoding: 'utf8',
47+
stdio: ['pipe', 'pipe', 'ignore'],
48+
timeout: 1000,
49+
});
50+
51+
const completions = result.trim().split('\n').filter(Boolean);
52+
debugLog(`Got ${completions.length} completions from ${cliName}`);
53+
return completions;
54+
} catch (error) {
55+
debugLog(`Error getting completions from ${cliName}:`, error);
56+
return [];
57+
}
58+
}
259

360
export function setupCompletionForPackageManager(
461
packageManager: string,
@@ -13,6 +70,70 @@ export function setupCompletionForPackageManager(
1370
} else if (packageManager === 'bun') {
1471
setupBunCompletions(completion);
1572
}
73+
74+
completion.onBeforeParse(async (args: string[]) => {
75+
debugLog(`onBeforeParse: args =`, args);
76+
77+
if (args.length >= 1) {
78+
const potentialCliName = args[0];
79+
const knownCommands = [...completion.commands.keys()];
80+
81+
debugLog(
82+
`Potential CLI: ${potentialCliName}, Known commands:`,
83+
knownCommands
84+
);
85+
86+
if (knownCommands.includes(potentialCliName)) {
87+
debugLog(`${potentialCliName} is a known command, skipping CLI check`);
88+
return;
89+
}
90+
91+
const hasCompletions = await checkCliHasCompletions(
92+
potentialCliName,
93+
packageManager
94+
);
95+
if (hasCompletions) {
96+
debugLog(
97+
`${potentialCliName} supports completions, getting suggestions`
98+
);
99+
100+
const cliArgs = args.slice(1);
101+
const suggestions = await getCliCompletions(
102+
potentialCliName,
103+
packageManager,
104+
cliArgs
105+
);
106+
107+
if (suggestions.length > 0) {
108+
debugLog(`Processing ${suggestions.length} suggestions`);
109+
110+
completion.result.suppressDefault = true;
111+
112+
for (const suggestion of suggestions) {
113+
if (suggestion.startsWith(':')) {
114+
debugLog(`Skipping directive: ${suggestion}`);
115+
continue;
116+
}
117+
118+
if (suggestion.includes('\t')) {
119+
const [value, description] = suggestion.split('\t');
120+
debugLog(
121+
`Adding completion with description: ${value} -> ${description}`
122+
);
123+
completion.result.items.push({ value, description });
124+
} else {
125+
debugLog(`Adding completion without description: ${suggestion}`);
126+
completion.result.items.push({ value: suggestion });
127+
}
128+
}
129+
} else {
130+
debugLog(`No suggestions found for ${potentialCliName}`);
131+
}
132+
} else {
133+
debugLog(`${potentialCliName} does not support completions`);
134+
}
135+
}
136+
});
16137
}
17138

18139
export function setupPnpmCompletions(completion: Completion) {

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,15 @@ export type Positional = {
6262
};
6363

6464
type Item = {
65-
description: string;
65+
description?: string;
6666
value: string;
6767
};
6868

69+
type CompletionResult = {
70+
items: Item[];
71+
suppressDefault: boolean;
72+
};
73+
6974
export type Handler = (
7075
previousArgs: string[],
7176
toComplete: string,
@@ -91,6 +96,12 @@ export class Completion {
9196
commands = new Map<string, Command>();
9297
completions: Item[] = [];
9398
directive = ShellCompDirective.ShellCompDirectiveDefault;
99+
result: CompletionResult = { items: [], suppressDefault: false };
100+
private beforeParseFn: ((args: string[]) => Promise<void>) | null = null;
101+
102+
onBeforeParse(fn: (args: string[]) => Promise<void>) {
103+
this.beforeParseFn = fn;
104+
}
94105

95106
// vite <entry> <another> [...files]
96107
// args: [false, false, true], only the last argument can be variadic
@@ -171,6 +182,17 @@ export class Completion {
171182
}
172183

173184
async parse(args: string[]) {
185+
this.result = { items: [], suppressDefault: false };
186+
187+
if (this.beforeParseFn) {
188+
await this.beforeParseFn(args);
189+
if (this.result.suppressDefault && this.result.items.length > 0) {
190+
this.completions = this.result.items;
191+
this.complete('');
192+
return;
193+
}
194+
}
195+
174196
const endsWithSpace = args[args.length - 1] === '';
175197

176198
if (endsWithSpace) {

0 commit comments

Comments
 (0)