Compare commits

...

1 Commits

Author SHA1 Message Date
jackwener d51ca38cee refactor(cli): move external management under external 2026-05-02 01:35:25 +08:00
5 changed files with 40 additions and 15 deletions
+2 -2
View File
@@ -83,7 +83,7 @@ Use OpenCLI directly when you want a reliable command instead of a live browser
- `opencli list` shows every registered command.
- `opencli <site> <command>` runs a built-in or generated adapter.
- `opencli register mycli` exposes a local CLI through the same discovery surface.
- `opencli external register mycli` exposes a local CLI through the same discovery surface.
- `opencli doctor` helps diagnose browser connectivity.
## For AI Agents
@@ -282,7 +282,7 @@ OpenCLI acts as a universal hub for your existing command-line tools — unified
**Register your own** — add any local CLI so AI agents can discover it via `opencli list`:
```bash
opencli register mycli
opencli external register mycli
```
### Desktop App Adapters
+4 -3
View File
@@ -13,7 +13,7 @@ OpenCLI turns any website, Electron desktop app, or external CLI into a uniform
- **Adapter commands** — `opencli <site> <command> [...]`. Built-in adapters live in `clis/`, user adapters in `~/.opencli/clis/`. Each is backed by a strategy (`PUBLIC | COOKIE | HEADER | INTERCEPT | UI | LOCAL`) that tells you whether a Chrome session is needed.
- **Browser driving** — `opencli browser *` subcommands (`open`, `state`, `click`, `type`, `select`, `find`, `extract`, `network`, …) for ad-hoc interaction and scraping when no adapter covers the task. See `opencli-browser`.
- **Current-tab binding** — `opencli browser bind --domain <host>` attaches a `bound:*` workspace to the Chrome tab the user already opened/logged into. Follow-up commands use `opencli browser --workspace bound:default ...`. See `opencli-browser` before using it; bound workspaces have stricter navigation/tab-mutation safety rules.
- **External CLI passthrough** — `opencli gh`, `opencli docker`, `opencli vercel`, etc. Registered via `opencli install <name>` (auto-install from `external-clis.yaml`) or `opencli register <name>` (bring your own).
- **External CLI passthrough** — `opencli gh`, `opencli docker`, `opencli vercel`, etc. Managed via `opencli external install <name>` (auto-install from `external-clis.yaml`) or `opencli external register <name>` (bring your own).
## Install
@@ -126,11 +126,12 @@ opencli plugin create <name> # scaffold a new plugin
Wraps external command-line tools so you can discover + invoke them through the same `opencli …` entrypoint:
```bash
opencli install gh # auto-install via brew/apt/npm per external-clis.yaml
opencli register my-tool \
opencli external install gh # auto-install via brew/apt/npm per external-clis.yaml
opencli external register my-tool \
--binary my-tool \
--install "npm i -g my-tool" \
--desc "My internal CLI"
opencli external list
opencli gh pr list --limit 5 # passthrough; stdio is inherited, exit code propagated
opencli docker ps
```
+30 -6
View File
@@ -427,11 +427,10 @@ export function createProgram(BUILTIN_CLIS: string, USER_CLIS: string): Command
.command('list')
.description('List all available CLI commands')
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
.option('--json', 'JSON output (deprecated)')
.action((opts) => {
const registry = getRegistry();
const commands = [...new Set(registry.values())].sort((a, b) => fullName(a).localeCompare(fullName(b)));
const fmt = opts.json && opts.format === 'table' ? 'json' : opts.format;
const fmt = opts.format;
const isStructured = fmt === 'json' || fmt === 'yaml';
if (fmt !== 'table') {
@@ -2301,7 +2300,11 @@ cli({
const externalClis = loadExternalClis();
program
const externalCmd = program
.command('external')
.description('Manage external CLI passthrough commands');
externalCmd
.command('install')
.description('Install an external CLI')
.argument('<name>', 'Name of the external CLI')
@@ -2315,7 +2318,7 @@ cli({
installExternalCli(ext);
});
program
externalCmd
.command('register')
.description('Register an external CLI')
.argument('<name>', 'Name of the CLI')
@@ -2326,6 +2329,27 @@ cli({
registerExternalCli(name, { binary: opts.binary, install: opts.install, description: opts.desc });
});
externalCmd
.command('list')
.description('List registered external CLIs')
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
.action((opts) => {
const rows = loadExternalClis().map((ext) => ({
name: ext.name,
binary: ext.binary,
installed: isBinaryInstalled(ext.binary),
description: ext.description ?? '',
homepage: ext.homepage ?? '',
tags: ext.tags?.join(', ') ?? '',
}));
renderOutput(rows, {
fmt: opts.format,
columns: ['name', 'binary', 'installed', 'description', 'homepage', 'tags'],
title: 'opencli/external/list',
source: 'opencli external list',
});
});
function passthroughExternal(name: string, parsedArgs?: string[]) {
const args = parsedArgs ?? (() => {
const idx = process.argv.indexOf(name);
@@ -2376,13 +2400,13 @@ cli({
// ── Unknown command fallback ──────────────────────────────────────────────
// Security: do NOT auto-discover and register arbitrary system binaries.
// Only explicitly registered external CLIs (via `opencli register`) are allowed.
// Only explicitly registered external CLIs are allowed.
program.on('command:*', (operands: string[]) => {
const binary = operands[0];
console.error(styleText('red', `error: unknown command '${binary}'`));
if (isBinaryInstalled(binary)) {
console.error(styleText('dim', ` Tip: '${binary}' exists on your PATH. Use 'opencli register ${binary}' to add it as an external CLI.`));
console.error(styleText('dim', ` Tip: '${binary}' exists on your PATH. Use 'opencli external register ${binary}' to add it as an external CLI.`));
}
program.outputHelp();
process.exitCode = EXIT_CODES.USAGE_ERROR;
+1 -2
View File
@@ -16,8 +16,7 @@ export const BUILTIN_COMMANDS = [
'tab',
'doctor',
'plugin',
'install',
'register',
'external',
'completion',
];
+3 -2
View File
@@ -17,8 +17,9 @@ describe('getCompletions', () => {
const completions = getCompletions([], 1);
expect(completions).toContain('plugin');
expect(completions).toContain('install');
expect(completions).toContain('register');
expect(completions).toContain('external');
expect(completions).not.toContain('install');
expect(completions).not.toContain('register');
expect(completions).not.toContain('setup');
});