@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.
- package/README.md +18 -17
- package/README.zh-CN.md +16 -18
- package/cli-manifest.json +311 -186
- 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 +8 -4
- package/clis/twitter/bookmark-folder.test.js +59 -1
- package/clis/twitter/bookmarks.js +12 -4
- package/clis/twitter/bookmarks.test.js +205 -0
- 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/daemon-client.d.ts +1 -0
- package/dist/src/browser/daemon-client.js +3 -0
- package/dist/src/browser/daemon-client.test.js +20 -0
- 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 +131 -89
- package/dist/src/cli.test.js +34 -28
- package/dist/src/commands/daemon.js +6 -7
- package/dist/src/daemon-utils.d.ts +18 -0
- package/dist/src/daemon-utils.js +37 -0
- package/dist/src/daemon.d.ts +1 -1
- package/dist/src/daemon.js +44 -13
- package/dist/src/daemon.test.js +42 -1
- 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/electron-apps.js +0 -1
- package/dist/src/electron-apps.test.js +1 -0
- package/dist/src/execution.js +1 -3
- package/dist/src/execution.test.js +4 -16
- package/dist/src/external-clis.yaml +12 -3
- package/dist/src/external.d.ts +4 -0
- package/dist/src/external.js +3 -0
- package/dist/src/external.test.js +24 -1
- package/dist/src/help.d.ts +16 -1
- package/dist/src/help.js +50 -8
- package/dist/src/help.test.js +5 -1
- 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
- package/clis/notion/export.js +0 -32
- package/clis/notion/favorites.js +0 -85
- package/clis/notion/new.js +0 -35
- package/clis/notion/read.js +0 -31
- package/clis/notion/search.js +0 -47
- package/clis/notion/sidebar.js +0 -42
- package/clis/notion/status.js +0 -17
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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]
|
package/dist/src/external.d.ts
CHANGED
|
@@ -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
|
package/dist/src/external.js
CHANGED
|
@@ -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 = {
|
package/dist/src/help.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
238
|
-
usage
|
|
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: `${
|
|
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:
|
|
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,
|
package/dist/src/help.test.js
CHANGED
|
@@ -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: [
|
|
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
|
});
|
package/dist/src/logger.js
CHANGED
|
@@ -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(
|
|
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(`${
|
|
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(`${
|
|
21
|
+
process.stderr.write(`${msg}\n`);
|
|
23
22
|
},
|
|
24
23
|
/** Warning (always shown) */
|
|
25
24
|
warn(msg) {
|
|
26
|
-
process.stderr.write(
|
|
25
|
+
process.stderr.write(`⚠ ${msg}\n`);
|
|
27
26
|
},
|
|
28
27
|
/** Error (always shown) */
|
|
29
28
|
error(msg) {
|
|
30
|
-
process.stderr.write(
|
|
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(
|
|
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(`
|
|
43
|
+
process.stderr.write(` [${stepNum}/${total}] ${op}${preview}\n`);
|
|
45
44
|
},
|
|
46
45
|
/** Step result summary */
|
|
47
46
|
stepResult(summary) {
|
|
48
|
-
process.stderr.write(`
|
|
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);
|
package/dist/src/output.js
CHANGED
|
@@ -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(
|
|
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 =>
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
34
|
-
expect(isSupportedNodeVersion('
|
|
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
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${
|
|
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 ?
|
|
48
|
-
const checkbox = item.checked ?
|
|
49
|
-
const
|
|
50
|
-
|
|
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
|
|
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(`
|
|
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(`
|
|
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} ${
|
|
139
|
+
stdout.write(` ${message} ${hint} `);
|
|
153
140
|
return new Promise((resolve) => {
|
|
154
141
|
const wasRaw = stdin.isRaw;
|
|
155
142
|
stdin.setRawMode(true);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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<
|
|
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
|
/**
|
package/dist/src/update-check.js
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
}
|