Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49cc6014fc | |||
| d2f4500bf5 |
Generated
+4
-7
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user