@jackwener/opencli 1.7.18 → 1.7.20

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 (120) hide show
  1. package/README.md +18 -17
  2. package/README.zh-CN.md +16 -18
  3. package/cli-manifest.json +311 -186
  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/google/search.js +16 -6
  9. package/clis/google-scholar/search.js +20 -5
  10. package/clis/google-scholar/search.test.js +35 -2
  11. package/clis/reddit/home.js +117 -0
  12. package/clis/reddit/home.test.js +127 -0
  13. package/clis/reddit/read.js +400 -54
  14. package/clis/reddit/read.test.js +315 -12
  15. package/clis/reddit/subreddit-info.js +117 -0
  16. package/clis/reddit/subreddit-info.test.js +163 -0
  17. package/clis/reddit/whoami.js +84 -0
  18. package/clis/reddit/whoami.test.js +105 -0
  19. package/clis/rednote/search.js +6 -2
  20. package/clis/twitter/bookmark-folder.js +8 -4
  21. package/clis/twitter/bookmark-folder.test.js +59 -1
  22. package/clis/twitter/bookmarks.js +12 -4
  23. package/clis/twitter/bookmarks.test.js +205 -0
  24. package/clis/twitter/followers.js +20 -5
  25. package/clis/twitter/followers.test.js +44 -0
  26. package/clis/twitter/following.js +36 -20
  27. package/clis/twitter/following.test.js +60 -8
  28. package/clis/twitter/likes.js +28 -13
  29. package/clis/twitter/likes.test.js +111 -1
  30. package/clis/twitter/list-add.js +128 -204
  31. package/clis/twitter/list-add.test.js +97 -1
  32. package/clis/twitter/list-tweets.js +13 -4
  33. package/clis/twitter/list-tweets.test.js +48 -0
  34. package/clis/twitter/lists.js +5 -2
  35. package/clis/twitter/post.js +23 -4
  36. package/clis/twitter/post.test.js +30 -0
  37. package/clis/twitter/profile.js +16 -8
  38. package/clis/twitter/profile.test.js +39 -0
  39. package/clis/twitter/reply.js +133 -10
  40. package/clis/twitter/reply.test.js +55 -0
  41. package/clis/twitter/search.js +188 -170
  42. package/clis/twitter/search.test.js +96 -258
  43. package/clis/twitter/shared.js +167 -16
  44. package/clis/twitter/shared.test.js +102 -1
  45. package/clis/twitter/timeline.js +3 -1
  46. package/clis/twitter/tweets.js +147 -51
  47. package/clis/twitter/tweets.test.js +238 -1
  48. package/clis/xiaohongshu/comments.js +23 -2
  49. package/clis/xiaohongshu/comments.test.js +63 -1
  50. package/clis/xiaohongshu/search.js +168 -13
  51. package/clis/xiaohongshu/search.test.js +82 -8
  52. package/clis/xueqiu/earnings-date.js +2 -2
  53. package/clis/xueqiu/kline.js +2 -2
  54. package/clis/xueqiu/utils.js +19 -0
  55. package/clis/xueqiu/utils.test.js +26 -0
  56. package/clis/zhihu/answer-detail.js +233 -0
  57. package/clis/zhihu/answer-detail.test.js +330 -0
  58. package/clis/zhihu/question.js +44 -10
  59. package/clis/zhihu/question.test.js +78 -1
  60. package/clis/zhihu/recommend.js +103 -0
  61. package/clis/zhihu/recommend.test.js +143 -0
  62. package/dist/src/browser/base-page.d.ts +3 -2
  63. package/dist/src/browser/base-page.test.js +2 -2
  64. package/dist/src/browser/cdp.js +3 -3
  65. package/dist/src/browser/daemon-client.d.ts +1 -0
  66. package/dist/src/browser/daemon-client.js +3 -0
  67. package/dist/src/browser/daemon-client.test.js +20 -0
  68. package/dist/src/browser/page.d.ts +3 -2
  69. package/dist/src/browser/page.js +4 -4
  70. package/dist/src/browser/page.test.js +31 -0
  71. package/dist/src/browser/utils.d.ts +10 -0
  72. package/dist/src/browser/utils.js +37 -0
  73. package/dist/src/browser/utils.test.d.ts +1 -0
  74. package/dist/src/browser/utils.test.js +29 -0
  75. package/dist/src/cli-argv-preprocess.d.ts +37 -0
  76. package/dist/src/cli-argv-preprocess.js +131 -0
  77. package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
  78. package/dist/src/cli-argv-preprocess.test.js +130 -0
  79. package/dist/src/cli.js +131 -89
  80. package/dist/src/cli.test.js +34 -28
  81. package/dist/src/commands/daemon.js +6 -7
  82. package/dist/src/daemon-utils.d.ts +18 -0
  83. package/dist/src/daemon-utils.js +37 -0
  84. package/dist/src/daemon.d.ts +1 -1
  85. package/dist/src/daemon.js +44 -13
  86. package/dist/src/daemon.test.js +42 -1
  87. package/dist/src/doctor.js +15 -16
  88. package/dist/src/download/progress.js +15 -11
  89. package/dist/src/download/progress.test.d.ts +1 -0
  90. package/dist/src/download/progress.test.js +25 -0
  91. package/dist/src/electron-apps.js +0 -1
  92. package/dist/src/electron-apps.test.js +1 -0
  93. package/dist/src/execution.js +1 -3
  94. package/dist/src/execution.test.js +4 -16
  95. package/dist/src/external-clis.yaml +12 -3
  96. package/dist/src/external.d.ts +4 -0
  97. package/dist/src/external.js +3 -0
  98. package/dist/src/external.test.js +24 -1
  99. package/dist/src/help.d.ts +16 -1
  100. package/dist/src/help.js +50 -8
  101. package/dist/src/help.test.js +5 -1
  102. package/dist/src/logger.js +8 -9
  103. package/dist/src/main.js +16 -0
  104. package/dist/src/output.js +4 -5
  105. package/dist/src/runtime-detect.d.ts +1 -1
  106. package/dist/src/runtime-detect.js +1 -1
  107. package/dist/src/runtime-detect.test.js +3 -2
  108. package/dist/src/tui.d.ts +0 -1
  109. package/dist/src/tui.js +9 -22
  110. package/dist/src/types.d.ts +3 -1
  111. package/dist/src/update-check.js +4 -5
  112. package/package.json +5 -4
  113. package/clis/notion/export.js +0 -32
  114. package/clis/notion/favorites.js +0 -85
  115. package/clis/notion/new.js +0 -35
  116. package/clis/notion/read.js +0 -31
  117. package/clis/notion/search.js +0 -47
  118. package/clis/notion/sidebar.js +0 -42
  119. package/clis/notion/status.js +0 -17
  120. package/clis/notion/write.js +0 -41
