@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.
@@ -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.5",
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();