@jackwener/opencli 1.7.17 → 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.
Files changed (118) hide show
  1. package/README.md +10 -8
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +585 -9
  4. package/clis/ctrip/ctrip.test.js +486 -1
  5. package/clis/ctrip/flight.js +136 -0
  6. package/clis/ctrip/hotel-search.js +132 -0
  7. package/clis/ctrip/utils.js +298 -0
  8. package/clis/doubao/utils.js +17 -0
  9. package/clis/doubao/utils.test.js +61 -0
  10. package/clis/google/search.js +16 -6
  11. package/clis/google-scholar/search.js +20 -5
  12. package/clis/google-scholar/search.test.js +35 -2
  13. package/clis/reddit/home.js +117 -0
  14. package/clis/reddit/home.test.js +127 -0
  15. package/clis/reddit/read.js +400 -54
  16. package/clis/reddit/read.test.js +315 -12
  17. package/clis/reddit/reply.js +182 -0
  18. package/clis/reddit/reply.test.js +89 -0
  19. package/clis/reddit/subreddit-info.js +117 -0
  20. package/clis/reddit/subreddit-info.test.js +163 -0
  21. package/clis/reddit/whoami.js +84 -0
  22. package/clis/reddit/whoami.test.js +105 -0
  23. package/clis/rednote/comments.js +76 -0
  24. package/clis/rednote/download.js +59 -0
  25. package/clis/rednote/feed.js +95 -0
  26. package/clis/rednote/navigation.test.js +26 -0
  27. package/clis/rednote/note.js +68 -0
  28. package/clis/rednote/notifications.js +139 -0
  29. package/clis/rednote/rednote.test.js +157 -0
  30. package/clis/rednote/search.js +101 -0
  31. package/clis/rednote/user.js +55 -0
  32. package/clis/twitter/bookmark-folder.js +3 -1
  33. package/clis/twitter/bookmarks.js +3 -1
  34. package/clis/twitter/followers.js +20 -5
  35. package/clis/twitter/followers.test.js +44 -0
  36. package/clis/twitter/following.js +36 -20
  37. package/clis/twitter/following.test.js +60 -8
  38. package/clis/twitter/likes.js +28 -13
  39. package/clis/twitter/likes.test.js +111 -1
  40. package/clis/twitter/list-add.js +128 -204
  41. package/clis/twitter/list-add.test.js +97 -1
  42. package/clis/twitter/list-tweets.js +13 -4
  43. package/clis/twitter/list-tweets.test.js +48 -0
  44. package/clis/twitter/lists.js +5 -2
  45. package/clis/twitter/post.js +23 -4
  46. package/clis/twitter/post.test.js +30 -0
  47. package/clis/twitter/profile.js +16 -8
  48. package/clis/twitter/profile.test.js +39 -0
  49. package/clis/twitter/reply.js +133 -10
  50. package/clis/twitter/reply.test.js +55 -0
  51. package/clis/twitter/search.js +188 -170
  52. package/clis/twitter/search.test.js +96 -258
  53. package/clis/twitter/shared.js +167 -16
  54. package/clis/twitter/shared.test.js +102 -1
  55. package/clis/twitter/timeline.js +3 -1
  56. package/clis/twitter/tweets.js +147 -51
  57. package/clis/twitter/tweets.test.js +238 -1
  58. package/clis/xiaohongshu/comments.js +57 -26
  59. package/clis/xiaohongshu/comments.test.js +63 -1
  60. package/clis/xiaohongshu/download.js +32 -23
  61. package/clis/xiaohongshu/feed.js +23 -15
  62. package/clis/xiaohongshu/note-helpers.js +16 -6
  63. package/clis/xiaohongshu/note.js +26 -20
  64. package/clis/xiaohongshu/notifications.js +26 -19
  65. package/clis/xiaohongshu/search.js +201 -37
  66. package/clis/xiaohongshu/search.test.js +82 -8
  67. package/clis/xiaohongshu/user-helpers.js +13 -4
  68. package/clis/xiaohongshu/user-helpers.test.js +20 -0
  69. package/clis/xiaohongshu/user.js +9 -4
  70. package/clis/xueqiu/earnings-date.js +2 -2
  71. package/clis/xueqiu/kline.js +2 -2
  72. package/clis/xueqiu/utils.js +19 -0
  73. package/clis/xueqiu/utils.test.js +26 -0
  74. package/clis/youtube/transcript.js +28 -3
  75. package/clis/youtube/transcript.test.js +90 -1
  76. package/clis/zhihu/answer-detail.js +233 -0
  77. package/clis/zhihu/answer-detail.test.js +330 -0
  78. package/clis/zhihu/question.js +44 -10
  79. package/clis/zhihu/question.test.js +78 -1
  80. package/clis/zhihu/recommend.js +103 -0
  81. package/clis/zhihu/recommend.test.js +143 -0
  82. package/dist/src/browser/base-page.d.ts +3 -2
  83. package/dist/src/browser/base-page.test.js +2 -2
  84. package/dist/src/browser/cdp.js +3 -3
  85. package/dist/src/browser/page.d.ts +3 -2
  86. package/dist/src/browser/page.js +4 -4
  87. package/dist/src/browser/page.test.js +31 -0
  88. package/dist/src/browser/utils.d.ts +10 -0
  89. package/dist/src/browser/utils.js +37 -0
  90. package/dist/src/browser/utils.test.d.ts +1 -0
  91. package/dist/src/browser/utils.test.js +29 -0
  92. package/dist/src/cli-argv-preprocess.d.ts +37 -0
  93. package/dist/src/cli-argv-preprocess.js +131 -0
  94. package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
  95. package/dist/src/cli-argv-preprocess.test.js +130 -0
  96. package/dist/src/cli.js +123 -86
  97. package/dist/src/cli.test.js +32 -22
  98. package/dist/src/commands/daemon.js +6 -7
  99. package/dist/src/doctor.js +21 -17
  100. package/dist/src/doctor.test.js +2 -0
  101. package/dist/src/download/progress.js +15 -11
  102. package/dist/src/download/progress.test.d.ts +1 -0
  103. package/dist/src/download/progress.test.js +25 -0
  104. package/dist/src/execution.js +1 -3
  105. package/dist/src/execution.test.js +4 -16
  106. package/dist/src/help.d.ts +11 -0
  107. package/dist/src/help.js +46 -5
  108. package/dist/src/logger.js +8 -9
  109. package/dist/src/main.js +16 -0
  110. package/dist/src/output.js +4 -5
  111. package/dist/src/runtime-detect.d.ts +1 -1
  112. package/dist/src/runtime-detect.js +1 -1
  113. package/dist/src/runtime-detect.test.js +3 -2
  114. package/dist/src/tui.d.ts +0 -1
  115. package/dist/src/tui.js +9 -22
  116. package/dist/src/types.d.ts +3 -1
  117. package/dist/src/update-check.js +4 -5
  118. 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('--session <name> is required for opencli browser commands');
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(styleText('bold', ' opencli') + styleText('dim', ' — available commands'));
528
+ console.log(' opencli' + ' — available commands');
527
529
  console.log();
