@jackwener/opencli 0.7.5 → 0.7.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.
- package/dist/completion.d.ts +21 -0
- package/dist/completion.js +116 -0
- package/dist/main.js +29 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +179 -0
- package/src/completion.ts +129 -0
- package/src/main.ts +29 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell tab-completion support for opencli.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Shell script generators for bash, zsh, and fish
|
|
6
|
+
* - Dynamic completion logic that returns candidates for the current cursor position
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Return completion candidates given the current command-line words and cursor index.
|
|
10
|
+
*
|
|
11
|
+
* @param words - The argv after 'opencli' (words[0] is the first arg, e.g. site name)
|
|
12
|
+
* @param cursor - 1-based position of the word being completed (1 = first arg)
|
|
13
|
+
*/
|
|
14
|
+
export declare function getCompletions(words: string[], cursor: number): string[];
|
|
15
|
+
export declare function bashCompletionScript(): string;
|
|
16
|
+
export declare function zshCompletionScript(): string;
|
|
17
|
+
export declare function fishCompletionScript(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Print the completion script for the requested shell.
|
|
20
|
+
*/
|
|
21
|
+
export declare function printCompletionScript(shell: string): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell tab-completion support for opencli.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Shell script generators for bash, zsh, and fish
|
|
6
|
+
* - Dynamic completion logic that returns candidates for the current cursor position
|
|
7
|
+
*/
|
|
8
|
+
import { getRegistry } from './registry.js';
|
|
9
|
+
// ── Dynamic completion logic ───────────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* Built-in (non-dynamic) top-level commands.
|
|
12
|
+
*/
|
|
13
|
+
const BUILTIN_COMMANDS = [
|
|
14
|
+
'list',
|
|
15
|
+
'validate',
|
|
16
|
+
'verify',
|
|
17
|
+
'explore',
|
|
18
|
+
'probe', // alias for explore
|
|
19
|
+
'synthesize',
|
|
20
|
+
'generate',
|
|
21
|
+
'cascade',
|
|
22
|
+
'doctor',
|
|
23
|
+
'setup',
|
|
24
|
+
'completion',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Return completion candidates given the current command-line words and cursor index.
|
|
28
|
+
*
|
|
29
|
+
* @param words - The argv after 'opencli' (words[0] is the first arg, e.g. site name)
|
|
30
|
+
* @param cursor - 1-based position of the word being completed (1 = first arg)
|
|
31
|
+
*/
|
|
32
|
+
export function getCompletions(words, cursor) {
|
|
33
|
+
// cursor === 1 → completing the first argument (site name or built-in command)
|
|
34
|
+
if (cursor <= 1) {
|
|
35
|
+
const sites = new Set();
|
|
36
|
+
for (const [, cmd] of getRegistry()) {
|
|
37
|
+
sites.add(cmd.site);
|
|
38
|
+
}
|
|
39
|
+
return [...BUILTIN_COMMANDS, ...sites].sort();
|
|
40
|
+
}
|
|
41
|
+
const site = words[0];
|
|
42
|
+
// If the first word is a built-in command, no further completion
|
|
43
|
+
if (BUILTIN_COMMANDS.includes(site)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
// cursor === 2 → completing the sub-command name under a site
|
|
47
|
+
if (cursor === 2) {
|
|
48
|
+
const subcommands = [];
|
|
49
|
+
for (const [, cmd] of getRegistry()) {
|
|
50
|
+
if (cmd.site === site) {
|
|
51
|
+
subcommands.push(cmd.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return subcommands.sort();
|
|
55
|
+
}
|
|
56
|
+
// cursor >= 3 → no further completion
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
// ── Shell script generators ────────────────────────────────────────────────
|
|
60
|
+
export function bashCompletionScript() {
|
|
61
|
+
return `# Bash completion for opencli
|
|
62
|
+
# Add to ~/.bashrc: eval "$(opencli completion bash)"
|
|
63
|
+
_opencli_completions() {
|
|
64
|
+
local cur words cword
|
|
65
|
+
_get_comp_words_by_ref -n : cur words cword
|
|
66
|
+
|
|
67
|
+
local completions
|
|
68
|
+
completions=$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)
|
|
69
|
+
|
|
70
|
+
COMPREPLY=( $(compgen -W "$completions" -- "$cur") )
|
|
71
|
+
__ltrim_colon_completions "$cur"
|
|
72
|
+
}
|
|
73
|
+
complete -F _opencli_completions opencli
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
export function zshCompletionScript() {
|
|
77
|
+
return `# Zsh completion for opencli
|
|
78
|
+
# Add to ~/.zshrc: eval "$(opencli completion zsh)"
|
|
79
|
+
_opencli() {
|
|
80
|
+
local -a completions
|
|
81
|
+
local cword=$((CURRENT - 1))
|
|
82
|
+
completions=(\${(f)"$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)"})
|
|
83
|
+
compadd -a completions
|
|
84
|
+
}
|
|
85
|
+
compdef _opencli opencli
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
export function fishCompletionScript() {
|
|
89
|
+
return `# Fish completion for opencli
|
|
90
|
+
# Add to ~/.config/fish/config.fish: opencli completion fish | source
|
|
91
|
+
complete -c opencli -f -a '(
|
|
92
|
+
set -l tokens (commandline -cop)
|
|
93
|
+
set -l cursor (count (commandline -cop))
|
|
94
|
+
opencli --get-completions --cursor $cursor $tokens[2..] 2>/dev/null
|
|
95
|
+
)'
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Print the completion script for the requested shell.
|
|
100
|
+
*/
|
|
101
|
+
export function printCompletionScript(shell) {
|
|
102
|
+
switch (shell) {
|
|
103
|
+
case 'bash':
|
|
104
|
+
process.stdout.write(bashCompletionScript());
|
|
105
|
+
break;
|
|
106
|
+
case 'zsh':
|
|
107
|
+
process.stdout.write(zshCompletionScript());
|
|
108
|
+
break;
|
|
109
|
+
case 'fish':
|
|
110
|
+
process.stdout.write(fishCompletionScript());
|
|
111
|
+
break;
|
|
112
|
+
default:
|
|
113
|
+
console.error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -13,11 +13,34 @@ import { render as renderOutput } from './output.js';
|
|
|
13
13
|
import { PlaywrightMCP } from './browser.js';
|
|
14
14
|
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
|
15
15
|
import { PKG_VERSION } from './version.js';
|
|
16
|
+
import { getCompletions, printCompletionScript } from './completion.js';
|
|
16
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
18
|
const __dirname = path.dirname(__filename);
|
|
18
19
|
const BUILTIN_CLIS = path.resolve(__dirname, 'clis');
|
|
19
20
|
const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
|
|
20
21
|
await discoverClis(BUILTIN_CLIS, USER_CLIS);
|
|
22
|
+
// ── Fast-path: handle --get-completions before commander parses ─────────
|
|
23
|
+
// Usage: opencli --get-completions --cursor <N> [word1 word2 ...]
|
|
24
|
+
const getCompIdx = process.argv.indexOf('--get-completions');
|
|
25
|
+
if (getCompIdx !== -1) {
|
|
26
|
+
const rest = process.argv.slice(getCompIdx + 1);
|
|
27
|
+
let cursor;
|
|
28
|
+
const words = [];
|
|
29
|
+
for (let i = 0; i < rest.length; i++) {
|
|
30
|
+
if (rest[i] === '--cursor' && i + 1 < rest.length) {
|
|
31
|
+
cursor = parseInt(rest[i + 1], 10);
|
|
32
|
+
i++; // skip the value
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
words.push(rest[i]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (cursor === undefined)
|
|
39
|
+
cursor = words.length;
|
|
40
|
+
const candidates = getCompletions(words, cursor);
|
|
41
|
+
process.stdout.write(candidates.join('\n') + '\n');
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
21
44
|
const program = new Command();
|
|
22
45
|
program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION);
|
|
23
46
|
// ── Built-in commands ──────────────────────────────────────────────────────
|
|
@@ -130,6 +153,12 @@ program.command('setup')
|
|
|
130
153
|
const { runSetup } = await import('./setup.js');
|
|
131
154
|
await runSetup({ cliVersion: PKG_VERSION, token: opts.token });
|
|
132
155
|
});
|
|
156
|
+
program.command('completion')
|
|
157
|
+
.description('Output shell completion script')
|
|
158
|
+
.argument('<shell>', 'Shell type: bash, zsh, or fish')
|
|
159
|
+
.action((shell) => {
|
|
160
|
+
printCompletionScript(shell);
|
|
161
|
+
});
|
|
133
162
|
// ── Dynamic site commands ──────────────────────────────────────────────────
|
|
134
163
|
const registry = getRegistry();
|
|
135
164
|
const siteGroups = new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackwener/opencli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"clean-yaml": "node -e \"const{readdirSync:r,rmSync:d,existsSync:e,statSync:s}=require('fs'),p=require('path');function w(dir){if(!e(dir))return;for(const f of r(dir)){const fp=p.join(dir,f);s(fp).isDirectory()?w(fp):/\\.ya?ml$/.test(f)&&d(fp)}}w('dist/clis')\"",
|
|
21
21
|
"copy-yaml": "node -e \"const{readdirSync:r,copyFileSync:c,mkdirSync:m,existsSync:e,statSync:s}=require('fs'),p=require('path');function w(src,dst){if(!e(src))return;for(const f of r(src)){const sp=p.join(src,f),dp=p.join(dst,f);s(sp).isDirectory()?w(sp,dp):/\\.ya?ml$/.test(f)&&(m(p.dirname(dp),{recursive:!0}),c(sp,dp))}}w('src/clis','dist/clis')\"",
|
|
22
22
|
"start": "node dist/main.js",
|
|
23
|
+
"postinstall": "node scripts/postinstall.js || true",
|
|
23
24
|
"typecheck": "tsc --noEmit",
|
|
24
25
|
"lint": "tsc --noEmit",
|
|
25
26
|
"prepublishOnly": "npm run build",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* postinstall script — automatically install shell completion files.
|
|
5
|
+
*
|
|
6
|
+
* Detects the user's default shell and writes the completion script to the
|
|
7
|
+
* standard system completion directory so that tab-completion works immediately
|
|
8
|
+
* after `npm install -g`.
|
|
9
|
+
*
|
|
10
|
+
* Supported shells: bash, zsh, fish.
|
|
11
|
+
*
|
|
12
|
+
* This script is intentionally plain Node.js (no TypeScript, no imports from
|
|
13
|
+
* the main source tree) so that it can run without a build step.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, appendFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
// ── Completion script content ──────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const BASH_COMPLETION = `# Bash completion for opencli (auto-installed)
|
|
24
|
+
_opencli_completions() {
|
|
25
|
+
local cur words cword
|
|
26
|
+
_get_comp_words_by_ref -n : cur words cword
|
|
27
|
+
|
|
28
|
+
local completions
|
|
29
|
+
completions=$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)
|
|
30
|
+
|
|
31
|
+
COMPREPLY=( $(compgen -W "$completions" -- "$cur") )
|
|
32
|
+
__ltrim_colon_completions "$cur"
|
|
33
|
+
}
|
|
34
|
+
complete -F _opencli_completions opencli
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const ZSH_COMPLETION = `#compdef opencli
|
|
38
|
+
# Zsh completion for opencli (auto-installed)
|
|
39
|
+
_opencli() {
|
|
40
|
+
local -a completions
|
|
41
|
+
local cword=$((CURRENT - 1))
|
|
42
|
+
completions=(\${(f)"$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)"})
|
|
43
|
+
compadd -a completions
|
|
44
|
+
}
|
|
45
|
+
_opencli
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
const FISH_COMPLETION = `# Fish completion for opencli (auto-installed)
|
|
49
|
+
complete -c opencli -f -a '(
|
|
50
|
+
set -l tokens (commandline -cop)
|
|
51
|
+
set -l cursor (count (commandline -cop))
|
|
52
|
+
opencli --get-completions --cursor $cursor $tokens[2..] 2>/dev/null
|
|
53
|
+
)'
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function detectShell() {
|
|
59
|
+
const shell = process.env.SHELL || '';
|
|
60
|
+
if (shell.includes('zsh')) return 'zsh';
|
|
61
|
+
if (shell.includes('bash')) return 'bash';
|
|
62
|
+
if (shell.includes('fish')) return 'fish';
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ensureDir(dir) {
|
|
67
|
+
if (!existsSync(dir)) {
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Ensure fpath contains the custom completions directory in .zshrc
|
|
74
|
+
*/
|
|
75
|
+
function ensureZshFpath(completionsDir, zshrcPath) {
|
|
76
|
+
const fpathLine = `fpath=(${completionsDir} $fpath)`;
|
|
77
|
+
const autoloadLine = `autoload -Uz compinit && compinit`;
|
|
78
|
+
|
|
79
|
+
if (!existsSync(zshrcPath)) {
|
|
80
|
+
writeFileSync(zshrcPath, `${fpathLine}\n${autoloadLine}\n`, 'utf8');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const content = readFileSync(zshrcPath, 'utf8');
|
|
85
|
+
|
|
86
|
+
// Check if completions dir is already in fpath
|
|
87
|
+
if (content.includes(completionsDir)) {
|
|
88
|
+
return; // already configured
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Append fpath configuration
|
|
92
|
+
let addition = `\n# opencli completion\n${fpathLine}\n`;
|
|
93
|
+
if (!content.includes('compinit')) {
|
|
94
|
+
addition += `${autoloadLine}\n`;
|
|
95
|
+
}
|
|
96
|
+
appendFileSync(zshrcPath, addition, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
function main() {
|
|
102
|
+
// Skip in CI environments
|
|
103
|
+
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Only install completion for global installs and npm link
|
|
108
|
+
const isGlobal = process.env.npm_config_global === 'true';
|
|
109
|
+
if (!isGlobal) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const shell = detectShell();
|
|
114
|
+
if (!shell) {
|
|
115
|
+
// Cannot determine shell; silently skip
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const home = homedir();
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
switch (shell) {
|
|
123
|
+
case 'zsh': {
|
|
124
|
+
const completionsDir = join(home, '.zsh', 'completions');
|
|
125
|
+
const completionFile = join(completionsDir, '_opencli');
|
|
126
|
+
ensureDir(completionsDir);
|
|
127
|
+
writeFileSync(completionFile, ZSH_COMPLETION, 'utf8');
|
|
128
|
+
|
|
129
|
+
// Ensure fpath is set up in .zshrc
|
|
130
|
+
const zshrcPath = join(home, '.zshrc');
|
|
131
|
+
ensureZshFpath(completionsDir, zshrcPath);
|
|
132
|
+
|
|
133
|
+
console.log(`✓ Zsh completion installed to ${completionFile}`);
|
|
134
|
+
console.log(` Restart your shell or run: source ~/.zshrc`);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'bash': {
|
|
138
|
+
// Try system-level first, fall back to user-level
|
|
139
|
+
const userCompDir = join(home, '.bash_completion.d');
|
|
140
|
+
const completionFile = join(userCompDir, 'opencli');
|
|
141
|
+
ensureDir(userCompDir);
|
|
142
|
+
writeFileSync(completionFile, BASH_COMPLETION, 'utf8');
|
|
143
|
+
|
|
144
|
+
// Ensure .bashrc sources the completion directory
|
|
145
|
+
const bashrcPath = join(home, '.bashrc');
|
|
146
|
+
if (existsSync(bashrcPath)) {
|
|
147
|
+
const content = readFileSync(bashrcPath, 'utf8');
|
|
148
|
+
if (!content.includes('.bash_completion.d/opencli')) {
|
|
149
|
+
appendFileSync(bashrcPath,
|
|
150
|
+
`\n# opencli completion\n[ -f "${completionFile}" ] && source "${completionFile}"\n`,
|
|
151
|
+
'utf8'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(`✓ Bash completion installed to ${completionFile}`);
|
|
157
|
+
console.log(` Restart your shell or run: source ~/.bashrc`);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'fish': {
|
|
161
|
+
const completionsDir = join(home, '.config', 'fish', 'completions');
|
|
162
|
+
const completionFile = join(completionsDir, 'opencli.fish');
|
|
163
|
+
ensureDir(completionsDir);
|
|
164
|
+
writeFileSync(completionFile, FISH_COMPLETION, 'utf8');
|
|
165
|
+
|
|
166
|
+
console.log(`✓ Fish completion installed to ${completionFile}`);
|
|
167
|
+
console.log(` Restart your shell to activate.`);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
// Completion install is best-effort; never fail the package install
|
|
173
|
+
if (process.env.OPENCLI_VERBOSE) {
|
|
174
|
+
console.error(`Warning: Could not install shell completion: ${err.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
main();
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell tab-completion support for opencli.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Shell script generators for bash, zsh, and fish
|
|
6
|
+
* - Dynamic completion logic that returns candidates for the current cursor position
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getRegistry } from './registry.js';
|
|
10
|
+
|
|
11
|
+
// ── Dynamic completion logic ───────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Built-in (non-dynamic) top-level commands.
|
|
15
|
+
*/
|
|
16
|
+
const BUILTIN_COMMANDS = [
|
|
17
|
+
'list',
|
|
18
|
+
'validate',
|
|
19
|
+
'verify',
|
|
20
|
+
'explore',
|
|
21
|
+
'probe', // alias for explore
|
|
22
|
+
'synthesize',
|
|
23
|
+
'generate',
|
|
24
|
+
'cascade',
|
|
25
|
+
'doctor',
|
|
26
|
+
'setup',
|
|
27
|
+
'completion',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Return completion candidates given the current command-line words and cursor index.
|
|
32
|
+
*
|
|
33
|
+
* @param words - The argv after 'opencli' (words[0] is the first arg, e.g. site name)
|
|
34
|
+
* @param cursor - 1-based position of the word being completed (1 = first arg)
|
|
35
|
+
*/
|
|
36
|
+
export function getCompletions(words: string[], cursor: number): string[] {
|
|
37
|
+
// cursor === 1 → completing the first argument (site name or built-in command)
|
|
38
|
+
if (cursor <= 1) {
|
|
39
|
+
const sites = new Set<string>();
|
|
40
|
+
for (const [, cmd] of getRegistry()) {
|
|
41
|
+
sites.add(cmd.site);
|
|
42
|
+
}
|
|
43
|
+
return [...BUILTIN_COMMANDS, ...sites].sort();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const site = words[0];
|
|
47
|
+
|
|
48
|
+
// If the first word is a built-in command, no further completion
|
|
49
|
+
if (BUILTIN_COMMANDS.includes(site)) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// cursor === 2 → completing the sub-command name under a site
|
|
54
|
+
if (cursor === 2) {
|
|
55
|
+
const subcommands: string[] = [];
|
|
56
|
+
for (const [, cmd] of getRegistry()) {
|
|
57
|
+
if (cmd.site === site) {
|
|
58
|
+
subcommands.push(cmd.name);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return subcommands.sort();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// cursor >= 3 → no further completion
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Shell script generators ────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export function bashCompletionScript(): string {
|
|
71
|
+
return `# Bash completion for opencli
|
|
72
|
+
# Add to ~/.bashrc: eval "$(opencli completion bash)"
|
|
73
|
+
_opencli_completions() {
|
|
74
|
+
local cur words cword
|
|
75
|
+
_get_comp_words_by_ref -n : cur words cword
|
|
76
|
+
|
|
77
|
+
local completions
|
|
78
|
+
completions=$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)
|
|
79
|
+
|
|
80
|
+
COMPREPLY=( $(compgen -W "$completions" -- "$cur") )
|
|
81
|
+
__ltrim_colon_completions "$cur"
|
|
82
|
+
}
|
|
83
|
+
complete -F _opencli_completions opencli
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function zshCompletionScript(): string {
|
|
88
|
+
return `# Zsh completion for opencli
|
|
89
|
+
# Add to ~/.zshrc: eval "$(opencli completion zsh)"
|
|
90
|
+
_opencli() {
|
|
91
|
+
local -a completions
|
|
92
|
+
local cword=$((CURRENT - 1))
|
|
93
|
+
completions=(\${(f)"$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)"})
|
|
94
|
+
compadd -a completions
|
|
95
|
+
}
|
|
96
|
+
compdef _opencli opencli
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function fishCompletionScript(): string {
|
|
101
|
+
return `# Fish completion for opencli
|
|
102
|
+
# Add to ~/.config/fish/config.fish: opencli completion fish | source
|
|
103
|
+
complete -c opencli -f -a '(
|
|
104
|
+
set -l tokens (commandline -cop)
|
|
105
|
+
set -l cursor (count (commandline -cop))
|
|
106
|
+
opencli --get-completions --cursor $cursor $tokens[2..] 2>/dev/null
|
|
107
|
+
)'
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Print the completion script for the requested shell.
|
|
113
|
+
*/
|
|
114
|
+
export function printCompletionScript(shell: string): void {
|
|
115
|
+
switch (shell) {
|
|
116
|
+
case 'bash':
|
|
117
|
+
process.stdout.write(bashCompletionScript());
|
|
118
|
+
break;
|
|
119
|
+
case 'zsh':
|
|
120
|
+
process.stdout.write(zshCompletionScript());
|
|
121
|
+
break;
|
|
122
|
+
case 'fish':
|
|
123
|
+
process.stdout.write(fishCompletionScript());
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
console.error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { render as renderOutput } from './output.js';
|
|
|
14
14
|
import { PlaywrightMCP } from './browser.js';
|
|
15
15
|
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
|
16
16
|
import { PKG_VERSION } from './version.js';
|
|
17
|
+
import { getCompletions, printCompletionScript } from './completion.js';
|
|
17
18
|
|
|
18
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
20
|
const __dirname = path.dirname(__filename);
|
|
@@ -22,6 +23,27 @@ const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
|
|
|
22
23
|
|
|
23
24
|
await discoverClis(BUILTIN_CLIS, USER_CLIS);
|
|
24
25
|
|
|
26
|
+
// ── Fast-path: handle --get-completions before commander parses ─────────
|
|
27
|
+
// Usage: opencli --get-completions --cursor <N> [word1 word2 ...]
|
|
28
|
+
const getCompIdx = process.argv.indexOf('--get-completions');
|
|
29
|
+
if (getCompIdx !== -1) {
|
|
30
|
+
const rest = process.argv.slice(getCompIdx + 1);
|
|
31
|
+
let cursor: number | undefined;
|
|
32
|
+
const words: string[] = [];
|
|
33
|
+
for (let i = 0; i < rest.length; i++) {
|
|
34
|
+
if (rest[i] === '--cursor' && i + 1 < rest.length) {
|
|
35
|
+
cursor = parseInt(rest[i + 1], 10);
|
|
36
|
+
i++; // skip the value
|
|
37
|
+
} else {
|
|
38
|
+
words.push(rest[i]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (cursor === undefined) cursor = words.length;
|
|
42
|
+
const candidates = getCompletions(words, cursor);
|
|
43
|
+
process.stdout.write(candidates.join('\n') + '\n');
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
const program = new Command();
|
|
26
48
|
program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION);
|
|
27
49
|
|
|
@@ -128,6 +150,13 @@ program.command('setup')
|
|
|
128
150
|
await runSetup({ cliVersion: PKG_VERSION, token: opts.token });
|
|
129
151
|
});
|
|
130
152
|
|
|
153
|
+
program.command('completion')
|
|
154
|
+
.description('Output shell completion script')
|
|
155
|
+
.argument('<shell>', 'Shell type: bash, zsh, or fish')
|
|
156
|
+
.action((shell) => {
|
|
157
|
+
printCompletionScript(shell);
|
|
158
|
+
});
|
|
159
|
+
|
|
131
160
|
// ── Dynamic site commands ──────────────────────────────────────────────────
|
|
132
161
|
|
|
133
162
|
const registry = getRegistry();
|