Compare commits

...

2 Commits

Author SHA1 Message Date
jackwener 49cc6014fc review: add missing probe alias to completion builtin commands 2026-03-17 16:32:19 +08:00
RinChanNOWWW d2f4500bf5 feat: support commands completion
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-17 16:30:38 +08:00
5 changed files with 342 additions and 7 deletions
+4 -7
View File
@@ -1,13 +1,14 @@
{
"name": "@jackwener/opencli",
"version": "0.7.5",
"version": "0.7.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@jackwener/opencli",
"version": "0.7.5",
"license": "BSD-3-Clause",
"version": "0.7.4",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"chalk": "^5.3.0",
"cli-table3": "^0.6.5",
@@ -894,7 +895,6 @@
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -1563,7 +1563,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -1806,7 +1805,6 @@
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
@@ -1848,7 +1846,6 @@
"integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@oxc-project/runtime": "0.115.0",
"lightningcss": "^1.32.0",
+1
View File
@@ -20,6 +20,7 @@
"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')\"",
"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')\"",
"start": "node dist/main.js",
"postinstall": "node scripts/postinstall.js || true",
"typecheck": "tsc --noEmit",
"lint": "tsc --noEmit",
"prepublishOnly": "npm run build",
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env node
/**
* postinstall script — automatically install shell completion files.
*
* Detects the user's default shell and writes the completion script to the
* standard system completion directory so that tab-completion works immediately
* after `npm install -g`.
*
* Supported shells: bash, zsh, fish.
*
* This script is intentionally plain Node.js (no TypeScript, no imports from
* the main source tree) so that it can run without a build step.
*/
import { mkdirSync, writeFileSync, existsSync, readFileSync, appendFileSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
// ── Completion script content ──────────────────────────────────────────────
const BASH_COMPLETION = `# Bash completion for opencli (auto-installed)
_opencli_completions() {
local cur words cword
_get_comp_words_by_ref -n : cur words cword
local completions
completions=$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)
COMPREPLY=( $(compgen -W "$completions" -- "$cur") )
__ltrim_colon_completions "$cur"
}
complete -F _opencli_completions opencli
`;
const ZSH_COMPLETION = `#compdef opencli
# Zsh completion for opencli (auto-installed)
_opencli() {
local -a completions
local cword=$((CURRENT - 1))
completions=(\${(f)"$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)"})
compadd -a completions
}
_opencli
`;
const FISH_COMPLETION = `# Fish completion for opencli (auto-installed)
complete -c opencli -f -a '(
set -l tokens (commandline -cop)
set -l cursor (count (commandline -cop))
opencli --get-completions --cursor $cursor $tokens[2..] 2>/dev/null
)'
`;
// ── Helpers ────────────────────────────────────────────────────────────────
function detectShell() {
const shell = process.env.SHELL || '';
if (shell.includes('zsh')) return 'zsh';
if (shell.includes('bash')) return 'bash';
if (shell.includes('fish')) return 'fish';
return null;
}
function ensureDir(dir) {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
/**
* Ensure fpath contains the custom completions directory in .zshrc
*/
function ensureZshFpath(completionsDir, zshrcPath) {
const fpathLine = `fpath=(${completionsDir} $fpath)`;
const autoloadLine = `autoload -Uz compinit && compinit`;
if (!existsSync(zshrcPath)) {
writeFileSync(zshrcPath, `${fpathLine}\n${autoloadLine}\n`, 'utf8');
return;
}
const content = readFileSync(zshrcPath, 'utf8');
// Check if completions dir is already in fpath
if (content.includes(completionsDir)) {
return; // already configured
}
// Append fpath configuration
let addition = `\n# opencli completion\n${fpathLine}\n`;
if (!content.includes('compinit')) {
addition += `${autoloadLine}\n`;
}
appendFileSync(zshrcPath, addition, 'utf8');
}
// ── Main ───────────────────────────────────────────────────────────────────
function main() {
// Skip in CI environments
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) {
return;
}
// Only install completion for global installs and npm link
const isGlobal = process.env.npm_config_global === 'true';
if (!isGlobal) {
return;
}
const shell = detectShell();
if (!shell) {
// Cannot determine shell; silently skip
return;
}
const home = homedir();
try {
switch (shell) {
case 'zsh': {
const completionsDir = join(home, '.zsh', 'completions');
const completionFile = join(completionsDir, '_opencli');
ensureDir(completionsDir);
writeFileSync(completionFile, ZSH_COMPLETION, 'utf8');
// Ensure fpath is set up in .zshrc
const zshrcPath = join(home, '.zshrc');
ensureZshFpath(completionsDir, zshrcPath);
console.log(`✓ Zsh completion installed to ${completionFile}`);
console.log(` Restart your shell or run: source ~/.zshrc`);
break;
}
case 'bash': {
// Try system-level first, fall back to user-level
const userCompDir = join(home, '.bash_completion.d');
const completionFile = join(userCompDir, 'opencli');
ensureDir(userCompDir);
writeFileSync(completionFile, BASH_COMPLETION, 'utf8');
// Ensure .bashrc sources the completion directory
const bashrcPath = join(home, '.bashrc');
if (existsSync(bashrcPath)) {
const content = readFileSync(bashrcPath, 'utf8');
if (!content.includes('.bash_completion.d/opencli')) {
appendFileSync(bashrcPath,
`\n# opencli completion\n[ -f "${completionFile}" ] && source "${completionFile}"\n`,
'utf8'
);
}
}
console.log(`✓ Bash completion installed to ${completionFile}`);
console.log(` Restart your shell or run: source ~/.bashrc`);
break;
}
case 'fish': {
const completionsDir = join(home, '.config', 'fish', 'completions');
const completionFile = join(completionsDir, 'opencli.fish');
ensureDir(completionsDir);
writeFileSync(completionFile, FISH_COMPLETION, 'utf8');
console.log(`✓ Fish completion installed to ${completionFile}`);
console.log(` Restart your shell to activate.`);
break;
}
}
} catch (err) {
// Completion install is best-effort; never fail the package install
if (process.env.OPENCLI_VERBOSE) {
console.error(`Warning: Could not install shell completion: ${err.message}`);
}
}
}
main();
+129
View File
@@ -0,0 +1,129 @@
/**
* Shell tab-completion support for opencli.
*
* Provides:
* - Shell script generators for bash, zsh, and fish
* - Dynamic completion logic that returns candidates for the current cursor position
*/
import { getRegistry } from './registry.js';
// ── Dynamic completion logic ───────────────────────────────────────────────
/**
* Built-in (non-dynamic) top-level commands.
*/
const BUILTIN_COMMANDS = [
'list',
'validate',
'verify',
'explore',
'probe', // alias for explore
'synthesize',
'generate',
'cascade',
'doctor',
'setup',
'completion',
];
/**
* Return completion candidates given the current command-line words and cursor index.
*
* @param words - The argv after 'opencli' (words[0] is the first arg, e.g. site name)
* @param cursor - 1-based position of the word being completed (1 = first arg)
*/
export function getCompletions(words: string[], cursor: number): string[] {
// cursor === 1 → completing the first argument (site name or built-in command)
if (cursor <= 1) {
const sites = new Set<string>();
for (const [, cmd] of getRegistry()) {
sites.add(cmd.site);
}
return [...BUILTIN_COMMANDS, ...sites].sort();
}
const site = words[0];
// If the first word is a built-in command, no further completion
if (BUILTIN_COMMANDS.includes(site)) {
return [];
}
// cursor === 2 → completing the sub-command name under a site
if (cursor === 2) {
const subcommands: string[] = [];
for (const [, cmd] of getRegistry()) {
if (cmd.site === site) {
subcommands.push(cmd.name);
}
}
return subcommands.sort();
}
// cursor >= 3 → no further completion
return [];
}
// ── Shell script generators ────────────────────────────────────────────────
export function bashCompletionScript(): string {
return `# Bash completion for opencli
# Add to ~/.bashrc: eval "$(opencli completion bash)"
_opencli_completions() {
local cur words cword
_get_comp_words_by_ref -n : cur words cword
local completions
completions=$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)
COMPREPLY=( $(compgen -W "$completions" -- "$cur") )
__ltrim_colon_completions "$cur"
}
complete -F _opencli_completions opencli
`;
}
export function zshCompletionScript(): string {
return `# Zsh completion for opencli
# Add to ~/.zshrc: eval "$(opencli completion zsh)"
_opencli() {
local -a completions
local cword=$((CURRENT - 1))
completions=(\${(f)"$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)"})
compadd -a completions
}
compdef _opencli opencli
`;
}
export function fishCompletionScript(): string {
return `# Fish completion for opencli
# Add to ~/.config/fish/config.fish: opencli completion fish | source
complete -c opencli -f -a '(
set -l tokens (commandline -cop)
set -l cursor (count (commandline -cop))
opencli --get-completions --cursor $cursor $tokens[2..] 2>/dev/null
)'
`;
}
/**
* Print the completion script for the requested shell.
*/
export function printCompletionScript(shell: string): void {
switch (shell) {
case 'bash':
process.stdout.write(bashCompletionScript());
break;
case 'zsh':
process.stdout.write(zshCompletionScript());
break;
case 'fish':
process.stdout.write(fishCompletionScript());
break;
default:
console.error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
process.exitCode = 1;
}
}
+29
View File
@@ -14,6 +14,7 @@ import { render as renderOutput } from './output.js';
import { PlaywrightMCP } from './browser.js';
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
import { PKG_VERSION } from './version.js';
import { getCompletions, printCompletionScript } from './completion.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -22,6 +23,27 @@ const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
await discoverClis(BUILTIN_CLIS, USER_CLIS);
// ── Fast-path: handle --get-completions before commander parses ─────────
// Usage: opencli --get-completions --cursor <N> [word1 word2 ...]
const getCompIdx = process.argv.indexOf('--get-completions');
if (getCompIdx !== -1) {
const rest = process.argv.slice(getCompIdx + 1);
let cursor: number | undefined;
const words: string[] = [];
for (let i = 0; i < rest.length; i++) {
if (rest[i] === '--cursor' && i + 1 < rest.length) {
cursor = parseInt(rest[i + 1], 10);
i++; // skip the value
} else {
words.push(rest[i]);
}
}
if (cursor === undefined) cursor = words.length;
const candidates = getCompletions(words, cursor);
process.stdout.write(candidates.join('\n') + '\n');
process.exit(0);
}
const program = new Command();
program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION);
@@ -128,6 +150,13 @@ program.command('setup')
await runSetup({ cliVersion: PKG_VERSION, token: opts.token });
});
program.command('completion')
.description('Output shell completion script')
.argument('<shell>', 'Shell type: bash, zsh, or fish')
.action((shell) => {
printCompletionScript(shell);
});
// ── Dynamic site commands ──────────────────────────────────────────────────
const registry = getRegistry();