528
530
  for (const [site, cmds] of sites) {
529
- console.log(styleText(['bold', 'cyan'], ` ${site}`));
531
+ console.log(` ${site}`);
530
532
  for (const cmd of cmds) {
531
533
  const label = strategyLabel(cmd);
532
534
  const tag = label === 'public'
533
- ? styleText('green', '[public]')
534
- : styleText('yellow', `[${label}]`);
535
- const aliases = cmd.aliases?.length ? styleText('dim', ` (aliases: ${cmd.aliases.join(', ')})`) : '';
536
- console.log(` ${cmd.name} ${tag}${aliases}${cmd.description ? styleText('dim', ` — ${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(styleText(['bold', 'cyan'], ' external CLIs'));
544
+ console.log(' external CLIs');
543
545
  for (const ext of externalClis) {
544
546
  const isInstalled = isBinaryInstalled(ext.binary);
545
- const tag = isInstalled ? styleText('green', '[installed]') : styleText('yellow', '[auto-install]');
546
- console.log(` ${ext.name} ${tag}${ext.description ? styleText('dim', ` — ${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(styleText('dim', ` ${commands.length} built-in commands across ${sites.size} sites, ${externalClis.length} external CLIs`));
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
- .option('--session <name>', 'Browser session to use')
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
- .option('--session <name>', 'Browser session name to bind')
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
- .option('--session <name>', 'Browser session name to detach')
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(styleText('yellow', 'No plugins were installed (all skipped or incompatible).'));
2596
+ console.log('No plugins were installed (all skipped or incompatible).');
2583
2597
  }
2584
2598
  else {
2585
- console.log(styleText('green', `\u2705 Installed ${result.length} plugin(s) from monorepo: ${result.join(', ')}`));
2599
+ console.log(`\u2705 Installed ${result.length} plugin(s) from monorepo: ${result.join(', ')}`);
2586
2600
  }
2587
2601
  }
2588
2602
  else {
2589
- console.log(styleText('green', `\u2705 Plugin "${result}" installed successfully. Commands are ready to use.`));
2603
+ console.log(`\u2705 Plugin "${result}" installed successfully. Commands are ready to use.`);
2590
2604
  }
2591
2605
  }
2592
2606
  catch (err) {
2593
- console.error(styleText('red', `Error: ${getErrorMessage(err)}`));
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(styleText('green', `✅ Plugin "${name}" uninstalled.`));
2619
+ console.log(`✅ Plugin "${name}" uninstalled.`);
2606
2620
  }
2607
2621
  catch (err) {
2608
- console.error(styleText('red', `Error: ${getErrorMessage(err)}`));
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(styleText('red', 'Error: Please specify a plugin name or use the --all flag.'));
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(styleText('red', 'Error: Cannot specify both a plugin name and --all.'));
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(styleText('bold', ' Update Results:'));
2650
+ console.log(' Update Results:');
2637
2651
  for (const result of results) {
2638
2652
  if (result.success) {
2639
- console.log(` ${styleText('green', '')} ${result.name}`);
2653
+ console.log(` ✓ ${result.name}`);
2640
2654
  continue;
2641
2655
  }
2642
2656
  hasErrors = true;
2643
- console.log(` ${styleText('red', '')} ${result.name} — ${styleText('dim', String(result.error))}`);
2657
+ console.log(` ✗ ${result.name} — ${String(result.error)}`);
2644
2658
  }
2645
2659
  if (results.length === 0) {
2646
- console.log(styleText('dim', ' No plugins installed.'));
2660
+ console.log(' No plugins installed.');
2647
2661
  return;
2648
2662
  }
2649
2663
  console.log();
2650
2664
  if (hasErrors) {
2651
- console.error(styleText('red', 'Completed with some errors.'));
2665
+ console.error('Completed with some errors.');
2652
2666
  process.exitCode = EXIT_CODES.GENERIC_ERROR;
2653
2667
  }
2654
2668
  else {
2655
- console.log(styleText('green', '✅ All plugins updated successfully.'));
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(styleText('green', `✅ Plugin "${name}" updated successfully.`));
2676
+ console.log(`✅ Plugin "${name}" updated successfully.`);
2663
2677
  }
2664
2678
  catch (err) {
2665
- console.error(styleText('red', `Error: ${getErrorMessage(err)}`));
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(styleText('dim', ' No plugins installed.'));
2678
- console.log(styleText('dim', ' Install one with: opencli plugin install github:user/repo'));
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(styleText('bold', ' Installed plugins'));
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 ? styleText('green', ` @${p.version}`) : '';
2705
- const desc = p.description ? styleText('dim', ` — ${p.description}`) : '';
2706
- const cmds = p.commands.length > 0 ? styleText('dim', ` (${p.commands.join(', ')})`) : '';
2707
- const src = p.source ? styleText('dim', ` ← ${p.source}`) : '';
2708
- console.log(` ${styleText('cyan', p.name)}${version}${desc}${cmds}${src}`);
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(styleText(['bold', 'magenta'], ` 📦 ${mono}`) + styleText('dim', ' (monorepo)'));
2726
+ console.log(` 📦 ${mono}` + ' (monorepo)');
2713
2727
  for (const p of group) {
2714
- const version = p.version ? styleText('green', ` @${p.version}`) : '';
2715
- const desc = p.description ? styleText('dim', ` — ${p.description}`) : '';
2716
- const cmds = p.commands.length > 0 ? styleText('dim', ` (${p.commands.join(', ')})`) : '';
2717
- console.log(` ${styleText('cyan', p.name)}${version}${desc}${cmds}`);
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(styleText('dim', ` ${plugins.length} plugin(s) installed`));
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(styleText('green', `✅ Plugin scaffold created at ${result.dir}`));
2751
+ console.log(`✅ Plugin scaffold created at ${result.dir}`);
2738
2752
  console.log();
2739
- console.log(styleText('bold', ' Files created:'));
2753
+ console.log(' Files created:');
2740
2754
  for (const f of result.files) {
2741
- console.log(` ${styleText('cyan', f)}`);
2755
+ console.log(` ${f}`);
2742
2756
  }
2743
2757
  console.log();
2744
- console.log(styleText('dim', ' Next steps:'));
2745
- console.log(styleText('dim', ` cd ${result.dir}`));
2746
- console.log(styleText('dim', ` opencli plugin install file://${result.dir}`));
2747
- console.log(styleText('dim', ` opencli ${name} hello`));
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(styleText('red', `Error: ${getErrorMessage(err)}`));
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(styleText('red', `Error: Site "${site}" not found in official adapters.`));
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(styleText('yellow', `Site "${site}" already exists in ~/.opencli/clis/. Use "opencli adapter reset ${site}" first to restore official version.`));
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(styleText('green', `✅ Ejected "${site}" to ~/.opencli/clis/${site}/`));
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(styleText('yellow', 'Note: Official updates to this adapter will overwrite your changes.'));
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(styleText('green', `✅ Reset ${dirs.length} site(s). All adapters now use official baseline.`));
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(styleText('red', 'Error: Please specify a site name or use --all.'));
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(styleText('yellow', `Site "${site}" has no local override.`));
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(styleText('green', isOfficial
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(styleText('yellow', 'Daemon is not running. Run opencli doctor after opening Chrome.'));
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(styleText('yellow', `Daemon ${formatDaemonVersion(status)} is stale for CLI v${PKG_VERSION}.`));
2881
- console.log(styleText('dim', 'Run: opencli daemon restart'));
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(styleText('yellow', 'No Browser Bridge profiles connected.'));
2886
- console.log(styleText('dim', 'Open a Chrome profile with the OpenCLI extension installed, then run opencli profile list again.'));
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(styleText('bold', 'Connected Browser Bridge profiles'));
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 ? styleText('green', ' default') : '';
2895
- const aliasText = alias ? ` ${styleText('cyan', 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(styleText('dim', 'Disconnected saved profiles:'));
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(styleText('dim', ` ${contextId} ${alias} — not connected`));
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(styleText('dim', ` ${config.defaultContextId} — default, not connected`));
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 ${styleText('cyan', alias)}.`);
2936
+ console.log(`Profile ${contextId} is now aliased as ${alias}.`);
2923
2937
  }
2924
2938
  catch (err) {
2925
- console.error(styleText('red', `Error: ${getErrorMessage(err)}`));
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: ${styleText('cyan', config.defaultContextId ?? profile)}`);
2950
+ console.log(`Default Browser Bridge profile: ${config.defaultContextId ?? profile}`);
2937
2951
  }
2938
2952
  catch (err) {
2939
- console.error(styleText('red', `Error: ${getErrorMessage(err)}`));
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(styleText('red', `External CLI '${name}' not found in registry.`));
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(styleText('red', `Error: ${getErrorMessage(err)}`));
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(styleText('red', `error: unknown command '${binary}'`));
3122
+ console.error(`error: unknown command '${binary}'`);
3086
3123
  if (isBinaryInstalled(binary)) {
3087
- console.error(styleText('dim', ` Tip: '${binary}' exists on your PATH. Use 'opencli external register ${binary}' to add it as an external CLI.`));
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;