@@ -370,13 +370,11 @@ describe('executeCommand — non-browser timeout', () => {
370
370
  expect(closeWindow).toHaveBeenCalledTimes(1);
371
371
  vi.restoreAllMocks();
372
372
  });
373
- it('skips closeWindow when OPENCLI_KEEP_TAB=true (success path)', async () => {
373
+ it('skips closeWindow when --keep-tab=true (success path)', async () => {
374
374
  const closeWindow = vi.fn().mockResolvedValue(undefined);
375
375
  const mockPage = { closeWindow };
376
376
  vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
377
377
  vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
378
- const prev = process.env.OPENCLI_KEEP_TAB;
379
- process.env.OPENCLI_KEEP_TAB = 'true';
380
378
  try {
381
379
  const cmd = cli({
382
380
  site: 'test-execution',
@@ -386,24 +384,18 @@ describe('executeCommand — non-browser timeout', () => {
386
384
  strategy: Strategy.PUBLIC,
387
385
  func: async () => [{ ok: true }],
388
386
  });
389
- await executeCommand(cmd, {});
387
+ await executeCommand(cmd, {}, false, { keepTab: 'true' });
390
388
  expect(closeWindow).not.toHaveBeenCalled();
391
389
  }
392
390
  finally {
393
- if (prev === undefined)
394
- delete process.env.OPENCLI_KEEP_TAB;
395
- else
396
- process.env.OPENCLI_KEEP_TAB = prev;
397
391
  vi.restoreAllMocks();
398
392
  }
399
393
  });
400
- it('skips closeWindow when OPENCLI_KEEP_TAB=true (failure path)', async () => {
394
+ it('skips closeWindow when --keep-tab=true (failure path)', async () => {
401
395
  const closeWindow = vi.fn().mockResolvedValue(undefined);
402
396
  const mockPage = { closeWindow };
403
397
  vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
404
398
  vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
405
- const prev = process.env.OPENCLI_KEEP_TAB;
406
- process.env.OPENCLI_KEEP_TAB = 'true';
407
399
  try {
408
400
  const cmd = cli({
409
401
  site: 'test-execution',
@@ -413,14 +405,10 @@ describe('executeCommand — non-browser timeout', () => {
413
405
  strategy: Strategy.PUBLIC,
414
406
  func: async () => { throw new Error('adapter failure'); },
415
407
  });
416
- await expect(executeCommand(cmd, {})).rejects.toThrow('adapter failure');
408
+ await expect(executeCommand(cmd, {}, false, { keepTab: 'true' })).rejects.toThrow('adapter failure');
417
409
  expect(closeWindow).not.toHaveBeenCalled();
418
410
  }
419
411
  finally {
420
- if (prev === undefined)
421
- delete process.env.OPENCLI_KEEP_TAB;
422
- else
423
- process.env.OPENCLI_KEEP_TAB = prev;
424
412
  vi.restoreAllMocks();
425
413
  }
426
414
  });
@@ -14,6 +14,12 @@
14
14
  install:
15
15
  mac: "brew install --cask obsidian"
16
16
 
17
+ - name: ntn
18
+ binary: ntn
19
+ description: "Notion CLI — official Notion API CLI for pages, databases, blocks, search, comments"
20
+ homepage: "https://ntn.dev"
21
+ tags: [notion, notes, knowledge, productivity]
22
+
17
23
  - name: docker
18
24
  binary: docker
19
25
  description: "Docker command-line interface"
@@ -55,24 +61,27 @@
55
61
  install:
56
62
  default: "npm install -g vercel"
57
63
 
58
- - name: tg-cli
64
+ - name: tg
59
65
  binary: tg
66
+ package: tg-cli
60
67
  description: "Telegram CLI — local-first sync, search, export via MTProto for AI agents"
61
68
  homepage: "https://github.com/jackwener/tg-cli"
62
69
  tags: [telegram, messaging, search, export, ai-agent]
63
70
  install:
64
71
  default: "uv tool install kabi-tg-cli"
65
72
 
66
- - name: discord-cli
73
+ - name: discord
67
74
  binary: discord
75
+ package: discord-cli
68
76
  description: "Discord CLI — local-first sync, search, export via SQLite for AI agents"
69
77
  homepage: "https://github.com/jackwener/discord-cli"
70
78
  tags: [discord, messaging, search, export, ai-agent]
71
79
  install:
72
80
  default: "uv tool install kabi-discord-cli"
73
81
 
74
- - name: wx-cli
82
+ - name: wx
75
83
  binary: wx
84
+ package: wx-cli
76
85
  description: "WeChat local data CLI — sessions, messages, search, contacts, export for AI agents"
77
86
  homepage: "https://github.com/jackwener/wx-cli"
78
87
  tags: [wechat, messaging, search, export, ai-agent]
@@ -5,8 +5,11 @@ export interface ExternalCliInstall {
5
5
  default?: string;
6
6
  }
7
7
  export interface ExternalCliConfig {
8
+ /** User-facing OpenCLI subcommand and, by default, the executable name. */
8
9
  name: string;
9
10
  binary: string;
11
+ /** Distribution/project name when it differs from the executable name. */
12
+ package?: string;
10
13
  description?: string;
11
14
  homepage?: string;
12
15
  tags?: string[];
@@ -15,6 +18,7 @@ export interface ExternalCliConfig {
15
18
  export declare function loadExternalClis(): ExternalCliConfig[];
16
19
  export declare function isBinaryInstalled(binary: string): boolean;
17
20
  export declare function getInstallCmd(installConfig?: ExternalCliInstall): string | null;
21
+ export declare function formatExternalCliLabel(cli: ExternalCliConfig): string;
18
22
  /**
19
23
  * Safely parses a command string into a binary and argument list.
20
24
  * Rejects commands containing shell operators (&&, ||, |, ;, >, <, `) that
@@ -70,6 +70,9 @@ export function getInstallCmd(installConfig) {
70
70
  return installConfig.default;
71
71
  return null;
72
72
  }
73
+ export function formatExternalCliLabel(cli) {
74
+ return cli.package && cli.package !== cli.name ? `${cli.name}(${cli.package})` : cli.name;
75
+ }
73
76
  /**
74
77
  * Safely parses a command string into a binary and argument list.
75
78
  * Rejects commands containing shell operators (&&, ||, |, ;, >, <, `) that
@@ -1,4 +1,8 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import yaml from 'js-yaml';
2
6
  const { mockExecFileSync, mockPlatform } = vi.hoisted(() => ({
3
7
  mockExecFileSync: vi.fn(),
4
8
  mockPlatform: vi.fn(() => 'darwin'),
@@ -14,7 +18,8 @@ vi.mock('node:os', async () => {
14
18
  platform: mockPlatform,
15
19
  };
16
20
  });
17
- import { installExternalCli, parseCommand } from './external.js';
21
+ import { formatExternalCliLabel, installExternalCli, parseCommand } from './external.js';
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
23
  describe('parseCommand', () => {
19
24
  it('splits binaries and quoted arguments without invoking a shell', () => {
20
25
  expect(parseCommand('npm install -g "@scope/tool name"')).toEqual({
@@ -29,6 +34,24 @@ describe('parseCommand', () => {
29
34
  expect(() => parseCommand('brew install $(whoami)')).toThrow('Install command contains unsafe shell operators');
30
35
  expect(() => parseCommand('brew install gh\nrm -rf /')).toThrow('Install command contains unsafe shell operators');
31
36
  });
37
+ it('keeps built-in install commands compatible with the shell-free parser', () => {
38
+ const raw = fs.readFileSync(path.join(__dirname, 'external-clis.yaml'), 'utf8');
39
+ const entries = (yaml.load(raw) || []);
40
+ for (const entry of entries) {
41
+ for (const command of Object.values(entry.install ?? {})) {
42
+ if (command)
43
+ expect(() => parseCommand(command)).not.toThrow();
44
+ }
45
+ }
46
+ });
47
+ });
48
+ describe('formatExternalCliLabel', () => {
49
+ it('shows the package name when the executable name differs', () => {
50
+ expect(formatExternalCliLabel({ name: 'wx', binary: 'wx', package: 'wx-cli' })).toBe('wx(wx-cli)');
51
+ });
52
+ it('keeps the label compact when package and name match', () => {
53
+ expect(formatExternalCliLabel({ name: 'docker', binary: 'docker', package: 'docker' })).toBe('docker');
54
+ });
32
55
  });
33
56
  describe('installExternalCli', () => {
34
57
  const cli = {
@@ -40,13 +40,28 @@ export type AdapterKind = 'site' | 'app';
40
40
  export declare function classifyAdapter(domain: string | undefined): AdapterKind;
41
41
  export interface RootAdapterGroups {
42
42
  /** Externally-registered CLIs (docker, gh, vercel, ...) — passthrough binaries */
43
- external: readonly string[];
43
+ external: readonly RootExternalCli[];
44
44
  /** Desktop-app adapters (chatgpt-app, chatwise, codex, ...) */
45
45
  apps: readonly string[];
46
46
  /** Web-site adapters (bilibili, dianping, ...) */
47
47
  sites: readonly string[];
48
48
  }
49
+ export interface RootExternalCli {
50
+ name: string;
51
+ label: string;
52
+ }
49
53
  export declare function formatRootAdapterHelpText(groups: RootAdapterGroups): string;
54
+ /**
55
+ * Extracts a positional placeholder that should appear immediately after this
56
+ * command's name in user-facing path strings. Reads the leading positional
57
+ * (e.g. `<session>`) from a `.usage()` override; commands without a positional
58
+ * override return `null` so the path stays as-is.
59
+ *
60
+ * Example: `browser` declares `.usage('<session> <command> [options]')`,
61
+ * so `commanderPath(browserClickCmd)` becomes
62
+ * `['opencli', 'browser', '<session>', 'click']`.
63
+ */
64
+ export declare function leadingPositionalFromUsage(command: Command): string | null;
50
65
  export declare function commanderNamespaceHelpData(namespaceRoot: Command, opts?: {
51
66
  globalCommand?: Command;
52
67
  description?: string;
package/dist/src/help.js CHANGED
@@ -116,7 +116,7 @@ export function formatRootAdapterHelpText(groups) {
116
116
  if (total === 0)
117
117
  return '';
118
118
  const lines = [''];
119
- lines.push(...formatGroupSection('External CLIs', groups.external));
119
+ lines.push(...formatGroupSection('External CLIs', groups.external.map(cli => cli.label)));
120
120
  lines.push(...formatGroupSection('App adapters', groups.apps));
121
121
  lines.push(...formatGroupSection('Site adapters', groups.sites));
122
122
  lines.push("Run 'opencli list' for full command details, or 'opencli <site> --help' to inspect one site.");
@@ -175,13 +175,42 @@ function compactCommanderOptions(options) {
175
175
  .map(compactCommanderOption)
176
176
  .filter((option) => option !== null);
177
177
  }
178
+ /**
179
+ * Extracts a positional placeholder that should appear immediately after this
180
+ * command's name in user-facing path strings. Reads the leading positional
181
+ * (e.g. `<session>`) from a `.usage()` override; commands without a positional
182
+ * override return `null` so the path stays as-is.
183
+ *
184
+ * Example: `browser` declares `.usage('<session> <command> [options]')`,
185
+ * so `commanderPath(browserClickCmd)` becomes
186
+ * `['opencli', 'browser', '<session>', 'click']`.
187
+ */
188
+ export function leadingPositionalFromUsage(command) {
189
+ const usage = command._usage;
190
+ if (!usage)
191
+ return null;
192
+ const match = usage.match(/^\s*(<[^>]+>)/);
193
+ return match ? match[1] : null;
194
+ }
178
195
  function commanderPath(command) {
179
196
  const parts = [];
180
197
  let current = command;
181
198
  while (current) {
182
199
  const name = current.name();
183
- if (name)
200
+ if (name) {
184
201
  parts.push(name);
202
+ // If this command declares a leading-positional usage override AND we
203
+ // have already collected a child name below it, the positional must
204
+ // appear between this command and the child (i.e. before the names
205
+ // already collected). parts is in reverse order, so push to the end.
206
+ const positional = leadingPositionalFromUsage(current);
207
+ if (positional && parts.length > 1) {
208
+ // We collected child names first (reverse order). Move them up by one
209
+ // and put the positional at index `parts.length - 2` so reverse()
210
+ // places it between this command and the first child name.
211
+ parts.splice(parts.length - 1, 0, positional);
212
+ }
213
+ }
185
214
  current = current.parent;
186
215
  }
187
216
  return parts.reverse();
@@ -189,7 +218,10 @@ function commanderPath(command) {
189
218
  function commandPathFromRoot(namespaceRoot, command) {
190
219
  const rootPath = commanderPath(namespaceRoot);
191
220
  const commandPath = commanderPath(command);
192
- return commandPath.slice(rootPath.length);
221
+ // Strip placeholder positional segments (e.g. `<session>`) from the relative
222
+ // name so agents can still address subcommands by their leaf name. Display
223
+ // paths in `command` / `usage` still include the placeholders.
224
+ return commandPath.slice(rootPath.length).filter(part => !/^<.+>$/.test(part));
193
225
  }
194
226
  function collectLeafCommands(command) {
195
227
  if (command.commands.length === 0)
@@ -232,10 +264,19 @@ export function commanderNamespaceHelpData(namespaceRoot, opts = {}) {
232
264
  const leaves = collectLeafCommands(namespaceRoot)
233
265
  .filter(command => command !== namespaceRoot)
234
266
  .sort((a, b) => commandPathFromRoot(namespaceRoot, a).join(' ').localeCompare(commandPathFromRoot(namespaceRoot, b).join(' ')));
267
+ // Respect commander's `.usage()` override (e.g. `<session> <command> [options]`
268
+ // on `browser`); fall back to the generic `<command> [args] [options]` form.
269
+ // Read the private `_usage` field directly because `.usage()` returns the
270
+ // auto-generated form if no override was set.
271
+ const commandPath = commanderPath(namespaceRoot).join(' ');
272
+ const usageOverride = namespaceRoot._usage;
273
+ const usage = usageOverride
274
+ ? `${commandPath} ${usageOverride}`
275
+ : `${commandPath} <command> [args] [options]`;
235
276
  return {
236
277
  namespace: namespaceRoot.name(),
237
- command: commanderPath(namespaceRoot).join(' '),
238
- usage: `${commanderPath(namespaceRoot).join(' ')} <command> [args] [options]`,
278
+ command: commandPath,
279
+ usage,
239
280
  description: opts.description ?? namespaceRoot.description(),
240
281
  command_count: leaves.length,
241
282
  commands: leaves.map(command => compactCommanderCommand(namespaceRoot, command, opts)),
@@ -243,7 +284,7 @@ export function commanderNamespaceHelpData(namespaceRoot, opts = {}) {
243
284
  ...(opts.globalCommand ? { global_options: compactCommanderOptions(opts.globalCommand.options) } : {}),
244
285
  structured_help: {
245
286
  formats: ['yaml', 'json'],
246
- usage: `${commanderPath(namespaceRoot).join(' ')} --help -f yaml`,
287
+ usage: `${commandPath} --help -f yaml`,
247
288
  },
248
289
  };
249
290
  }
@@ -335,7 +376,7 @@ function compactCommand(cmd) {
335
376
  };
336
377
  }
337
378
  export function rootHelpData(program, groups) {
338
- const adapterNames = new Set([...groups.external, ...groups.apps, ...groups.sites]);
379
+ const adapterNames = new Set([...groups.external.map(cli => cli.name), ...groups.apps, ...groups.sites]);
339
380
  const commands = program.commands
340
381
  .filter(command => !adapterNames.has(command.name()))
341
382
  .map(command => ({
@@ -349,7 +390,8 @@ export function rootHelpData(program, groups) {
349
390
  commands,
350
391
  external_clis: {
351
392
  count: groups.external.length,
352
- clis: [...groups.external].sort(sortLocale),
393
+ clis: groups.external.map(cli => cli.name).sort(sortLocale),
394
+ display: groups.external.map(cli => cli.label).sort(sortLocale),
353
395
  },
354
396
  app_adapters: {
355
397
  count: groups.apps.length,
@@ -20,13 +20,17 @@ describe('classifyAdapter', () => {
20
20
  describe('formatRootAdapterHelpText', () => {
21
21
  it('renders all three sections in External / App / Site order when populated', () => {
22
22
  const text = formatRootAdapterHelpText({
23
- external: ['gh', 'docker'],
23
+ external: [
24
+ { name: 'gh', label: 'gh' },
25
+ { name: 'wx', label: 'wx(wx-cli)' },
26
+ ],
24
27
  apps: ['chatwise', 'codex'],
25
28
  sites: ['bilibili'],
26
29
  });
27
30
  expect(text).toContain('External CLIs (2):');
28
31
  expect(text).toContain('App adapters (2):');
29
32
  expect(text).toContain('Site adapters (1):');
33
+ expect(text).toContain('wx(wx-cli)');
30
34
  expect(text.indexOf('External CLIs')).toBeLessThan(text.indexOf('App adapters'));
31
35
  expect(text.indexOf('App adapters')).toBeLessThan(text.indexOf('Site adapters'));
32
36
  });
@@ -4,35 +4,34 @@
4
4
  * All framework output (warnings, debug info, errors) should go through
5
5
  * this module so that verbosity levels are respected consistently.
6
6
  */
7
- import { styleText } from 'node:util';
8
7
  function isVerbose() {
9
8
  return !!process.env.OPENCLI_VERBOSE;
10
9
  }
11
10
  export const log = {
12
11
  /** Informational message (always shown) */
13
12
  info(msg) {
14
- process.stderr.write(`${styleText('blue', 'ℹ')} ${msg}\n`);
13
+ process.stderr.write(`ℹ ${msg}\n`);
15
14
  },
16
15
  /** Lightweight status line for adapter progress updates */
17
16
  status(msg) {
18
- process.stderr.write(`${styleText('dim', msg)}\n`);
17
+ process.stderr.write(`${msg}\n`);
19
18
  },
20
19
  /** Positive completion/status line without the heavier info prefix */
21
20
  success(msg) {
22
- process.stderr.write(`${styleText('green', msg)}\n`);
21
+ process.stderr.write(`${msg}\n`);
23
22
  },
24
23
  /** Warning (always shown) */
25
24
  warn(msg) {
26
- process.stderr.write(`${styleText('yellow', '⚠')} ${msg}\n`);
25
+ process.stderr.write(`⚠ ${msg}\n`);
27
26
  },
28
27
  /** Error (always shown) */
29
28
  error(msg) {
30
- process.stderr.write(`${styleText('red', '✖')} ${msg}\n`);
29
+ process.stderr.write(`✖ ${msg}\n`);
31
30
  },
32
31
  /** Verbose output (shown when -v flag or OPENCLI_VERBOSE is set) */
33
32
  verbose(msg) {
34
33
  if (isVerbose()) {
35
- process.stderr.write(`${styleText('dim', '[verbose]')} ${msg}\n`);
34
+ process.stderr.write(`[verbose] ${msg}\n`);
36
35
  }
37
36
  },
38
37
  /** Alias for verbose output. */
@@ -41,10 +40,10 @@ export const log = {
41
40
  },
42
41
  /** Step-style debug (for pipeline steps, etc.) */
43
42
  step(stepNum, total, op, preview = '') {
44
- process.stderr.write(` ${styleText('dim', `[${stepNum}/${total}]`)} ${styleText(['bold', 'cyan'], op)}${preview}\n`);
43
+ process.stderr.write(` [${stepNum}/${total}] ${op}${preview}\n`);
45
44
  },
46
45
  /** Step result summary */
47
46
  stepResult(summary) {
48
- process.stderr.write(` ${styleText('dim', `→ ${summary}`)}\n`);
47
+ process.stderr.write(` ${summary}\n`);
49
48
  },
50
49
  };
package/dist/src/main.js CHANGED
@@ -139,5 +139,21 @@ if (getCompIdx !== -1) {
139
139
  process.stdout.write(candidates.join('\n') + '\n');
140
140
  process.exit(EXIT_CODES.SUCCESS);
141
141
  }
142
+ // Rewrite `opencli browser <session> <subcommand> ...` so commander (which
143
+ // can't combine a parent positional with subcommand dispatch) sees the internal
144
+ // `--session <name>` flag form. Also refuses the retired `opencli browser
145
+ // --session foo ...` user form with a friendly usage error.
146
+ const { rewriteBrowserArgv, BrowserSessionArgvError } = await import('./cli-argv-preprocess.js');
147
+ try {
148
+ const rewritten = rewriteBrowserArgv(process.argv.slice(2));
149
+ process.argv.splice(2, process.argv.length - 2, ...rewritten);
150
+ }
151
+ catch (err) {
152
+ if (err instanceof BrowserSessionArgvError) {
153
+ process.stderr.write(`error: ${err.message}\n`);
154
+ process.exit(EXIT_CODES.GENERIC_ERROR);
155
+ }
156
+ throw err;
157
+ }
142
158
  await emitHook('onStartup', { command: '__startup__', args: {} });
143
159
  runCli(BUILTIN_CLIS, USER_CLIS);
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Output formatting: table, JSON, Markdown, CSV, YAML.
3
3
  */
4
- import { styleText } from 'node:util';
5
4
  import Table from 'cli-table3';
6
5
  import yaml from 'js-yaml';
7
6
  function normalizeRows(data) {
@@ -51,13 +50,13 @@ export function render(data, opts = {}) {
51
50
  function renderTable(data, opts) {
52
51
  const rows = normalizeRows(data);
53
52
  if (!rows.length) {
54
- console.log(styleText('dim', '(no data)'));
53
+ console.log('(no data)');
55
54
  return;
56
55
  }
57
56
  const columns = resolveColumns(rows, opts);
58
57
  const header = columns.map(c => capitalize(c));
59
58
  const table = new Table({
60
- head: header.map(h => styleText('bold', h)),
59
+ head: header.map(h => h),
61
60
  style: { head: [], border: [] },
62
61
  wordWrap: true,
63
62
  wrapOnWordBoundary: true,
@@ -70,7 +69,7 @@ function renderTable(data, opts) {
70
69
  }
71
70
  console.log();
72
71
  if (opts.title)
73
- console.log(styleText('dim', ` ${opts.title}`));
72
+ console.log(` ${opts.title}`);
74
73
  console.log(table.toString());
75
74
  const footer = [];
76
75
  footer.push(`${rows.length} items`);
@@ -80,7 +79,7 @@ function renderTable(data, opts) {
80
79
  footer.push(opts.source);
81
80
  if (opts.footerExtra)
82
81
  footer.push(opts.footerExtra);
83
- console.log(styleText('dim', footer.join(' · ')));
82
+ console.log(footer.join(' · '));
84
83
  }
85
84
  function renderJson(data) {
86
85
  console.log(JSON.stringify(data, null, 2));
@@ -6,7 +6,7 @@
6
6
  * (e.g. logging, diagnostics) without littering runtime sniffing everywhere.
7
7
  */
8
8
  export type Runtime = 'bun' | 'node';
9
- export declare const MIN_SUPPORTED_NODE_MAJOR = 21;
9
+ export declare const MIN_SUPPORTED_NODE_MAJOR = 20;
10
10
  /**
11
11
  * Detect the current JavaScript runtime.
12
12
  */
@@ -5,7 +5,7 @@
5
5
  * This module centralises the check so other code can adapt behaviour
6
6
  * (e.g. logging, diagnostics) without littering runtime sniffing everywhere.
7
7
  */
8
- export const MIN_SUPPORTED_NODE_MAJOR = 21;
8
+ export const MIN_SUPPORTED_NODE_MAJOR = 20;
9
9
  /**
10
10
  * Detect the current JavaScript runtime.
11
11
  */
@@ -30,8 +30,9 @@ describe('runtime-detect', () => {
30
30
  expect(parseNodeMajor('bun-1.2.0')).toBeNull();
31
31
  });
32
32
  it('checks the current minimum supported Node major version', () => {
33
- expect(MIN_SUPPORTED_NODE_MAJOR).toBe(21);
34
- expect(isSupportedNodeVersion('v20.18.0')).toBe(false);
33
+ expect(MIN_SUPPORTED_NODE_MAJOR).toBe(20);
34
+ expect(isSupportedNodeVersion('v19.9.0')).toBe(false);
35
+ expect(isSupportedNodeVersion('v20.0.0')).toBe(true);
35
36
  expect(isSupportedNodeVersion('v21.0.0')).toBe(true);
36
37
  expect(isSupportedNodeVersion('v25.0.0')).toBe(true);
37
38
  });
package/dist/src/tui.d.ts CHANGED
@@ -4,7 +4,6 @@ export interface CheckboxItem {
4
4
  checked: boolean;
5
5
  /** Optional status to display after the label */
6
6
  status?: string;
7
- statusColor?: 'green' | 'yellow' | 'red' | 'dim';
8
7
  }
9
8
  /**
10
9
  * Interactive multi-select checkbox prompt.
package/dist/src/tui.js CHANGED
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Uses raw stdin mode + ANSI escape codes for interactive prompts.
5
5
  */
6
- import { styleText } from 'node:util';
7
6
  import { EXIT_CODES } from './errors.js';
8
7
  /**
9
8
  * Interactive multi-select checkbox prompt.
@@ -25,32 +24,20 @@ export async function checkboxPrompt(items, opts = {}) {
25
24
  }
26
25
  let cursor = 0;
27
26
  const state = items.map(i => ({ ...i }));
28
- function colorStatus(status, color) {
29
- if (!status)
30
- return '';
31
- switch (color) {
32
- case 'green': return styleText('green', status);
33
- case 'yellow': return styleText('yellow', status);
34
- case 'red': return styleText('red', status);
35
- case 'dim': return styleText('dim', status);
36
- default: return styleText('dim', status);
37
- }
38
- }
39
27
  function render() {
40
28
  // Move cursor to start and clear
41
29
  let out = '';
42
30
  if (opts.title) {
43
- out += `\n${styleText('bold', opts.title)}\n\n`;
31
+ out += `\n${opts.title}\n\n`;
44
32
  }
45
33
  for (let i = 0; i < state.length; i++) {
46
34
  const item = state[i];
47
- const pointer = i === cursor ? styleText('cyan', '❯') : ' ';
48
- const checkbox = item.checked ? styleText('green', '◉') : styleText('dim', '○');
49
- const label = i === cursor ? styleText('bold', item.label) : item.label;
50
- const status = colorStatus(item.status, item.statusColor);
51
- out += ` ${pointer} ${checkbox} ${label}${status ? ` ${status}` : ''}\n`;
35
+ const pointer = i === cursor ? '❯' : ' ';
36
+ const checkbox = item.checked ? '◉' : '○';
37
+ const status = item.status ?? '';
38
+ out += ` ${pointer} ${checkbox} ${item.label}${status ? ` ${status}` : ''}\n`;
52
39
  }
53
- out += `\n ${styleText('dim', '↑↓ navigate · Space toggle · a all · Enter confirm · q cancel')}\n`;
40
+ out += `\n ↑↓ navigate · Space toggle · a all · Enter confirm · q cancel\n`;
54
41
  return out;
55
42
  }
56
43
  return new Promise((resolve) => {
@@ -117,14 +104,14 @@ export async function checkboxPrompt(items, opts = {}) {
117
104
  cleanup();
118
105
  const selected = state.filter(i => i.checked).map(i => i.value);
119
106
  // Show summary
120
- stdout.write(` ${styleText('green', '')} ${styleText('bold', `${selected.length} file(s) selected`)}\n\n`);
107
+ stdout.write(` ✓ ${selected.length} file(s) selected\n\n`);
121
108
  resolve(selected);
122
109
  return;
123
110
  }
124
111
  // q / Esc — cancel
125
112
  if (key === 'q' || key === '\x1b') {
126
113
  cleanup();
127
- stdout.write(` ${styleText('yellow', '')} ${styleText('dim', 'Cancelled')}\n\n`);
114
+ stdout.write(` ✗ Cancelled\n\n`);
128
115
  resolve([]);
129
116
  return;
130
117
  }
@@ -149,7 +136,7 @@ export async function confirmPrompt(message, defaultYes = true) {
149
136
  if (!stdin.isTTY)
150
137
  return defaultYes;
151
138
  const hint = defaultYes ? '[Y/n]' : '[y/N]';
152
- stdout.write(` ${message} ${styleText('dim', hint)} `);
139
+ stdout.write(` ${message} ${hint} `);
153
140
  return new Promise((resolve) => {
154
141
  const wasRaw = stdin.isRaw;
155
142
  stdin.setRawMode(true);
@@ -60,12 +60,14 @@ export interface FetchJsonOptions {
60
60
  body?: unknown;
61
61
  timeoutMs?: number;
62
62
  }
63
+ export type BrowserEvaluateFunction<Args extends unknown[] = unknown[], Result = unknown> = (...args: Args) => Result | Promise<Result>;
63
64
  export interface IPage {
64
65
  goto(url: string, options?: {
65
66
  waitUntil?: 'load' | 'none';
66
67
  settleMs?: number;
67
68
  }): Promise<void>;
68
- evaluate(js: string): Promise<any>;
69
+ evaluate<T = any>(js: string): Promise<T>;
70
+ evaluate<Args extends unknown[], T>(fn: BrowserEvaluateFunction<Args, T>, ...args: Args): Promise<Awaited<T>>;
69
71
  /** Safely evaluate JS with pre-serialized arguments — prevents injection. */
70
72
  evaluateWithArgs?(js: string, args: Record<string, unknown>): Promise<any>;
71
73
  /**
@@ -16,7 +16,6 @@
16
16
  import * as fs from 'node:fs';
17
17
  import * as path from 'node:path';
18
18
  import * as os from 'node:os';
19
- import { styleText } from 'node:util';
20
19
  import { PKG_VERSION } from './version.js';
21
20
  const CACHE_DIR = path.join(os.homedir(), '.opencli');
22
21
  const CACHE_FILE = path.join(CACHE_DIR, 'update-check.json');
@@ -70,8 +69,8 @@ function buildUpdateNotices({ cliVersion, cache, now }) {
70
69
  const lines = {};
71
70
  if (cache.latestVersion && isNewer(cache.latestVersion, cliVersion)) {
72
71
  lines.cli =
73
- styleText('yellow', `\n Update available: v${cliVersion} → v${cache.latestVersion}\n`) +
74
- styleText('dim', ` Run: npm install -g @jackwener/opencli\n`);
72
+ `\n Update available: v${cliVersion} → v${cache.latestVersion}\n` +
73
+ ` Run: npm install -g @jackwener/opencli\n`;
75
74
  }
76
75
  const { currentExtensionVersion, latestExtensionVersion, extensionLastSeenAt } = cache;
77
76
  if (currentExtensionVersion &&
@@ -80,8 +79,8 @@ function buildUpdateNotices({ cliVersion, cache, now }) {
80
79
  now - extensionLastSeenAt < EXTENSION_STALE_MS &&
81
80
  isNewer(latestExtensionVersion, currentExtensionVersion)) {
82
81
  lines.extension =
83
- styleText('yellow', `\n Extension update available: v${currentExtensionVersion} → v${latestExtensionVersion}\n`) +
84
- styleText('dim', ` Download: https://github.com/jackwener/opencli/releases\n`);
82
+ `\n Extension update available: v${currentExtensionVersion} → v${latestExtensionVersion}\n` +
83
+ ` Download: https://github.com/jackwener/opencli/releases\n`;
85
84
  }
86
85
  return lines;
87
86
  }