@jackwener/opencli 1.7.18 → 1.7.19
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/README.md +7 -8
- package/README.zh-CN.md +7 -8
- package/cli-manifest.json +305 -9
- package/clis/ctrip/ctrip.test.js +486 -1
- package/clis/ctrip/flight.js +136 -0
- package/clis/ctrip/hotel-search.js +132 -0
- package/clis/ctrip/utils.js +298 -0
- package/clis/google/search.js +16 -6
- package/clis/google-scholar/search.js +20 -5
- package/clis/google-scholar/search.test.js +35 -2
- package/clis/reddit/home.js +117 -0
- package/clis/reddit/home.test.js +127 -0
- package/clis/reddit/read.js +400 -54
- package/clis/reddit/read.test.js +315 -12
- package/clis/reddit/subreddit-info.js +117 -0
- package/clis/reddit/subreddit-info.test.js +163 -0
- package/clis/reddit/whoami.js +84 -0
- package/clis/reddit/whoami.test.js +105 -0
- package/clis/rednote/search.js +6 -2
- package/clis/twitter/bookmark-folder.js +3 -1
- package/clis/twitter/bookmarks.js +3 -1
- package/clis/twitter/followers.js +20 -5
- package/clis/twitter/followers.test.js +44 -0
- package/clis/twitter/following.js +36 -20
- package/clis/twitter/following.test.js +60 -8
- package/clis/twitter/likes.js +28 -13
- package/clis/twitter/likes.test.js +111 -1
- package/clis/twitter/list-add.js +128 -204
- package/clis/twitter/list-add.test.js +97 -1
- package/clis/twitter/list-tweets.js +13 -4
- package/clis/twitter/list-tweets.test.js +48 -0
- package/clis/twitter/lists.js +5 -2
- package/clis/twitter/post.js +23 -4
- package/clis/twitter/post.test.js +30 -0
- package/clis/twitter/profile.js +16 -8
- package/clis/twitter/profile.test.js +39 -0
- package/clis/twitter/reply.js +133 -10
- package/clis/twitter/reply.test.js +55 -0
- package/clis/twitter/search.js +188 -170
- package/clis/twitter/search.test.js +96 -258
- package/clis/twitter/shared.js +167 -16
- package/clis/twitter/shared.test.js +102 -1
- package/clis/twitter/timeline.js +3 -1
- package/clis/twitter/tweets.js +147 -51
- package/clis/twitter/tweets.test.js +238 -1
- package/clis/xiaohongshu/comments.js +23 -2
- package/clis/xiaohongshu/comments.test.js +63 -1
- package/clis/xiaohongshu/search.js +168 -13
- package/clis/xiaohongshu/search.test.js +82 -8
- package/clis/xueqiu/earnings-date.js +2 -2
- package/clis/xueqiu/kline.js +2 -2
- package/clis/xueqiu/utils.js +19 -0
- package/clis/xueqiu/utils.test.js +26 -0
- package/clis/zhihu/answer-detail.js +233 -0
- package/clis/zhihu/answer-detail.test.js +330 -0
- package/clis/zhihu/question.js +44 -10
- package/clis/zhihu/question.test.js +78 -1
- package/clis/zhihu/recommend.js +103 -0
- package/clis/zhihu/recommend.test.js +143 -0
- package/dist/src/browser/base-page.d.ts +3 -2
- package/dist/src/browser/base-page.test.js +2 -2
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/page.d.ts +3 -2
- package/dist/src/browser/page.js +4 -4
- package/dist/src/browser/page.test.js +31 -0
- package/dist/src/browser/utils.d.ts +10 -0
- package/dist/src/browser/utils.js +37 -0
- package/dist/src/browser/utils.test.d.ts +1 -0
- package/dist/src/browser/utils.test.js +29 -0
- package/dist/src/cli-argv-preprocess.d.ts +37 -0
- package/dist/src/cli-argv-preprocess.js +131 -0
- package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
- package/dist/src/cli-argv-preprocess.test.js +130 -0
- package/dist/src/cli.js +123 -86
- package/dist/src/cli.test.js +33 -28
- package/dist/src/commands/daemon.js +6 -7
- package/dist/src/doctor.js +15 -16
- package/dist/src/download/progress.js +15 -11
- package/dist/src/download/progress.test.d.ts +1 -0
- package/dist/src/download/progress.test.js +25 -0
- package/dist/src/execution.js +1 -3
- package/dist/src/execution.test.js +4 -16
- package/dist/src/help.d.ts +11 -0
- package/dist/src/help.js +46 -5
- package/dist/src/logger.js +8 -9
- package/dist/src/main.js +16 -0
- package/dist/src/output.js +4 -5
- package/dist/src/runtime-detect.d.ts +1 -1
- package/dist/src/runtime-detect.js +1 -1
- package/dist/src/runtime-detect.test.js +3 -2
- package/dist/src/tui.d.ts +0 -1
- package/dist/src/tui.js +9 -22
- package/dist/src/types.d.ts +3 -1
- package/dist/src/update-check.js +4 -5
- package/package.json +5 -4
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getBrowserSubcommandNames, rewriteBrowserArgv } from './cli-argv-preprocess.js';
|
|
3
|
+
describe('rewriteBrowserArgv', () => {
|
|
4
|
+
it('rewrites `browser <session> <subcommand>` into `browser --session <name> <subcommand>`', () => {
|
|
5
|
+
expect(rewriteBrowserArgv(['browser', 'work', 'state'])).toEqual([
|
|
6
|
+
'browser',
|
|
7
|
+
'--session',
|
|
8
|
+
'work',
|
|
9
|
+
'state',
|
|
10
|
+
]);
|
|
11
|
+
});
|
|
12
|
+
it('rewrites with subcommand arguments preserved', () => {
|
|
13
|
+
expect(rewriteBrowserArgv(['browser', 'mercury', 'open', 'https://x.com'])).toEqual([
|
|
14
|
+
'browser',
|
|
15
|
+
'--session',
|
|
16
|
+
'mercury',
|
|
17
|
+
'open',
|
|
18
|
+
'https://x.com',
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
it('rewrites `browser <session> bind`', () => {
|
|
22
|
+
expect(rewriteBrowserArgv(['browser', 'mercury', 'bind'])).toEqual([
|
|
23
|
+
'browser',
|
|
24
|
+
'--session',
|
|
25
|
+
'mercury',
|
|
26
|
+
'bind',
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
it('leaves argv alone when session omitted and a subcommand follows', () => {
|
|
30
|
+
// Commander surfaces the required-flag error itself.
|
|
31
|
+
expect(rewriteBrowserArgv(['browser', 'state'])).toEqual(['browser', 'state']);
|
|
32
|
+
expect(rewriteBrowserArgv(['browser', 'bind'])).toEqual(['browser', 'bind']);
|
|
33
|
+
});
|
|
34
|
+
it('leaves argv alone when the token after `browser` is a flag', () => {
|
|
35
|
+
expect(rewriteBrowserArgv(['browser', '--help'])).toEqual(['browser', '--help']);
|
|
36
|
+
expect(rewriteBrowserArgv(['browser', '-h'])).toEqual(['browser', '-h']);
|
|
37
|
+
});
|
|
38
|
+
it('refuses the retired `opencli browser --session foo ...` user form', () => {
|
|
39
|
+
// The flag form is no longer a public entrance. Tests calling
|
|
40
|
+
// program.parseAsync directly bypass the preprocessor, so internal
|
|
41
|
+
// callers still work; but the user-facing pipeline throws.
|
|
42
|
+
expect(() => rewriteBrowserArgv(['browser', '--session', 'foo', 'state']))
|
|
43
|
+
.toThrowError(/no longer a public option/i);
|
|
44
|
+
expect(() => rewriteBrowserArgv(['browser', '--session=foo', 'state']))
|
|
45
|
+
.toThrowError(/no longer a public option/i);
|
|
46
|
+
});
|
|
47
|
+
it('leaves argv alone when `browser` is not present', () => {
|
|
48
|
+
expect(rewriteBrowserArgv(['twitter', 'tweets', '@elonmusk'])).toEqual([
|
|
49
|
+
'twitter',
|
|
50
|
+
'tweets',
|
|
51
|
+
'@elonmusk',
|
|
52
|
+
]);
|
|
53
|
+
expect(rewriteBrowserArgv(['doctor'])).toEqual(['doctor']);
|
|
54
|
+
});
|
|
55
|
+
it('returns argv unchanged when `browser` is the last token', () => {
|
|
56
|
+
expect(rewriteBrowserArgv(['browser'])).toEqual(['browser']);
|
|
57
|
+
});
|
|
58
|
+
it('only rewrites when `browser` is the root command, not deeper in argv', () => {
|
|
59
|
+
// `opencli adapter init browser/x` — the literal `browser` is a path argument,
|
|
60
|
+
// not the root command. Must not be touched.
|
|
61
|
+
expect(rewriteBrowserArgv(['adapter', 'init', 'browser', 'x'])).toEqual([
|
|
62
|
+
'adapter',
|
|
63
|
+
'init',
|
|
64
|
+
'browser',
|
|
65
|
+
'x',
|
|
66
|
+
]);
|
|
67
|
+
// Same for URLs or arbitrary arg values that happen to contain `browser`.
|
|
68
|
+
expect(rewriteBrowserArgv(['twitter', 'tweets', 'https://browser.example.com'])).toEqual([
|
|
69
|
+
'twitter',
|
|
70
|
+
'tweets',
|
|
71
|
+
'https://browser.example.com',
|
|
72
|
+
]);
|
|
73
|
+
// First-match heuristic must NOT rewrite when an earlier non-flag token
|
|
74
|
+
// already established a different root command.
|
|
75
|
+
expect(rewriteBrowserArgv(['list', 'browser', 'state'])).toEqual([
|
|
76
|
+
'list',
|
|
77
|
+
'browser',
|
|
78
|
+
'state',
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
it('skips leading root flags before identifying the root command', () => {
|
|
82
|
+
// `--profile` takes a value — the value is not the command.
|
|
83
|
+
expect(rewriteBrowserArgv(['--profile', 'work', 'browser', 'mercury', 'state'])).toEqual([
|
|
84
|
+
'--profile',
|
|
85
|
+
'work',
|
|
86
|
+
'browser',
|
|
87
|
+
'--session',
|
|
88
|
+
'mercury',
|
|
89
|
+
'state',
|
|
90
|
+
]);
|
|
91
|
+
// Long form with `=` separator consumes one slot only.
|
|
92
|
+
expect(rewriteBrowserArgv(['--profile=work', 'browser', 'mercury', 'state'])).toEqual([
|
|
93
|
+
'--profile=work',
|
|
94
|
+
'browser',
|
|
95
|
+
'--session',
|
|
96
|
+
'mercury',
|
|
97
|
+
'state',
|
|
98
|
+
]);
|
|
99
|
+
// Boolean flags don't consume values.
|
|
100
|
+
expect(rewriteBrowserArgv(['-v', 'browser', 'mercury', 'state'])).toEqual([
|
|
101
|
+
'-v',
|
|
102
|
+
'browser',
|
|
103
|
+
'--session',
|
|
104
|
+
'mercury',
|
|
105
|
+
'state',
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
it('leaves argv alone when the root command is not `browser`, even if `browser` appears later', () => {
|
|
109
|
+
// The first browser keyword does NOT win — it must be at the root.
|
|
110
|
+
expect(rewriteBrowserArgv(['twitter', 'browser', 'work', 'state'])).toEqual([
|
|
111
|
+
'twitter',
|
|
112
|
+
'browser',
|
|
113
|
+
'work',
|
|
114
|
+
'state',
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
it('reserved subcommand list covers every known browser subcommand registered in cli.ts', () => {
|
|
118
|
+
const names = getBrowserSubcommandNames();
|
|
119
|
+
const required = [
|
|
120
|
+
'analyze', 'back', 'bind', 'check', 'click', 'close', 'console', 'dblclick',
|
|
121
|
+
'dialog', 'drag', 'eval', 'extract', 'fill', 'find', 'focus', 'frames',
|
|
122
|
+
'get', 'hover', 'init', 'keys', 'network', 'open', 'screenshot', 'scroll',
|
|
123
|
+
'select', 'state', 'tab', 'type', 'unbind', 'uncheck', 'upload', 'verify',
|
|
124
|
+
'wait',
|
|
125
|
+
];
|
|
126
|
+
for (const name of required) {
|
|
127
|
+
expect(names.has(name)).toBe(true);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
package/dist/src/cli.js
CHANGED
|
@@ -8,8 +8,7 @@ import * as fs from 'node:fs';
|
|
|
8
8
|
import * as os from 'node:os';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { Command, InvalidArgumentError } from 'commander';
|
|
12
|
-
import { styleText } from 'node:util';
|
|
11
|
+
import { Command, InvalidArgumentError, Option } from 'commander';
|
|
13
12
|
import { findPackageRoot, getBuiltEntryCandidates } from './package-paths.js';
|
|
14
13
|
import { fullName, getRegistry, strategyLabel } from './registry.js';
|
|
15
14
|
import { serializeCommand, formatArgSummary } from './serialization.js';
|
|
@@ -18,7 +17,7 @@ import { PKG_VERSION } from './version.js';
|
|
|
18
17
|
import { printCompletionScript } from './completion.js';
|
|
19
18
|
import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
|
|
20
19
|
import { registerAllCommands } from './commanderAdapter.js';
|
|
21
|
-
import { classifyAdapter, formatRootAdapterHelpText, installCommanderNamespaceStructuredHelp, installStructuredHelp, rootHelpData } from './help.js';
|
|
20
|
+
import { classifyAdapter, formatRootAdapterHelpText, installCommanderNamespaceStructuredHelp, installStructuredHelp, leadingPositionalFromUsage, rootHelpData } from './help.js';
|
|
22
21
|
import { EXIT_CODES, getErrorMessage, BrowserConnectError } from './errors.js';
|
|
23
22
|
import { TargetError } from './browser/target-errors.js';
|
|
24
23
|
import { resolveTargetJs, getTextResolvedJs, getValueResolvedJs, getAttributesResolvedJs, selectResolvedJs, isAutocompleteResolvedJs } from './browser/target-resolver.js';
|
|
@@ -372,10 +371,13 @@ function getCommandOption(command, option) {
|
|
|
372
371
|
return undefined;
|
|
373
372
|
}
|
|
374
373
|
function getBrowserSession(command) {
|
|
374
|
+
// The CLI surface is `opencli browser <session> <subcommand>`. main.ts rewrites
|
|
375
|
+
// argv to insert `--session <name>` before commander parses it; this helper
|
|
376
|
+
// reads back the rewritten flag.
|
|
375
377
|
const raw = getCommandOption(command, 'session');
|
|
376
378
|
if (typeof raw === 'string' && raw.trim())
|
|
377
379
|
return raw.trim();
|
|
378
|
-
throw new Error('
|
|
380
|
+
throw new Error('<session> is a required positional argument: opencli browser <session> <command>');
|
|
379
381
|
}
|
|
380
382
|
function getBrowserContextId(command) {
|
|
381
383
|
const raw = getCommandOption(command, 'profile');
|
|
@@ -523,31 +525,31 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
523
525
|
sites.set(cmd.site, g);
|
|
524
526
|
}
|
|
525
527
|
console.log();
|
|
526
|
-
console.log(
|
|
528
|
+
console.log(' opencli' + ' — available commands');
|
|
527
529
|
console.log();
|
|
528
530
|
for (const [site, cmds] of sites) {
|
|
529
|
-
console.log(
|
|
531
|
+
console.log(` ${site}`);
|
|
530
532
|
for (const cmd of cmds) {
|
|
531
533
|
const label = strategyLabel(cmd);
|
|
532
534
|
const tag = label === 'public'
|
|
533
|
-
?
|
|
534
|
-
:
|
|
535
|
-
const aliases = cmd.aliases?.length ?
|
|
536
|
-
console.log(` ${cmd.name} ${tag}${aliases}${cmd.description ?
|
|
535
|
+
? '[public]'
|
|
536
|
+
: `[${label}]`;
|
|
537
|
+
const aliases = cmd.aliases?.length ? ` (aliases: ${cmd.aliases.join(', ')})` : '';
|
|
538
|
+
console.log(` ${cmd.name} ${tag}${aliases}${cmd.description ? ` — ${cmd.description}` : ''}`);
|
|
537
539
|
}
|
|
538
540
|
console.log();
|
|
539
541
|
}
|
|
540
542
|
const externalClis = loadExternalClis();
|
|
541
543
|
if (externalClis.length > 0) {
|
|
542
|
-
console.log(
|
|
544
|
+
console.log(' external CLIs');
|
|
543
545
|
for (const ext of externalClis) {
|
|
544
546
|
const isInstalled = isBinaryInstalled(ext.binary);
|
|
545
|
-
const tag = isInstalled ?
|
|
546
|
-
console.log(` ${ext.name} ${tag}${ext.description ?
|
|
547
|
+
const tag = isInstalled ? '[installed]' : '[auto-install]';
|
|
548
|
+
console.log(` ${ext.name} ${tag}${ext.description ? ` — ${ext.description}` : ''}`);
|
|
547
549
|
}
|
|
548
550
|
console.log();
|
|
549
551
|
}
|
|
550
|
-
console.log(
|
|
552
|
+
console.log(` ${commands.length} built-in commands across ${sites.size} sites, ${externalClis.length} external CLIs`);
|
|
551
553
|
console.log();
|
|
552
554
|
});
|
|
553
555
|
// ── Built-in: validate / verify ───────────────────────────────────────────
|
|
@@ -600,9 +602,23 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
600
602
|
// All commands wrapped in browserAction() for consistent error handling.
|
|
601
603
|
const browser = program
|
|
602
604
|
.command('browser')
|
|
603
|
-
|
|
605
|
+
// --session is an internal hidden option used by the daemon protocol and direct
|
|
606
|
+
// program.parseAsync callers (tests). User-facing surface is the <session>
|
|
607
|
+
// positional; main.ts argv preprocessor rewrites positional -> --session.
|
|
608
|
+
.addOption(new Option('--session <name>', 'Internal — set automatically from the <session> positional').hideHelp())
|
|
604
609
|
.option('--window <mode>', 'Browser window mode: foreground or background')
|
|
605
|
-
.description('Browser control — navigate, click, type, extract, wait (no LLM needed)')
|
|
610
|
+
.description('Browser control — navigate, click, type, extract, wait (no LLM needed)')
|
|
611
|
+
.usage('<session> <command> [options]')
|
|
612
|
+
.addHelpText('after', `
|
|
613
|
+
<session> is a required positional: pass the name of the browser session every subcommand should operate on. Reuse the same name across calls to keep the tab/state alive; pick a different name to isolate parallel browser work.
|
|
614
|
+
|
|
615
|
+
Examples:
|
|
616
|
+
$ opencli browser work open https://x.com
|
|
617
|
+
$ opencli browser work click 12
|
|
618
|
+
$ opencli browser work state
|
|
619
|
+
$ opencli browser work bind
|
|
620
|
+
$ opencli browser work unbind
|
|
621
|
+
`);
|
|
606
622
|
const originalBrowserDescription = browser.description();
|
|
607
623
|
/**
|
|
608
624
|
* Resolve a `<target>` (numeric ref or CSS selector) via the unified resolver.
|
|
@@ -723,8 +739,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
723
739
|
};
|
|
724
740
|
}
|
|
725
741
|
browser.command('bind')
|
|
726
|
-
.
|
|
727
|
-
.description('Bind the current Chrome tab/window to a browser session')
|
|
742
|
+
.description('Bind the current Chrome tab/window to the browser session named by <session>')
|
|
728
743
|
.action(async (optsOrCommand, maybeCommand) => {
|
|
729
744
|
const command = optsOrCommand instanceof Command ? optsOrCommand : maybeCommand;
|
|
730
745
|
const session = getBrowserSession(command);
|
|
@@ -754,8 +769,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
754
769
|
}
|
|
755
770
|
});
|
|
756
771
|
browser.command('unbind')
|
|
757
|
-
.
|
|
758
|
-
.description('Detach a bound browser session without closing the user tab/window')
|
|
772
|
+
.description('Detach the bound browser session named by <session> without closing the user tab/window')
|
|
759
773
|
.action(async (optsOrCommand, maybeCommand) => {
|
|
760
774
|
const command = optsOrCommand instanceof Command ? optsOrCommand : maybeCommand;
|
|
761
775
|
const session = getBrowserSession(command);
|
|
@@ -2579,18 +2593,18 @@ cli({
|
|
|
2579
2593
|
await discoverPlugins();
|
|
2580
2594
|
if (Array.isArray(result)) {
|
|
2581
2595
|
if (result.length === 0) {
|
|
2582
|
-
console.log(
|
|
2596
|
+
console.log('No plugins were installed (all skipped or incompatible).');
|
|
2583
2597
|
}
|
|
2584
2598
|
else {
|
|
2585
|
-
console.log(
|
|
2599
|
+
console.log(`\u2705 Installed ${result.length} plugin(s) from monorepo: ${result.join(', ')}`);
|
|
2586
2600
|
}
|
|
2587
2601
|
}
|
|
2588
2602
|
else {
|
|
2589
|
-
console.log(
|
|
2603
|
+
console.log(`\u2705 Plugin "${result}" installed successfully. Commands are ready to use.`);
|
|
2590
2604
|
}
|
|
2591
2605
|
}
|
|
2592
2606
|
catch (err) {
|
|
2593
|
-
console.error(
|
|
2607
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
2594
2608
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2595
2609
|
}
|
|
2596
2610
|
});
|
|
@@ -2602,10 +2616,10 @@ cli({
|
|
|
2602
2616
|
const { uninstallPlugin } = await import('./plugin.js');
|
|
2603
2617
|
try {
|
|
2604
2618
|
uninstallPlugin(name);
|
|
2605
|
-
console.log(
|
|
2619
|
+
console.log(`✅ Plugin "${name}" uninstalled.`);
|
|
2606
2620
|
}
|
|
2607
2621
|
catch (err) {
|
|
2608
|
-
console.error(
|
|
2622
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
2609
2623
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2610
2624
|
}
|
|
2611
2625
|
});
|
|
@@ -2616,12 +2630,12 @@ cli({
|
|
|
2616
2630
|
.option('--all', 'Update all installed plugins')
|
|
2617
2631
|
.action(async (name, opts) => {
|
|
2618
2632
|
if (!name && !opts.all) {
|
|
2619
|
-
console.error(
|
|
2633
|
+
console.error('Error: Please specify a plugin name or use the --all flag.');
|
|
2620
2634
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2621
2635
|
return;
|
|
2622
2636
|
}
|
|
2623
2637
|
if (name && opts.all) {
|
|
2624
|
-
console.error(
|
|
2638
|
+
console.error('Error: Cannot specify both a plugin name and --all.');
|
|
2625
2639
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2626
2640
|
return;
|
|
2627
2641
|
}
|
|
@@ -2633,36 +2647,36 @@ cli({
|
|
|
2633
2647
|
await discoverPlugins();
|
|
2634
2648
|
}
|
|
2635
2649
|
let hasErrors = false;
|
|
2636
|
-
console.log(
|
|
2650
|
+
console.log(' Update Results:');
|
|
2637
2651
|
for (const result of results) {
|
|
2638
2652
|
if (result.success) {
|
|
2639
|
-
console.log(`
|
|
2653
|
+
console.log(` ✓ ${result.name}`);
|
|
2640
2654
|
continue;
|
|
2641
2655
|
}
|
|
2642
2656
|
hasErrors = true;
|
|
2643
|
-
console.log(`
|
|
2657
|
+
console.log(` ✗ ${result.name} — ${String(result.error)}`);
|
|
2644
2658
|
}
|
|
2645
2659
|
if (results.length === 0) {
|
|
2646
|
-
console.log(
|
|
2660
|
+
console.log(' No plugins installed.');
|
|
2647
2661
|
return;
|
|
2648
2662
|
}
|
|
2649
2663
|
console.log();
|
|
2650
2664
|
if (hasErrors) {
|
|
2651
|
-
console.error(
|
|
2665
|
+
console.error('Completed with some errors.');
|
|
2652
2666
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2653
2667
|
}
|
|
2654
2668
|
else {
|
|
2655
|
-
console.log(
|
|
2669
|
+
console.log('✅ All plugins updated successfully.');
|
|
2656
2670
|
}
|
|
2657
2671
|
return;
|
|
2658
2672
|
}
|
|
2659
2673
|
try {
|
|
2660
2674
|
updatePlugin(name);
|
|
2661
2675
|
await discoverPlugins();
|
|
2662
|
-
console.log(
|
|
2676
|
+
console.log(`✅ Plugin "${name}" updated successfully.`);
|
|
2663
2677
|
}
|
|
2664
2678
|
catch (err) {
|
|
2665
|
-
console.error(
|
|
2679
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
2666
2680
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2667
2681
|
}
|
|
2668
2682
|
});
|
|
@@ -2674,8 +2688,8 @@ cli({
|
|
|
2674
2688
|
const { listPlugins } = await import('./plugin.js');
|
|
2675
2689
|
const plugins = listPlugins();
|
|
2676
2690
|
if (plugins.length === 0) {
|
|
2677
|
-
console.log(
|
|
2678
|
-
console.log(
|
|
2691
|
+
console.log(' No plugins installed.');
|
|
2692
|
+
console.log(' Install one with: opencli plugin install github:user/repo');
|
|
2679
2693
|
return;
|
|
2680
2694
|
}
|
|
2681
2695
|
if (opts.format === 'json') {
|
|
@@ -2688,7 +2702,7 @@ cli({
|
|
|
2688
2702
|
return;
|
|
2689
2703
|
}
|
|
2690
2704
|
console.log();
|
|
2691
|
-
console.log(
|
|
2705
|
+
console.log(' Installed plugins');
|
|
2692
2706
|
console.log();
|
|
2693
2707
|
// Group by monorepo
|
|
2694
2708
|
const standalone = plugins.filter((p) => !p.monorepoName);
|
|
@@ -2701,24 +2715,24 @@ cli({
|
|
|
2701
2715
|
monoGroups.set(p.monorepoName, g);
|
|
2702
2716
|
}
|
|
2703
2717
|
for (const p of standalone) {
|
|
2704
|
-
const version = p.version ?
|
|
2705
|
-
const desc = p.description ?
|
|
2706
|
-
const cmds = p.commands.length > 0 ?
|
|
2707
|
-
const src = p.source ?
|
|
2708
|
-
console.log(` ${
|
|
2718
|
+
const version = p.version ? ` @${p.version}` : '';
|
|
2719
|
+
const desc = p.description ? ` — ${p.description}` : '';
|
|
2720
|
+
const cmds = p.commands.length > 0 ? ` (${p.commands.join(', ')})` : '';
|
|
2721
|
+
const src = p.source ? ` ← ${p.source}` : '';
|
|
2722
|
+
console.log(` ${p.name}${version}${desc}${cmds}${src}`);
|
|
2709
2723
|
}
|
|
2710
2724
|
for (const [mono, group] of monoGroups) {
|
|
2711
2725
|
console.log();
|
|
2712
|
-
console.log(
|
|
2726
|
+
console.log(` 📦 ${mono}` + ' (monorepo)');
|
|
2713
2727
|
for (const p of group) {
|
|
2714
|
-
const version = p.version ?
|
|
2715
|
-
const desc = p.description ?
|
|
2716
|
-
const cmds = p.commands.length > 0 ?
|
|
2717
|
-
console.log(` ${
|
|
2728
|
+
const version = p.version ? ` @${p.version}` : '';
|
|
2729
|
+
const desc = p.description ? ` — ${p.description}` : '';
|
|
2730
|
+
const cmds = p.commands.length > 0 ? ` (${p.commands.join(', ')})` : '';
|
|
2731
|
+
console.log(` ${p.name}${version}${desc}${cmds}`);
|
|
2718
2732
|
}
|
|
2719
2733
|
}
|
|
2720
2734
|
console.log();
|
|
2721
|
-
console.log(
|
|
2735
|
+
console.log(` ${plugins.length} plugin(s) installed`);
|
|
2722
2736
|
console.log();
|
|
2723
2737
|
});
|
|
2724
2738
|
pluginCmd
|
|
@@ -2734,20 +2748,20 @@ cli({
|
|
|
2734
2748
|
dir: opts.dir,
|
|
2735
2749
|
description: opts.description,
|
|
2736
2750
|
});
|
|
2737
|
-
console.log(
|
|
2751
|
+
console.log(`✅ Plugin scaffold created at ${result.dir}`);
|
|
2738
2752
|
console.log();
|
|
2739
|
-
console.log(
|
|
2753
|
+
console.log(' Files created:');
|
|
2740
2754
|
for (const f of result.files) {
|
|
2741
|
-
console.log(` ${
|
|
2755
|
+
console.log(` ${f}`);
|
|
2742
2756
|
}
|
|
2743
2757
|
console.log();
|
|
2744
|
-
console.log(
|
|
2745
|
-
console.log(
|
|
2746
|
-
console.log(
|
|
2747
|
-
console.log(
|
|
2758
|
+
console.log(' Next steps:');
|
|
2759
|
+
console.log(` cd ${result.dir}`);
|
|
2760
|
+
console.log(` opencli plugin install file://${result.dir}`);
|
|
2761
|
+
console.log(` opencli ${name} hello`);
|
|
2748
2762
|
}
|
|
2749
2763
|
catch (err) {
|
|
2750
|
-
console.error(
|
|
2764
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
2751
2765
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2752
2766
|
}
|
|
2753
2767
|
});
|
|
@@ -2800,21 +2814,21 @@ cli({
|
|
|
2800
2814
|
await fs.promises.access(builtinSiteDir);
|
|
2801
2815
|
}
|
|
2802
2816
|
catch {
|
|
2803
|
-
console.error(
|
|
2817
|
+
console.error(`Error: Site "${site}" not found in official adapters.`);
|
|
2804
2818
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2805
2819
|
return;
|
|
2806
2820
|
}
|
|
2807
2821
|
try {
|
|
2808
2822
|
await fs.promises.access(userSiteDir);
|
|
2809
|
-
console.error(
|
|
2823
|
+
console.error(`Site "${site}" already exists in ~/.opencli/clis/. Use "opencli adapter reset ${site}" first to restore official version.`);
|
|
2810
2824
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2811
2825
|
return;
|
|
2812
2826
|
}
|
|
2813
2827
|
catch { /* good, doesn't exist yet */ }
|
|
2814
2828
|
fs.cpSync(builtinSiteDir, userSiteDir, { recursive: true });
|
|
2815
|
-
console.log(
|
|
2829
|
+
console.log(`✅ Ejected "${site}" to ~/.opencli/clis/${site}/`);
|
|
2816
2830
|
console.log('You can now edit the adapter files. Changes take effect immediately.');
|
|
2817
|
-
console.log(
|
|
2831
|
+
console.log('Note: Official updates to this adapter will overwrite your changes.');
|
|
2818
2832
|
});
|
|
2819
2833
|
adapterCmd
|
|
2820
2834
|
.command('reset')
|
|
@@ -2835,7 +2849,7 @@ cli({
|
|
|
2835
2849
|
for (const dir of dirs) {
|
|
2836
2850
|
fs.rmSync(path.join(userClisDir, dir.name), { recursive: true, force: true });
|
|
2837
2851
|
}
|
|
2838
|
-
console.log(
|
|
2852
|
+
console.log(`✅ Reset ${dirs.length} site(s). All adapters now use official baseline.`);
|
|
2839
2853
|
}
|
|
2840
2854
|
catch {
|
|
2841
2855
|
console.log('No local sites to reset.');
|
|
@@ -2843,7 +2857,7 @@ cli({
|
|
|
2843
2857
|
return;
|
|
2844
2858
|
}
|
|
2845
2859
|
if (!site) {
|
|
2846
|
-
console.error(
|
|
2860
|
+
console.error('Error: Please specify a site name or use --all.');
|
|
2847
2861
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2848
2862
|
return;
|
|
2849
2863
|
}
|
|
@@ -2852,14 +2866,14 @@ cli({
|
|
|
2852
2866
|
await fs.promises.access(userSiteDir);
|
|
2853
2867
|
}
|
|
2854
2868
|
catch {
|
|
2855
|
-
console.error(
|
|
2869
|
+
console.error(`Site "${site}" has no local override.`);
|
|
2856
2870
|
return;
|
|
2857
2871
|
}
|
|
2858
2872
|
const isOfficial = fs.existsSync(path.join(BUILTIN_CLIS, site));
|
|
2859
2873
|
fs.rmSync(userSiteDir, { recursive: true, force: true });
|
|
2860
|
-
console.log(
|
|
2874
|
+
console.log(isOfficial
|
|
2861
2875
|
? `✅ Reset "${site}". Now using official baseline.`
|
|
2862
|
-
: `✅ Removed custom site "${site}".`)
|
|
2876
|
+
: `✅ Removed custom site "${site}".`);
|
|
2863
2877
|
});
|
|
2864
2878
|
// ── Built-in: browser profile selection ──────────────────────────────────
|
|
2865
2879
|
const profileCmd = program.command('profile').description('Manage Browser Bridge Chrome profiles');
|
|
@@ -2873,26 +2887,26 @@ cli({
|
|
|
2873
2887
|
const config = loadProfileConfig();
|
|
2874
2888
|
const profiles = status?.profiles ?? [];
|
|
2875
2889
|
if (!status) {
|
|
2876
|
-
console.log(
|
|
2890
|
+
console.log('Daemon is not running. Run opencli doctor after opening Chrome.');
|
|
2877
2891
|
return;
|
|
2878
2892
|
}
|
|
2879
2893
|
if (isDaemonStale(status, PKG_VERSION) || !Array.isArray(status.profiles)) {
|
|
2880
|
-
console.log(
|
|
2881
|
-
console.log(
|
|
2894
|
+
console.log(`Daemon ${formatDaemonVersion(status)} is stale for CLI v${PKG_VERSION}.`);
|
|
2895
|
+
console.log('Run: opencli daemon restart');
|
|
2882
2896
|
return;
|
|
2883
2897
|
}
|
|
2884
2898
|
if (profiles.length === 0) {
|
|
2885
|
-
console.log(
|
|
2886
|
-
console.log(
|
|
2899
|
+
console.log('No Browser Bridge profiles connected.');
|
|
2900
|
+
console.log('Open a Chrome profile with the OpenCLI extension installed, then run opencli profile list again.');
|
|
2887
2901
|
return;
|
|
2888
2902
|
}
|
|
2889
2903
|
const knownContextIds = new Set(profiles.map((profile) => profile.contextId));
|
|
2890
|
-
console.log(
|
|
2904
|
+
console.log('Connected Browser Bridge profiles');
|
|
2891
2905
|
console.log();
|
|
2892
2906
|
for (const profile of profiles) {
|
|
2893
2907
|
const alias = aliasForContextId(config, profile.contextId);
|
|
2894
|
-
const defaultMark = config.defaultContextId === profile.contextId ?
|
|
2895
|
-
const aliasText = alias ? ` ${
|
|
2908
|
+
const defaultMark = config.defaultContextId === profile.contextId ? ' default' : '';
|
|
2909
|
+
const aliasText = alias ? ` ${alias}` : '';
|
|
2896
2910
|
const version = profile.extensionVersion ? ` v${profile.extensionVersion}` : ' version unknown';
|
|
2897
2911
|
console.log(` ${profile.contextId}${aliasText}${defaultMark} — connected${version}`);
|
|
2898
2912
|
}
|
|
@@ -2900,14 +2914,14 @@ cli({
|
|
|
2900
2914
|
.filter(([, contextId]) => !knownContextIds.has(contextId));
|
|
2901
2915
|
if (disconnectedAliases.length > 0 || (config.defaultContextId && !knownContextIds.has(config.defaultContextId))) {
|
|
2902
2916
|
console.log();
|
|
2903
|
-
console.log(
|
|
2917
|
+
console.log('Disconnected saved profiles:');
|
|
2904
2918
|
const shown = new Set();
|
|
2905
2919
|
for (const [alias, contextId] of disconnectedAliases) {
|
|
2906
2920
|
shown.add(contextId);
|
|
2907
|
-
console.log(
|
|
2921
|
+
console.log(` ${contextId} ${alias} — not connected`);
|
|
2908
2922
|
}
|
|
2909
2923
|
if (config.defaultContextId && !shown.has(config.defaultContextId) && !knownContextIds.has(config.defaultContextId)) {
|
|
2910
|
-
console.log(
|
|
2924
|
+
console.log(` ${config.defaultContextId} — default, not connected`);
|
|
2911
2925
|
}
|
|
2912
2926
|
}
|
|
2913
2927
|
});
|
|
@@ -2919,10 +2933,10 @@ cli({
|
|
|
2919
2933
|
.action((contextId, alias) => {
|
|
2920
2934
|
try {
|
|
2921
2935
|
renameProfile(contextId, alias);
|
|
2922
|
-
console.log(`Profile ${contextId} is now aliased as ${
|
|
2936
|
+
console.log(`Profile ${contextId} is now aliased as ${alias}.`);
|
|
2923
2937
|
}
|
|
2924
2938
|
catch (err) {
|
|
2925
|
-
console.error(
|
|
2939
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
2926
2940
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2927
2941
|
}
|
|
2928
2942
|
});
|
|
@@ -2933,10 +2947,10 @@ cli({
|
|
|
2933
2947
|
.action((profile) => {
|
|
2934
2948
|
try {
|
|
2935
2949
|
const config = setDefaultProfile(profile);
|
|
2936
|
-
console.log(`Default Browser Bridge profile: ${
|
|
2950
|
+
console.log(`Default Browser Bridge profile: ${config.defaultContextId ?? profile}`);
|
|
2937
2951
|
}
|
|
2938
2952
|
catch (err) {
|
|
2939
|
-
console.error(
|
|
2953
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
2940
2954
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2941
2955
|
}
|
|
2942
2956
|
});
|
|
@@ -2968,7 +2982,7 @@ cli({
|
|
|
2968
2982
|
.action((name) => {
|
|
2969
2983
|
const ext = externalClis.find(e => e.name === name);
|
|
2970
2984
|
if (!ext) {
|
|
2971
|
-
console.error(
|
|
2985
|
+
console.error(`External CLI '${name}' not found in registry.`);
|
|
2972
2986
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2973
2987
|
return;
|
|
2974
2988
|
}
|
|
@@ -3013,7 +3027,7 @@ cli({
|
|
|
3013
3027
|
executeExternalCli(name, args, externalClis);
|
|
3014
3028
|
}
|
|
3015
3029
|
catch (err) {
|
|
3016
|
-
console.error(
|
|
3030
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
3017
3031
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
3018
3032
|
}
|
|
3019
3033
|
}
|
|
@@ -3076,15 +3090,38 @@ cli({
|
|
|
3076
3090
|
program.configureHelp({
|
|
3077
3091
|
visibleCommands: (command) => command.commands.filter(child => command !== program || !adapterNameSet.has(child.name())),
|
|
3078
3092
|
});
|
|
3093
|
+
// When an ancestor command declares a leading positional via `.usage(...)`
|
|
3094
|
+
// (e.g. `browser` -> `<session> <command> [options]`), inject the positional
|
|
3095
|
+
// between that ancestor's name and the next path segment so the help Usage
|
|
3096
|
+
// line is accurate: `Usage: opencli browser <session> click [target] [options]`
|
|
3097
|
+
// instead of `opencli browser click [target] [options]`. Commander does NOT
|
|
3098
|
+
// inherit configureHelp into subcommands, so we walk the descendant tree and
|
|
3099
|
+
// apply the override on each.
|
|
3100
|
+
const ancestorAwareCommandUsage = (cmd) => {
|
|
3101
|
+
const ancestors = [];
|
|
3102
|
+
let ancestor = cmd.parent;
|
|
3103
|
+
while (ancestor) {
|
|
3104
|
+
const positional = leadingPositionalFromUsage(ancestor);
|
|
3105
|
+
ancestors.unshift(positional ? `${ancestor.name()} ${positional}` : ancestor.name());
|
|
3106
|
+
ancestor = ancestor.parent;
|
|
3107
|
+
}
|
|
3108
|
+
return [...ancestors, cmd.name(), cmd.usage()].filter(Boolean).join(' ').trim();
|
|
3109
|
+
};
|
|
3110
|
+
function applyAncestorAwareUsage(cmd) {
|
|
3111
|
+
cmd.configureHelp({ commandUsage: ancestorAwareCommandUsage });
|
|
3112
|
+
for (const sub of cmd.commands)
|
|
3113
|
+
applyAncestorAwareUsage(sub);
|
|
3114
|
+
}
|
|
3115
|
+
applyAncestorAwareUsage(browser);
|
|
3079
3116
|
installStructuredHelp(program, () => rootHelpData(program, adapterGroups), () => formatRootAdapterHelpText(adapterGroups));
|
|
3080
3117
|
// ── Unknown command fallback ──────────────────────────────────────────────
|
|
3081
3118
|
// Security: do NOT auto-discover and register arbitrary system binaries.
|
|
3082
3119
|
// Only explicitly registered external CLIs are allowed.
|
|
3083
3120
|
program.on('command:*', (operands) => {
|
|
3084
3121
|
const binary = operands[0];
|
|
3085
|
-
console.error(
|
|
3122
|
+
console.error(`error: unknown command '${binary}'`);
|
|
3086
3123
|
if (isBinaryInstalled(binary)) {
|
|
3087
|
-
console.error(
|
|
3124
|
+
console.error(` Tip: '${binary}' exists on your PATH. Use 'opencli external register ${binary}' to add it as an external CLI.`);
|
|
3088
3125
|
}
|
|
3089
3126
|
program.outputHelp();
|
|
3090
3127
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|