@jackwener/opencli 1.7.8 → 1.7.9
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 +49 -14
- package/README.zh-CN.md +30 -10
- package/cli-manifest.json +612 -29
- package/clis/36kr/news.js +1 -1
- package/clis/apple-podcasts/commands.test.js +4 -4
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/apple-podcasts/search.js +1 -1
- package/clis/apple-podcasts/top.js +1 -1
- package/clis/arxiv/paper.js +1 -1
- package/clis/arxiv/search.js +1 -1
- package/clis/band/mentions.js +3 -3
- package/clis/bbc/news.js +1 -1
- package/clis/bilibili/subtitle.js +2 -2
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/economics.js +1 -1
- package/clis/bloomberg/industries.js +1 -1
- package/clis/bloomberg/main.js +1 -1
- package/clis/bloomberg/markets.js +1 -1
- package/clis/bloomberg/opinions.js +1 -1
- package/clis/bloomberg/politics.js +1 -1
- package/clis/bloomberg/tech.js +1 -1
- package/clis/boss/search.js +49 -8
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +3 -3
- package/clis/chatgpt/image.js +37 -8
- package/clis/chatgpt/image.test.js +92 -0
- package/clis/chatgpt/utils.js +39 -6
- package/clis/chatgpt/utils.test.js +63 -0
- package/clis/chatgpt-app/ask.js +1 -1
- package/clis/chatgpt-app/ax.js +4 -2
- package/clis/chatgpt-app/ax.test.js +12 -0
- package/clis/chatgpt-app/model.js +1 -1
- package/clis/chatgpt-app/new.js +1 -1
- package/clis/chatgpt-app/read.js +1 -1
- package/clis/chatgpt-app/send.js +1 -1
- package/clis/chatgpt-app/status.js +1 -1
- package/clis/chatwise/ask.js +2 -2
- package/clis/chatwise/model.js +2 -2
- package/clis/chatwise/send.js +2 -2
- package/clis/claude/ask.js +128 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +29 -0
- package/clis/claude/history.js +31 -0
- package/clis/claude/new.js +21 -0
- package/clis/claude/read.js +24 -0
- package/clis/claude/send.js +41 -0
- package/clis/claude/status.js +24 -0
- package/clis/claude/utils.js +440 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/codex/ask.js +2 -2
- package/clis/codex/send.js +2 -2
- package/clis/ctrip/search.js +1 -1
- package/clis/ctrip/search.test.js +4 -4
- package/clis/cursor/ask.js +2 -2
- package/clis/cursor/composer.js +2 -2
- package/clis/cursor/send.js +2 -2
- package/clis/deepseek/ask.js +17 -4
- package/clis/deepseek/ask.test.js +46 -0
- package/clis/deepseek/utils.js +55 -16
- package/clis/deepseek/utils.test.js +124 -5
- package/clis/doubao/utils.js +53 -11
- package/clis/doubao/utils.test.js +22 -2
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/convertible.js +1 -1
- package/clis/eastmoney/etf.js +1 -1
- package/clis/eastmoney/holders.js +1 -1
- package/clis/eastmoney/index-board.js +1 -1
- package/clis/eastmoney/kline.js +1 -1
- package/clis/eastmoney/kuaixun.js +1 -1
- package/clis/eastmoney/longhu.js +1 -1
- package/clis/eastmoney/money-flow.js +1 -1
- package/clis/eastmoney/northbound.js +1 -1
- package/clis/eastmoney/quote.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/facebook/marketplace-inbox.js +83 -0
- package/clis/facebook/marketplace-listings.js +83 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/google/news.js +1 -1
- package/clis/google/suggest.js +1 -1
- package/clis/google/trends.js +1 -1
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +1 -1
- package/clis/google-scholar/search.test.js +15 -0
- package/clis/hf/top.js +1 -1
- package/clis/jd/item.js +679 -47
- package/clis/jd/item.test.js +318 -7
- package/clis/jd/item.test.ts +517 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/tags.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/lesswrong/user-posts.js +1 -1
- package/clis/lesswrong/user.js +1 -1
- package/clis/paperreview/commands.test.js +6 -6
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/producthunt/posts.js +1 -1
- package/clis/producthunt/today.js +1 -1
- package/clis/sinablog/search.js +1 -1
- package/clis/sinafinance/news.js +1 -1
- package/clis/sinafinance/stock.js +1 -1
- package/clis/sinafinance/stock.test.js +2 -2
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +5 -6
- package/clis/toutiao/articles.test.js +22 -15
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +224 -73
- package/clis/twitter/following.test.js +277 -0
- package/clis/twitter/post.js +184 -47
- package/clis/twitter/post.test.js +114 -34
- package/clis/uiverse/_shared.js +63 -4
- package/clis/uiverse/_shared.test.js +7 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/web/read.js +319 -81
- package/clis/web/read.test.js +221 -5
- package/clis/weibo/favorites.js +169 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/publish.js +282 -0
- package/clis/weibo/publish.test.js +183 -0
- package/clis/weread/ranking.js +1 -1
- package/clis/weread/search-regression.test.js +8 -8
- package/clis/weread/search.js +1 -1
- package/clis/wikipedia/random.js +1 -1
- package/clis/wikipedia/search.js +1 -1
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/trending.js +1 -1
- package/clis/xianyu/chat.js +3 -3
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/item.test.js +3 -3
- package/clis/xiaohongshu/search.js +17 -2
- package/clis/xiaohongshu/search.test.js +37 -1
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/download.test.js +3 -3
- package/clis/xiaoyuzhou/episode.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
- package/clis/xiaoyuzhou/podcast.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/xiaoyuzhou/transcript.test.js +5 -5
- package/clis/yollomi/models.js +1 -1
- package/clis/youtube/channel.js +24 -1
- package/clis/youtube/channel.test.js +59 -0
- package/clis/zhihu/answer.js +21 -162
- package/clis/zhihu/answer.test.js +26 -53
- package/clis/zhihu/collection.js +197 -0
- package/clis/zhihu/collection.test.js +290 -0
- package/clis/zhihu/collections.js +127 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +24 -305
- package/clis/zhihu/comment.test.js +31 -35
- package/clis/zhihu/favorite.js +44 -182
- package/clis/zhihu/favorite.test.js +30 -167
- package/clis/zhihu/follow.js +25 -56
- package/clis/zhihu/follow.test.js +20 -23
- package/clis/zhihu/like.js +22 -67
- package/clis/zhihu/like.test.js +19 -42
- package/clis/zhihu/search.js +3 -2
- package/clis/zhihu/write-shared.js +8 -1
- package/clis/zhihu/write-shared.test.js +1 -0
- package/clis/zlibrary/commands.test.js +75 -0
- package/clis/zlibrary/info.js +47 -0
- package/clis/zlibrary/search.js +46 -0
- package/clis/zlibrary/utils.js +136 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/base-page.d.ts +3 -1
- package/dist/src/browser/base-page.js +76 -1
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +74 -0
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +36 -9
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +38 -4
- package/dist/src/browser/daemon-client.js +24 -7
- package/dist/src/browser/daemon-client.test.js +49 -0
- package/dist/src/browser/errors.js +3 -0
- package/dist/src/browser/errors.test.js +3 -0
- package/dist/src/browser/network-cache.d.ts +1 -0
- package/dist/src/browser/page.d.ts +3 -1
- package/dist/src/browser/page.js +10 -2
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/build-manifest.d.ts +2 -0
- package/dist/src/build-manifest.js +13 -3
- package/dist/src/build-manifest.test.js +20 -2
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +462 -32
- package/dist/src/cli.test.js +209 -2
- package/dist/src/commanderAdapter.js +17 -9
- package/dist/src/commanderAdapter.test.js +67 -2
- package/dist/src/commands/daemon.js +6 -0
- package/dist/src/completion-shared.js +1 -2
- package/dist/src/completion.test.js +3 -2
- package/dist/src/daemon.js +125 -41
- package/dist/src/doctor.d.ts +4 -6
- package/dist/src/doctor.js +80 -22
- package/dist/src/doctor.test.js +82 -0
- package/dist/src/engine.test.js +6 -5
- package/dist/src/errors.d.ts +14 -8
- package/dist/src/errors.js +36 -30
- package/dist/src/errors.test.js +5 -5
- package/dist/src/execution.d.ts +4 -0
- package/dist/src/execution.js +173 -25
- package/dist/src/execution.test.js +171 -1
- package/dist/src/main.js +10 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/pipeline/steps/fetch.js +1 -21
- package/dist/src/pipeline/steps/fetch.test.js +6 -12
- package/dist/src/plugin-scaffold.js +1 -1
- package/dist/src/plugin-scaffold.test.js +1 -1
- package/dist/src/registry.d.ts +40 -9
- package/dist/src/registry.js +3 -1
- package/dist/src/runtime-detect.d.ts +10 -0
- package/dist/src/runtime-detect.js +19 -0
- package/dist/src/runtime-detect.test.js +12 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/update-check.d.ts +31 -1
- package/dist/src/update-check.js +62 -16
- package/dist/src/update-check.test.js +86 -1
- package/package.json +1 -1
- package/dist/src/diagnostic.d.ts +0 -63
- package/dist/src/diagnostic.js +0 -292
- package/dist/src/diagnostic.test.js +0 -302
- /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
package/dist/src/cli.test.js
CHANGED
|
@@ -2,10 +2,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
|
+
import { BrowserCommandError } from './browser/daemon-client.js';
|
|
5
6
|
import { TargetError } from './browser/target-errors.js';
|
|
6
|
-
const { mockBrowserConnect, mockBrowserClose, browserState, } = vi.hoisted(() => ({
|
|
7
|
+
const { mockBrowserConnect, mockBrowserClose, mockBindTab, mockSendCommand, browserState, } = vi.hoisted(() => ({
|
|
7
8
|
mockBrowserConnect: vi.fn(),
|
|
8
9
|
mockBrowserClose: vi.fn(),
|
|
10
|
+
mockBindTab: vi.fn(),
|
|
11
|
+
mockSendCommand: vi.fn(),
|
|
9
12
|
browserState: { page: null },
|
|
10
13
|
}));
|
|
11
14
|
vi.mock('./browser/index.js', () => {
|
|
@@ -17,7 +20,15 @@ vi.mock('./browser/index.js', () => {
|
|
|
17
20
|
},
|
|
18
21
|
};
|
|
19
22
|
});
|
|
20
|
-
|
|
23
|
+
vi.mock('./browser/daemon-client.js', async () => {
|
|
24
|
+
const actual = await vi.importActual('./browser/daemon-client.js');
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
bindTab: mockBindTab,
|
|
28
|
+
sendCommand: mockSendCommand,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
import { createProgram, findPackageRoot, normalizeVerifyRows, renderVerifyPreview, resolveBrowserVerifyInvocation, selectFreshByTimestamp } from './cli.js';
|
|
21
32
|
describe('resolveBrowserVerifyInvocation', () => {
|
|
22
33
|
it('prefers the built entry declared in package metadata', () => {
|
|
23
34
|
const projectRoot = path.join('repo-root');
|
|
@@ -82,6 +93,22 @@ describe('resolveBrowserVerifyInvocation', () => {
|
|
|
82
93
|
});
|
|
83
94
|
});
|
|
84
95
|
});
|
|
96
|
+
describe('selectFreshByTimestamp', () => {
|
|
97
|
+
it('uses timestamp watermarks so rolled buffers still emit new messages', () => {
|
|
98
|
+
const first = selectFreshByTimestamp([
|
|
99
|
+
{ timestamp: 1, text: 'a' },
|
|
100
|
+
{ timestamp: 2, text: 'b' },
|
|
101
|
+
], 0);
|
|
102
|
+
expect(first.fresh.map((item) => item.text)).toEqual(['a', 'b']);
|
|
103
|
+
expect(first.lastSeenTs).toBe(2);
|
|
104
|
+
const rolled = selectFreshByTimestamp([
|
|
105
|
+
{ timestamp: 2, text: 'b' },
|
|
106
|
+
{ timestamp: 3, text: 'c' },
|
|
107
|
+
], first.lastSeenTs);
|
|
108
|
+
expect(rolled.fresh.map((item) => item.text)).toEqual(['c']);
|
|
109
|
+
expect(rolled.lastSeenTs).toBe(3);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
85
112
|
describe('browser tab targeting commands', () => {
|
|
86
113
|
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
87
114
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
@@ -95,6 +122,13 @@ describe('browser tab targeting commands', () => {
|
|
|
95
122
|
stderrSpy.mockClear();
|
|
96
123
|
mockBrowserConnect.mockClear();
|
|
97
124
|
mockBrowserClose.mockReset().mockResolvedValue(undefined);
|
|
125
|
+
mockBindTab.mockReset().mockResolvedValue({
|
|
126
|
+
workspace: 'bound:default',
|
|
127
|
+
page: 'tab-2',
|
|
128
|
+
url: 'https://user.example/inbox',
|
|
129
|
+
title: 'Inbox',
|
|
130
|
+
});
|
|
131
|
+
mockSendCommand.mockReset().mockResolvedValue({ closed: true });
|
|
98
132
|
browserState.page = {
|
|
99
133
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
100
134
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
@@ -104,6 +138,7 @@ describe('browser tab targeting commands', () => {
|
|
|
104
138
|
startNetworkCapture: vi.fn().mockResolvedValue(true),
|
|
105
139
|
getCookies: vi.fn().mockResolvedValue([]),
|
|
106
140
|
evaluate: vi.fn().mockResolvedValue({ ok: true }),
|
|
141
|
+
snapshot: vi.fn().mockResolvedValue('snapshot'),
|
|
107
142
|
tabs: vi.fn().mockResolvedValue([
|
|
108
143
|
{ index: 0, page: 'tab-1', url: 'https://one.example', title: 'one', active: true },
|
|
109
144
|
{ index: 1, page: 'tab-2', url: 'https://two.example', title: 'two', active: false },
|
|
@@ -127,6 +162,62 @@ describe('browser tab targeting commands', () => {
|
|
|
127
162
|
throw new Error(`Expected string arg to console.log, got ${typeof last}`);
|
|
128
163
|
return JSON.parse(last);
|
|
129
164
|
}
|
|
165
|
+
it('binds the current Chrome tab into a bound workspace', async () => {
|
|
166
|
+
const program = createProgram('', '');
|
|
167
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'bind', '--domain', 'user.example', '--path-prefix', '/inbox']);
|
|
168
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
|
|
169
|
+
expect(mockBindTab).toHaveBeenCalledWith('bound:default', {
|
|
170
|
+
matchDomain: 'user.example',
|
|
171
|
+
matchPathPrefix: '/inbox',
|
|
172
|
+
});
|
|
173
|
+
const out = lastJsonLog();
|
|
174
|
+
expect(out.workspace).toBe('bound:default');
|
|
175
|
+
expect(out.url).toBe('https://user.example/inbox');
|
|
176
|
+
});
|
|
177
|
+
it('rejects bind workspaces outside the bound namespace', async () => {
|
|
178
|
+
const program = createProgram('', '');
|
|
179
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'bind', '--workspace', 'browser:default']);
|
|
180
|
+
expect(mockBrowserConnect).not.toHaveBeenCalled();
|
|
181
|
+
expect(mockBindTab).not.toHaveBeenCalled();
|
|
182
|
+
const out = lastJsonLog();
|
|
183
|
+
expect(out.error.code).toBe('invalid_bind_workspace');
|
|
184
|
+
expect(process.exitCode).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
it('runs browser commands against an explicit bound workspace', async () => {
|
|
187
|
+
const program = createProgram('', '');
|
|
188
|
+
await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'state']);
|
|
189
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
|
|
190
|
+
expect(browserState.page?.snapshot).toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
it('blocks history navigation on bound workspaces unless explicitly allowed', async () => {
|
|
193
|
+
browserState.page = {
|
|
194
|
+
...browserState.page,
|
|
195
|
+
workspace: 'bound:default',
|
|
196
|
+
evaluate: vi.fn(),
|
|
197
|
+
wait: vi.fn(),
|
|
198
|
+
};
|
|
199
|
+
const program = createProgram('', '');
|
|
200
|
+
await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'back']);
|
|
201
|
+
expect(browserState.page?.evaluate).not.toHaveBeenCalled();
|
|
202
|
+
const out = lastJsonLog();
|
|
203
|
+
expect(out.error.code).toBe('bound_navigation_blocked');
|
|
204
|
+
});
|
|
205
|
+
it('unbinds a bound workspace through the daemon close-window command', async () => {
|
|
206
|
+
const program = createProgram('', '');
|
|
207
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'unbind', '--workspace', 'bound:default']);
|
|
208
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
|
|
209
|
+
expect(mockSendCommand).toHaveBeenCalledWith('close-window', { workspace: 'bound:default' });
|
|
210
|
+
const out = lastJsonLog();
|
|
211
|
+
expect(out).toEqual({ unbound: true, workspace: 'bound:default' });
|
|
212
|
+
});
|
|
213
|
+
it('does not print false success when unbind fails', async () => {
|
|
214
|
+
mockSendCommand.mockRejectedValueOnce(new BrowserCommandError('Workspace "bound:default" is not attached to a tab.', 'bound_session_missing', 'Run bind again, then retry the browser command.'));
|
|
215
|
+
const program = createProgram('', '');
|
|
216
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'unbind', '--workspace', 'bound:default']);
|
|
217
|
+
const out = lastJsonLog();
|
|
218
|
+
expect(out.error.code).toBe('bound_session_missing');
|
|
219
|
+
expect(process.exitCode).toBeDefined();
|
|
220
|
+
});
|
|
130
221
|
it('binds browser commands to an explicit target tab via --tab', async () => {
|
|
131
222
|
const program = createProgram('', '');
|
|
132
223
|
await program.parseAsync(['node', 'opencli', 'browser', 'eval', '--tab', 'tab-2', 'document.title']);
|
|
@@ -446,6 +537,9 @@ describe('browser network command', () => {
|
|
|
446
537
|
function getNetworkCachePath(cacheDir) {
|
|
447
538
|
return path.join(cacheDir, 'browser-network', 'browser_default.json');
|
|
448
539
|
}
|
|
540
|
+
function getBoundNetworkCachePath(cacheDir) {
|
|
541
|
+
return path.join(cacheDir, 'browser-network', 'bound_default.json');
|
|
542
|
+
}
|
|
449
543
|
function lastJsonLog() {
|
|
450
544
|
const calls = consoleLogSpy.mock.calls;
|
|
451
545
|
if (calls.length === 0)
|
|
@@ -473,6 +567,7 @@ describe('browser network command', () => {
|
|
|
473
567
|
responseStatus: 200,
|
|
474
568
|
responseContentType: 'application/json',
|
|
475
569
|
responsePreview: JSON.stringify({ data: { user: { rest_id: '42' } } }),
|
|
570
|
+
timestamp: Date.now(),
|
|
476
571
|
},
|
|
477
572
|
{
|
|
478
573
|
url: 'https://cdn.example.com/app.js',
|
|
@@ -496,6 +591,19 @@ describe('browser network command', () => {
|
|
|
496
591
|
expect(out.entries[0]).not.toHaveProperty('body');
|
|
497
592
|
expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(true);
|
|
498
593
|
});
|
|
594
|
+
it('uses the selected browser workspace for network cache scope', async () => {
|
|
595
|
+
const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
|
|
596
|
+
browserState.page = {
|
|
597
|
+
...browserState.page,
|
|
598
|
+
workspace: 'bound:default',
|
|
599
|
+
};
|
|
600
|
+
const program = createProgram('', '');
|
|
601
|
+
await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'network']);
|
|
602
|
+
const out = lastJsonLog();
|
|
603
|
+
expect(out.workspace).toBe('bound:default');
|
|
604
|
+
expect(fs.existsSync(getBoundNetworkCachePath(cacheDir))).toBe(true);
|
|
605
|
+
expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(false);
|
|
606
|
+
});
|
|
499
607
|
it('--all includes static resources that the default filter drops', async () => {
|
|
500
608
|
const program = createProgram('', '');
|
|
501
609
|
await program.parseAsync(['node', 'opencli', 'browser', 'network', '--all']);
|
|
@@ -504,11 +612,73 @@ describe('browser network command', () => {
|
|
|
504
612
|
expect(out.entries.map((e) => e.key)).toContain('UserTweets');
|
|
505
613
|
expect(out.entries.map((e) => e.key)).toContain('GET cdn.example.com/app.js');
|
|
506
614
|
});
|
|
615
|
+
it('--failed and --since filter captured entries by status and time window', async () => {
|
|
616
|
+
const now = Date.now();
|
|
617
|
+
browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
|
|
618
|
+
{
|
|
619
|
+
url: 'https://api.example.com/new-fail',
|
|
620
|
+
method: 'GET',
|
|
621
|
+
responseStatus: 500,
|
|
622
|
+
responseContentType: 'application/json',
|
|
623
|
+
responsePreview: JSON.stringify({ error: true }),
|
|
624
|
+
timestamp: now,
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
url: 'https://api.example.com/old-fail',
|
|
628
|
+
method: 'GET',
|
|
629
|
+
responseStatus: 500,
|
|
630
|
+
responseContentType: 'application/json',
|
|
631
|
+
responsePreview: JSON.stringify({ error: true }),
|
|
632
|
+
timestamp: now - 180_000,
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
url: 'https://api.example.com/new-ok',
|
|
636
|
+
method: 'GET',
|
|
637
|
+
responseStatus: 200,
|
|
638
|
+
responseContentType: 'application/json',
|
|
639
|
+
responsePreview: JSON.stringify({ ok: true }),
|
|
640
|
+
timestamp: now,
|
|
641
|
+
},
|
|
642
|
+
]);
|
|
643
|
+
const program = createProgram('', '');
|
|
644
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'network', '--since', '120s', '--failed']);
|
|
645
|
+
const out = lastJsonLog();
|
|
646
|
+
expect(out.count).toBe(1);
|
|
647
|
+
expect(out.entries[0].url).toBe('https://api.example.com/new-fail');
|
|
648
|
+
expect(out.entries[0].timestamp).toMatch(/T/);
|
|
649
|
+
});
|
|
650
|
+
it('default output keeps text/javascript API responses while dropping static JS files', async () => {
|
|
651
|
+
browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
|
|
652
|
+
{
|
|
653
|
+
url: 'https://hw.mail.163.com/js6/s?sid=abc&func=mbox:listMessages',
|
|
654
|
+
method: 'POST',
|
|
655
|
+
responseStatus: 200,
|
|
656
|
+
responseContentType: 'text/javascript',
|
|
657
|
+
responsePreview: JSON.stringify({ messages: [{ id: 'm1', subject: 'hello' }] }),
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
url: 'https://cdn.example.com/app.js',
|
|
661
|
+
method: 'GET',
|
|
662
|
+
responseStatus: 200,
|
|
663
|
+
responseContentType: 'application/javascript',
|
|
664
|
+
responsePreview: '// js',
|
|
665
|
+
},
|
|
666
|
+
]);
|
|
667
|
+
const program = createProgram('', '');
|
|
668
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'network']);
|
|
669
|
+
const out = lastJsonLog();
|
|
670
|
+
expect(out.count).toBe(1);
|
|
671
|
+
expect(out.filtered_out).toBe(1);
|
|
672
|
+
expect(out.entries[0].key).toBe('POST hw.mail.163.com/js6/s');
|
|
673
|
+
expect(out.entries[0].ct).toBe('text/javascript');
|
|
674
|
+
expect(out.entries[0].shape['$.messages']).toBe('array(1)');
|
|
675
|
+
});
|
|
507
676
|
it('--raw emits full bodies inline for every entry', async () => {
|
|
508
677
|
const program = createProgram('', '');
|
|
509
678
|
await program.parseAsync(['node', 'opencli', 'browser', 'network', '--raw']);
|
|
510
679
|
const out = lastJsonLog();
|
|
511
680
|
expect(out.entries[0].body).toEqual({ data: { user: { rest_id: '42' } } });
|
|
681
|
+
expect(out.entries[0].timestamp).toMatch(/T/);
|
|
512
682
|
});
|
|
513
683
|
it('--detail <key> returns the full body for the requested entry', async () => {
|
|
514
684
|
const program = createProgram('', '');
|
|
@@ -519,6 +689,7 @@ describe('browser network command', () => {
|
|
|
519
689
|
expect(out.key).toBe('UserTweets');
|
|
520
690
|
expect(out.body).toEqual({ data: { user: { rest_id: '42' } } });
|
|
521
691
|
expect(out.shape['$.data.user.rest_id']).toBe('string');
|
|
692
|
+
expect(out.timestamp).toMatch(/T/);
|
|
522
693
|
});
|
|
523
694
|
it('--detail reports key_not_found with the list of available keys', async () => {
|
|
524
695
|
const program = createProgram('', '');
|
|
@@ -793,6 +964,42 @@ describe('browser network command', () => {
|
|
|
793
964
|
});
|
|
794
965
|
});
|
|
795
966
|
});
|
|
967
|
+
describe('browser console command', () => {
|
|
968
|
+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
969
|
+
beforeEach(() => {
|
|
970
|
+
process.exitCode = undefined;
|
|
971
|
+
consoleLogSpy.mockClear();
|
|
972
|
+
mockBrowserConnect.mockClear();
|
|
973
|
+
mockBrowserClose.mockReset().mockResolvedValue(undefined);
|
|
974
|
+
const now = Date.now();
|
|
975
|
+
browserState.page = {
|
|
976
|
+
setActivePage: vi.fn(),
|
|
977
|
+
getActivePage: vi.fn().mockReturnValue('tab-1'),
|
|
978
|
+
tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
|
|
979
|
+
consoleMessages: vi.fn().mockResolvedValue([
|
|
980
|
+
{ type: 'error', text: 'boom', timestamp: now },
|
|
981
|
+
{ type: 'log', text: 'ok', timestamp: now },
|
|
982
|
+
{ type: 'warning', text: 'old warning', timestamp: now - 180_000 },
|
|
983
|
+
]),
|
|
984
|
+
};
|
|
985
|
+
});
|
|
986
|
+
function lastJsonLog() {
|
|
987
|
+
const calls = consoleLogSpy.mock.calls;
|
|
988
|
+
if (calls.length === 0)
|
|
989
|
+
throw new Error('Expected at least one console.log call');
|
|
990
|
+
const last = calls[calls.length - 1][0];
|
|
991
|
+
if (typeof last !== 'string')
|
|
992
|
+
throw new Error(`Expected string arg to console.log, got ${typeof last}`);
|
|
993
|
+
return JSON.parse(last);
|
|
994
|
+
}
|
|
995
|
+
it('filters console messages by level and time window', async () => {
|
|
996
|
+
const program = createProgram('', '');
|
|
997
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'console', '--level', 'error', '--since', '120s']);
|
|
998
|
+
const out = lastJsonLog();
|
|
999
|
+
expect(out.count).toBe(1);
|
|
1000
|
+
expect(out.messages[0]).toMatchObject({ type: 'error', text: 'boom' });
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
796
1003
|
describe('browser get html command', () => {
|
|
797
1004
|
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
798
1005
|
function lastLogArg() {
|
|
@@ -16,7 +16,6 @@ import { formatRegistryHelpText } from './serialization.js';
|
|
|
16
16
|
import { render as renderOutput } from './output.js';
|
|
17
17
|
import { executeCommand, prepareCommandArgs } from './execution.js';
|
|
18
18
|
import { CliError, EXIT_CODES, toEnvelope, } from './errors.js';
|
|
19
|
-
import { isDiagnosticEnabled } from './diagnostic.js';
|
|
20
19
|
/**
|
|
21
20
|
* Register a single CliCommand as a Commander subcommand.
|
|
22
21
|
*/
|
|
@@ -48,6 +47,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
48
47
|
}
|
|
49
48
|
subCmd
|
|
50
49
|
.option('-f, --format <fmt>', 'Output format: table, plain, json, yaml, md, csv', 'table')
|
|
50
|
+
.option('--trace <mode>', 'Trace capture: off, on, retain-on-failure', 'off')
|
|
51
51
|
.option('-v, --verbose', 'Debug output', false);
|
|
52
52
|
subCmd.addHelpText('after', formatRegistryHelpText(cmd));
|
|
53
53
|
subCmd.action(async (...actionArgs) => {
|
|
@@ -94,7 +94,12 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
94
94
|
const replacement = cmd.replacedBy ? ` Use ${cmd.replacedBy} instead.` : '';
|
|
95
95
|
log.warn(`Deprecated: ${message}${replacement}`);
|
|
96
96
|
}
|
|
97
|
-
const
|
|
97
|
+
const globals = typeof subCmd.optsWithGlobals === 'function' ? subCmd.optsWithGlobals() : {};
|
|
98
|
+
const result = await executeCommand(cmd, kwargs, verbose, {
|
|
99
|
+
prepared: true,
|
|
100
|
+
...(typeof globals.profile === 'string' && globals.profile.trim() ? { profile: globals.profile.trim() } : {}),
|
|
101
|
+
...(typeof optionsRecord.trace === 'string' && optionsRecord.trace !== 'off' ? { trace: optionsRecord.trace } : {}),
|
|
102
|
+
});
|
|
98
103
|
if (result === null || result === undefined) {
|
|
99
104
|
return;
|
|
100
105
|
}
|
|
@@ -116,7 +121,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
116
121
|
});
|
|
117
122
|
}
|
|
118
123
|
catch (err) {
|
|
119
|
-
renderError(err, fullName(cmd), optionsRecord.verbose === true);
|
|
124
|
+
renderError(err, fullName(cmd), optionsRecord.verbose === true, optionsRecord.trace);
|
|
120
125
|
process.exitCode = resolveExitCode(err);
|
|
121
126
|
}
|
|
122
127
|
});
|
|
@@ -128,13 +133,16 @@ function resolveExitCode(err) {
|
|
|
128
133
|
return EXIT_CODES.GENERIC_ERROR;
|
|
129
134
|
}
|
|
130
135
|
// ── Error rendering ─────────────────────────────────────────────────────────
|
|
131
|
-
/** Emit AutoFix hint for repairable adapter errors (skipped if already
|
|
132
|
-
function emitAutoFixHint(envelope, cmdName) {
|
|
133
|
-
if (
|
|
136
|
+
/** Emit AutoFix hint for repairable adapter errors (skipped if trace already exported). */
|
|
137
|
+
function emitAutoFixHint(envelope, cmdName, traceMode) {
|
|
138
|
+
if (traceMode === 'on' || traceMode === 'retain-on-failure')
|
|
134
139
|
return envelope;
|
|
135
|
-
|
|
140
|
+
const runnable = cmdName.replace('/', ' ');
|
|
141
|
+
return envelope
|
|
142
|
+
+ `# AutoFix: re-run with --trace=retain-on-failure for trace artifact\n`
|
|
143
|
+
+ `# opencli ${runnable} --trace retain-on-failure\n`;
|
|
136
144
|
}
|
|
137
|
-
function renderError(err, cmdName, verbose) {
|
|
145
|
+
function renderError(err, cmdName, verbose, traceMode) {
|
|
138
146
|
const envelope = toEnvelope(err);
|
|
139
147
|
// In verbose mode, include stack trace for debugging
|
|
140
148
|
if (verbose && err instanceof Error && err.stack) {
|
|
@@ -144,7 +152,7 @@ function renderError(err, cmdName, verbose) {
|
|
|
144
152
|
// Append AutoFix hint for repairable errors
|
|
145
153
|
const code = envelope.error.code;
|
|
146
154
|
if (code === 'SELECTOR' || code === 'EMPTY_RESULT' || code === 'ADAPTER_LOAD' || code === 'UNKNOWN') {
|
|
147
|
-
output = emitAutoFixHint(output, cmdName);
|
|
155
|
+
output = emitAutoFixHint(output, cmdName, traceMode);
|
|
148
156
|
}
|
|
149
157
|
process.stderr.write(output);
|
|
150
158
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { EmptyResultError,
|
|
3
|
+
import { attachTraceReceipt, EmptyResultError, selectorError } from './errors.js';
|
|
4
4
|
const { mockExecuteCommand, mockRenderOutput } = vi.hoisted(() => ({
|
|
5
5
|
mockExecuteCommand: vi.fn(),
|
|
6
6
|
mockRenderOutput: vi.fn(),
|
|
@@ -67,6 +67,13 @@ describe('commanderAdapter arg passing', () => {
|
|
|
67
67
|
'prepare-only': 'cli',
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
|
+
it('passes explicit trace mode to executeCommand', async () => {
|
|
71
|
+
const program = new Command();
|
|
72
|
+
const siteCmd = program.command('paperreview');
|
|
73
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
74
|
+
await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--trace', 'retain-on-failure']);
|
|
75
|
+
expect(mockExecuteCommand).toHaveBeenCalledWith(expect.objectContaining({ site: 'paperreview', name: 'submit' }), expect.objectContaining({ pdf: './paper.pdf' }), false, { prepared: true, trace: 'retain-on-failure' });
|
|
76
|
+
});
|
|
70
77
|
it('rejects invalid bool values before calling executeCommand', async () => {
|
|
71
78
|
const program = new Command();
|
|
72
79
|
const siteCmd = program.command('paperreview');
|
|
@@ -269,6 +276,9 @@ describe('commanderAdapter error envelope output', () => {
|
|
|
269
276
|
expect(output).toContain('ok: false');
|
|
270
277
|
expect(output).toContain('code: EMPTY_RESULT');
|
|
271
278
|
expect(output).toContain('xsec_token');
|
|
279
|
+
expect(output).toContain('--trace=retain-on-failure');
|
|
280
|
+
expect(output).toContain('opencli xiaohongshu note --trace retain-on-failure');
|
|
281
|
+
expect(output).not.toContain('OPENCLI_DIAGNOSTIC');
|
|
272
282
|
stderrSpy.mockRestore();
|
|
273
283
|
});
|
|
274
284
|
it('outputs YAML error envelope for selector errors', async () => {
|
|
@@ -276,12 +286,67 @@ describe('commanderAdapter error envelope output', () => {
|
|
|
276
286
|
const siteCmd = program.command('xiaohongshu');
|
|
277
287
|
registerCommandToProgram(siteCmd, cmd);
|
|
278
288
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
279
|
-
mockExecuteCommand.mockRejectedValueOnce(
|
|
289
|
+
mockExecuteCommand.mockRejectedValueOnce(selectorError('.note-title', 'The note title selector no longer matches the current page.'));
|
|
280
290
|
await program.parseAsync(['node', 'opencli', 'xiaohongshu', 'note', '69ca3927000000001a020fd5']);
|
|
281
291
|
const output = stderrSpy.mock.calls.map(c => String(c[0])).join('');
|
|
282
292
|
expect(output).toContain('ok: false');
|
|
283
293
|
expect(output).toContain('code: SELECTOR');
|
|
284
294
|
expect(output).toContain('selector no longer matches');
|
|
295
|
+
expect(output).toContain('--trace=retain-on-failure');
|
|
296
|
+
stderrSpy.mockRestore();
|
|
297
|
+
});
|
|
298
|
+
it('does not add an AutoFix rerun hint when trace is already enabled', async () => {
|
|
299
|
+
const program = new Command();
|
|
300
|
+
const siteCmd = program.command('xiaohongshu');
|
|
301
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
302
|
+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
303
|
+
mockExecuteCommand.mockRejectedValueOnce(selectorError('.note-title'));
|
|
304
|
+
await program.parseAsync([
|
|
305
|
+
'node',
|
|
306
|
+
'opencli',
|
|
307
|
+
'xiaohongshu',
|
|
308
|
+
'note',
|
|
309
|
+
'69ca3927000000001a020fd5',
|
|
310
|
+
'--trace',
|
|
311
|
+
'retain-on-failure',
|
|
312
|
+
]);
|
|
313
|
+
const output = stderrSpy.mock.calls.map(c => String(c[0])).join('');
|
|
314
|
+
expect(output).toContain('code: SELECTOR');
|
|
315
|
+
expect(output).not.toContain('AutoFix: re-run');
|
|
316
|
+
stderrSpy.mockRestore();
|
|
317
|
+
});
|
|
318
|
+
it('includes trace metadata from the error envelope when execution attached it', async () => {
|
|
319
|
+
const program = new Command();
|
|
320
|
+
const siteCmd = program.command('xiaohongshu');
|
|
321
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
322
|
+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
323
|
+
const err = selectorError('.note-title');
|
|
324
|
+
attachTraceReceipt(err, {
|
|
325
|
+
schemaVersion: 1,
|
|
326
|
+
opencliVersion: '1.7.8',
|
|
327
|
+
traceId: 'trace-1',
|
|
328
|
+
traceDir: '/tmp/opencli/profiles/default/traces/trace-1',
|
|
329
|
+
summaryPath: '/tmp/opencli/profiles/default/traces/trace-1/summary.md',
|
|
330
|
+
receiptPath: '/tmp/opencli/profiles/default/traces/trace-1/receipt.json',
|
|
331
|
+
status: 'failure',
|
|
332
|
+
createdAt: '2026-05-03T00:00:00.000Z',
|
|
333
|
+
error: { code: 'SELECTOR', message: 'Could not find element: .note-title' },
|
|
334
|
+
});
|
|
335
|
+
mockExecuteCommand.mockRejectedValueOnce(err);
|
|
336
|
+
await program.parseAsync([
|
|
337
|
+
'node',
|
|
338
|
+
'opencli',
|
|
339
|
+
'xiaohongshu',
|
|
340
|
+
'note',
|
|
341
|
+
'69ca3927000000001a020fd5',
|
|
342
|
+
'--trace',
|
|
343
|
+
'retain-on-failure',
|
|
344
|
+
]);
|
|
345
|
+
const output = stderrSpy.mock.calls.map(c => String(c[0])).join('');
|
|
346
|
+
expect(output).toContain('trace:');
|
|
347
|
+
expect(output).toContain('dir: /tmp/opencli/profiles/default/traces/trace-1');
|
|
348
|
+
expect(output).toContain('summaryPath: /tmp/opencli/profiles/default/traces/trace-1/summary.md');
|
|
349
|
+
expect(output).toContain('receiptPath: /tmp/opencli/profiles/default/traces/trace-1/receipt.json');
|
|
285
350
|
stderrSpy.mockRestore();
|
|
286
351
|
});
|
|
287
352
|
});
|
|
@@ -21,6 +21,12 @@ export async function daemonStatus() {
|
|
|
21
21
|
console.log(`Daemon: ${styleText('green', 'running')} (PID ${status.pid})`);
|
|
22
22
|
console.log(`Uptime: ${formatDuration(Math.round(status.uptime * 1000))}`);
|
|
23
23
|
console.log(`Extension: ${extensionLabel}`);
|
|
24
|
+
if (status.profiles && status.profiles.length > 0) {
|
|
25
|
+
console.log(`Profiles: ${status.profiles.map((profile) => {
|
|
26
|
+
const version = profile.extensionVersion ? ` v${profile.extensionVersion}` : '';
|
|
27
|
+
return `${profile.contextId}${version}`;
|
|
28
|
+
}).join(', ')}`);
|
|
29
|
+
}
|
|
24
30
|
console.log(`Memory: ${status.memoryMB} MB`);
|
|
25
31
|
console.log(`Port: ${status.port}`);
|
|
26
32
|
}
|
|
@@ -12,8 +12,9 @@ describe('getCompletions', () => {
|
|
|
12
12
|
it('includes top-level built-ins that are registered outside the site registry', () => {
|
|
13
13
|
const completions = getCompletions([], 1);
|
|
14
14
|
expect(completions).toContain('plugin');
|
|
15
|
-
expect(completions).toContain('
|
|
16
|
-
expect(completions).toContain('
|
|
15
|
+
expect(completions).toContain('external');
|
|
16
|
+
expect(completions).not.toContain('install');
|
|
17
|
+
expect(completions).not.toContain('register');
|
|
17
18
|
expect(completions).not.toContain('setup');
|
|
18
19
|
});
|
|
19
20
|
it('still includes discovered site names', () => {
|