@jackwener/opencli 1.7.8 → 1.7.10
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 +646 -30
- 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/instagram/collection-create.js +57 -0
- package/clis/instagram/saved.js +21 -7
- 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 -2
- package/dist/src/browser/bridge.js +40 -41
- 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/daemon-lifecycle.d.ts +23 -0
- package/dist/src/browser/daemon-lifecycle.js +67 -0
- package/dist/src/browser/daemon-version.d.ts +4 -0
- package/dist/src/browser/daemon-version.js +12 -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 +477 -35
- package/dist/src/cli.test.js +303 -2
- package/dist/src/commanderAdapter.js +17 -9
- package/dist/src/commanderAdapter.test.js +67 -2
- package/dist/src/commands/daemon.d.ts +2 -0
- package/dist/src/commands/daemon.js +42 -1
- package/dist/src/commands/daemon.test.js +103 -2
- 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 +5 -6
- package/dist/src/doctor.js +77 -19
- package/dist/src/doctor.test.js +117 -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,15 @@ 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
|
-
|
|
7
|
+
import { PKG_VERSION } from './version.js';
|
|
8
|
+
const { mockBrowserConnect, mockBrowserClose, mockBindTab, mockSendCommand, mockExecFileSync, browserState, } = vi.hoisted(() => ({
|
|
7
9
|
mockBrowserConnect: vi.fn(),
|
|
8
10
|
mockBrowserClose: vi.fn(),
|
|
11
|
+
mockBindTab: vi.fn(),
|
|
12
|
+
mockSendCommand: vi.fn(),
|
|
13
|
+
mockExecFileSync: vi.fn(),
|
|
9
14
|
browserState: { page: null },
|
|
10
15
|
}));
|
|
11
16
|
vi.mock('./browser/index.js', () => {
|
|
@@ -17,7 +22,22 @@ vi.mock('./browser/index.js', () => {
|
|
|
17
22
|
},
|
|
18
23
|
};
|
|
19
24
|
});
|
|
20
|
-
|
|
25
|
+
vi.mock('./browser/daemon-client.js', async () => {
|
|
26
|
+
const actual = await vi.importActual('./browser/daemon-client.js');
|
|
27
|
+
return {
|
|
28
|
+
...actual,
|
|
29
|
+
bindTab: mockBindTab,
|
|
30
|
+
sendCommand: mockSendCommand,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
vi.mock('node:child_process', async () => {
|
|
34
|
+
const actual = await vi.importActual('node:child_process');
|
|
35
|
+
return {
|
|
36
|
+
...actual,
|
|
37
|
+
execFileSync: mockExecFileSync,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
import { createProgram, findPackageRoot, normalizeVerifyRows, renderVerifyPreview, resolveBrowserVerifyInvocation, selectFreshByTimestamp } from './cli.js';
|
|
21
41
|
describe('resolveBrowserVerifyInvocation', () => {
|
|
22
42
|
it('prefers the built entry declared in package metadata', () => {
|
|
23
43
|
const projectRoot = path.join('repo-root');
|
|
@@ -82,6 +102,107 @@ describe('resolveBrowserVerifyInvocation', () => {
|
|
|
82
102
|
});
|
|
83
103
|
});
|
|
84
104
|
});
|
|
105
|
+
describe('selectFreshByTimestamp', () => {
|
|
106
|
+
it('uses timestamp watermarks so rolled buffers still emit new messages', () => {
|
|
107
|
+
const first = selectFreshByTimestamp([
|
|
108
|
+
{ timestamp: 1, text: 'a' },
|
|
109
|
+
{ timestamp: 2, text: 'b' },
|
|
110
|
+
], 0);
|
|
111
|
+
expect(first.fresh.map((item) => item.text)).toEqual(['a', 'b']);
|
|
112
|
+
expect(first.lastSeenTs).toBe(2);
|
|
113
|
+
const rolled = selectFreshByTimestamp([
|
|
114
|
+
{ timestamp: 2, text: 'b' },
|
|
115
|
+
{ timestamp: 3, text: 'c' },
|
|
116
|
+
], first.lastSeenTs);
|
|
117
|
+
expect(rolled.fresh.map((item) => item.text)).toEqual(['c']);
|
|
118
|
+
expect(rolled.lastSeenTs).toBe(3);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('browser verify', () => {
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
process.exitCode = undefined;
|
|
124
|
+
mockExecFileSync.mockReset().mockReturnValue('[]');
|
|
125
|
+
});
|
|
126
|
+
it('passes --trace through to the adapter subprocess', async () => {
|
|
127
|
+
const originalHome = process.env.HOME;
|
|
128
|
+
const originalUserProfile = process.env.USERPROFILE;
|
|
129
|
+
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-browser-verify-trace-'));
|
|
130
|
+
process.env.HOME = fakeHome;
|
|
131
|
+
process.env.USERPROFILE = fakeHome;
|
|
132
|
+
try {
|
|
133
|
+
const adapterDir = path.join(fakeHome, '.opencli', 'clis', 'hn');
|
|
134
|
+
fs.mkdirSync(adapterDir, { recursive: true });
|
|
135
|
+
fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
|
|
136
|
+
const program = createProgram('', '');
|
|
137
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'verify', 'hn/top', '--no-fixture', '--trace', 'retain-on-failure']);
|
|
138
|
+
expect(mockExecFileSync).toHaveBeenCalledTimes(1);
|
|
139
|
+
const [, execArgs] = mockExecFileSync.mock.calls[0];
|
|
140
|
+
expect(execArgs.slice(-6)).toEqual(['hn', 'top', '--trace', 'retain-on-failure', '--format', 'json']);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
if (originalHome === undefined)
|
|
144
|
+
delete process.env.HOME;
|
|
145
|
+
else
|
|
146
|
+
process.env.HOME = originalHome;
|
|
147
|
+
if (originalUserProfile === undefined)
|
|
148
|
+
delete process.env.USERPROFILE;
|
|
149
|
+
else
|
|
150
|
+
process.env.USERPROFILE = originalUserProfile;
|
|
151
|
+
fs.rmSync(fakeHome, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe('profile list', () => {
|
|
156
|
+
const stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
process.exitCode = undefined;
|
|
159
|
+
stdoutSpy.mockClear();
|
|
160
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
161
|
+
});
|
|
162
|
+
it('reports stale daemon instead of no profiles when status lacks profile support', async () => {
|
|
163
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
164
|
+
ok: true,
|
|
165
|
+
json: async () => ({
|
|
166
|
+
ok: true,
|
|
167
|
+
pid: 123,
|
|
168
|
+
uptime: 1,
|
|
169
|
+
daemonVersion: '1.7.6',
|
|
170
|
+
extensionConnected: true,
|
|
171
|
+
extensionVersion: '1.0.3',
|
|
172
|
+
pending: 0,
|
|
173
|
+
memoryMB: 20,
|
|
174
|
+
port: 19825,
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
const program = createProgram('', '');
|
|
178
|
+
await program.parseAsync(['node', 'opencli', 'profile', 'list']);
|
|
179
|
+
const output = stdoutSpy.mock.calls.flat().join('\n');
|
|
180
|
+
expect(output).toContain('stale');
|
|
181
|
+
expect(output).toContain('opencli daemon restart');
|
|
182
|
+
expect(output).not.toContain('No Browser Bridge profiles connected');
|
|
183
|
+
});
|
|
184
|
+
it('keeps the empty profile message for current daemon status with no profiles', async () => {
|
|
185
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
186
|
+
ok: true,
|
|
187
|
+
json: async () => ({
|
|
188
|
+
ok: true,
|
|
189
|
+
pid: 123,
|
|
190
|
+
uptime: 1,
|
|
191
|
+
daemonVersion: PKG_VERSION,
|
|
192
|
+
extensionConnected: false,
|
|
193
|
+
profiles: [],
|
|
194
|
+
pending: 0,
|
|
195
|
+
memoryMB: 20,
|
|
196
|
+
port: 19825,
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
const program = createProgram('', '');
|
|
200
|
+
await program.parseAsync(['node', 'opencli', 'profile', 'list']);
|
|
201
|
+
const output = stdoutSpy.mock.calls.flat().join('\n');
|
|
202
|
+
expect(output).toContain('No Browser Bridge profiles connected');
|
|
203
|
+
expect(output).not.toContain('opencli daemon restart');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
85
206
|
describe('browser tab targeting commands', () => {
|
|
86
207
|
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
87
208
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
@@ -95,6 +216,13 @@ describe('browser tab targeting commands', () => {
|
|
|
95
216
|
stderrSpy.mockClear();
|
|
96
217
|
mockBrowserConnect.mockClear();
|
|
97
218
|
mockBrowserClose.mockReset().mockResolvedValue(undefined);
|
|
219
|
+
mockBindTab.mockReset().mockResolvedValue({
|
|
220
|
+
workspace: 'bound:default',
|
|
221
|
+
page: 'tab-2',
|
|
222
|
+
url: 'https://user.example/inbox',
|
|
223
|
+
title: 'Inbox',
|
|
224
|
+
});
|
|
225
|
+
mockSendCommand.mockReset().mockResolvedValue({ closed: true });
|
|
98
226
|
browserState.page = {
|
|
99
227
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
100
228
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
@@ -104,6 +232,7 @@ describe('browser tab targeting commands', () => {
|
|
|
104
232
|
startNetworkCapture: vi.fn().mockResolvedValue(true),
|
|
105
233
|
getCookies: vi.fn().mockResolvedValue([]),
|
|
106
234
|
evaluate: vi.fn().mockResolvedValue({ ok: true }),
|
|
235
|
+
snapshot: vi.fn().mockResolvedValue('snapshot'),
|
|
107
236
|
tabs: vi.fn().mockResolvedValue([
|
|
108
237
|
{ index: 0, page: 'tab-1', url: 'https://one.example', title: 'one', active: true },
|
|
109
238
|
{ index: 1, page: 'tab-2', url: 'https://two.example', title: 'two', active: false },
|
|
@@ -127,6 +256,62 @@ describe('browser tab targeting commands', () => {
|
|
|
127
256
|
throw new Error(`Expected string arg to console.log, got ${typeof last}`);
|
|
128
257
|
return JSON.parse(last);
|
|
129
258
|
}
|
|
259
|
+
it('binds the current Chrome tab into a bound workspace', async () => {
|
|
260
|
+
const program = createProgram('', '');
|
|
261
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'bind', '--domain', 'user.example', '--path-prefix', '/inbox']);
|
|
262
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
|
|
263
|
+
expect(mockBindTab).toHaveBeenCalledWith('bound:default', {
|
|
264
|
+
matchDomain: 'user.example',
|
|
265
|
+
matchPathPrefix: '/inbox',
|
|
266
|
+
});
|
|
267
|
+
const out = lastJsonLog();
|
|
268
|
+
expect(out.workspace).toBe('bound:default');
|
|
269
|
+
expect(out.url).toBe('https://user.example/inbox');
|
|
270
|
+
});
|
|
271
|
+
it('rejects bind workspaces outside the bound namespace', async () => {
|
|
272
|
+
const program = createProgram('', '');
|
|
273
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'bind', '--workspace', 'browser:default']);
|
|
274
|
+
expect(mockBrowserConnect).not.toHaveBeenCalled();
|
|
275
|
+
expect(mockBindTab).not.toHaveBeenCalled();
|
|
276
|
+
const out = lastJsonLog();
|
|
277
|
+
expect(out.error.code).toBe('invalid_bind_workspace');
|
|
278
|
+
expect(process.exitCode).toBeDefined();
|
|
279
|
+
});
|
|
280
|
+
it('runs browser commands against an explicit bound workspace', async () => {
|
|
281
|
+
const program = createProgram('', '');
|
|
282
|
+
await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'state']);
|
|
283
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
|
|
284
|
+
expect(browserState.page?.snapshot).toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
it('blocks history navigation on bound workspaces unless explicitly allowed', async () => {
|
|
287
|
+
browserState.page = {
|
|
288
|
+
...browserState.page,
|
|
289
|
+
workspace: 'bound:default',
|
|
290
|
+
evaluate: vi.fn(),
|
|
291
|
+
wait: vi.fn(),
|
|
292
|
+
};
|
|
293
|
+
const program = createProgram('', '');
|
|
294
|
+
await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'back']);
|
|
295
|
+
expect(browserState.page?.evaluate).not.toHaveBeenCalled();
|
|
296
|
+
const out = lastJsonLog();
|
|
297
|
+
expect(out.error.code).toBe('bound_navigation_blocked');
|
|
298
|
+
});
|
|
299
|
+
it('unbinds a bound workspace through the daemon close-window command', async () => {
|
|
300
|
+
const program = createProgram('', '');
|
|
301
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'unbind', '--workspace', 'bound:default']);
|
|
302
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
|
|
303
|
+
expect(mockSendCommand).toHaveBeenCalledWith('close-window', { workspace: 'bound:default' });
|
|
304
|
+
const out = lastJsonLog();
|
|
305
|
+
expect(out).toEqual({ unbound: true, workspace: 'bound:default' });
|
|
306
|
+
});
|
|
307
|
+
it('does not print false success when unbind fails', async () => {
|
|
308
|
+
mockSendCommand.mockRejectedValueOnce(new BrowserCommandError('Workspace "bound:default" is not attached to a tab.', 'bound_session_missing', 'Run bind again, then retry the browser command.'));
|
|
309
|
+
const program = createProgram('', '');
|
|
310
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'unbind', '--workspace', 'bound:default']);
|
|
311
|
+
const out = lastJsonLog();
|
|
312
|
+
expect(out.error.code).toBe('bound_session_missing');
|
|
313
|
+
expect(process.exitCode).toBeDefined();
|
|
314
|
+
});
|
|
130
315
|
it('binds browser commands to an explicit target tab via --tab', async () => {
|
|
131
316
|
const program = createProgram('', '');
|
|
132
317
|
await program.parseAsync(['node', 'opencli', 'browser', 'eval', '--tab', 'tab-2', 'document.title']);
|
|
@@ -446,6 +631,9 @@ describe('browser network command', () => {
|
|
|
446
631
|
function getNetworkCachePath(cacheDir) {
|
|
447
632
|
return path.join(cacheDir, 'browser-network', 'browser_default.json');
|
|
448
633
|
}
|
|
634
|
+
function getBoundNetworkCachePath(cacheDir) {
|
|
635
|
+
return path.join(cacheDir, 'browser-network', 'bound_default.json');
|
|
636
|
+
}
|
|
449
637
|
function lastJsonLog() {
|
|
450
638
|
const calls = consoleLogSpy.mock.calls;
|
|
451
639
|
if (calls.length === 0)
|
|
@@ -473,6 +661,7 @@ describe('browser network command', () => {
|
|
|
473
661
|
responseStatus: 200,
|
|
474
662
|
responseContentType: 'application/json',
|
|
475
663
|
responsePreview: JSON.stringify({ data: { user: { rest_id: '42' } } }),
|
|
664
|
+
timestamp: Date.now(),
|
|
476
665
|
},
|
|
477
666
|
{
|
|
478
667
|
url: 'https://cdn.example.com/app.js',
|
|
@@ -496,6 +685,19 @@ describe('browser network command', () => {
|
|
|
496
685
|
expect(out.entries[0]).not.toHaveProperty('body');
|
|
497
686
|
expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(true);
|
|
498
687
|
});
|
|
688
|
+
it('uses the selected browser workspace for network cache scope', async () => {
|
|
689
|
+
const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
|
|
690
|
+
browserState.page = {
|
|
691
|
+
...browserState.page,
|
|
692
|
+
workspace: 'bound:default',
|
|
693
|
+
};
|
|
694
|
+
const program = createProgram('', '');
|
|
695
|
+
await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'network']);
|
|
696
|
+
const out = lastJsonLog();
|
|
697
|
+
expect(out.workspace).toBe('bound:default');
|
|
698
|
+
expect(fs.existsSync(getBoundNetworkCachePath(cacheDir))).toBe(true);
|
|
699
|
+
expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(false);
|
|
700
|
+
});
|
|
499
701
|
it('--all includes static resources that the default filter drops', async () => {
|
|
500
702
|
const program = createProgram('', '');
|
|
501
703
|
await program.parseAsync(['node', 'opencli', 'browser', 'network', '--all']);
|
|
@@ -504,11 +706,73 @@ describe('browser network command', () => {
|
|
|
504
706
|
expect(out.entries.map((e) => e.key)).toContain('UserTweets');
|
|
505
707
|
expect(out.entries.map((e) => e.key)).toContain('GET cdn.example.com/app.js');
|
|
506
708
|
});
|
|
709
|
+
it('--failed and --since filter captured entries by status and time window', async () => {
|
|
710
|
+
const now = Date.now();
|
|
711
|
+
browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
|
|
712
|
+
{
|
|
713
|
+
url: 'https://api.example.com/new-fail',
|
|
714
|
+
method: 'GET',
|
|
715
|
+
responseStatus: 500,
|
|
716
|
+
responseContentType: 'application/json',
|
|
717
|
+
responsePreview: JSON.stringify({ error: true }),
|
|
718
|
+
timestamp: now,
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
url: 'https://api.example.com/old-fail',
|
|
722
|
+
method: 'GET',
|
|
723
|
+
responseStatus: 500,
|
|
724
|
+
responseContentType: 'application/json',
|
|
725
|
+
responsePreview: JSON.stringify({ error: true }),
|
|
726
|
+
timestamp: now - 180_000,
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
url: 'https://api.example.com/new-ok',
|
|
730
|
+
method: 'GET',
|
|
731
|
+
responseStatus: 200,
|
|
732
|
+
responseContentType: 'application/json',
|
|
733
|
+
responsePreview: JSON.stringify({ ok: true }),
|
|
734
|
+
timestamp: now,
|
|
735
|
+
},
|
|
736
|
+
]);
|
|
737
|
+
const program = createProgram('', '');
|
|
738
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'network', '--since', '120s', '--failed']);
|
|
739
|
+
const out = lastJsonLog();
|
|
740
|
+
expect(out.count).toBe(1);
|
|
741
|
+
expect(out.entries[0].url).toBe('https://api.example.com/new-fail');
|
|
742
|
+
expect(out.entries[0].timestamp).toMatch(/T/);
|
|
743
|
+
});
|
|
744
|
+
it('default output keeps text/javascript API responses while dropping static JS files', async () => {
|
|
745
|
+
browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
|
|
746
|
+
{
|
|
747
|
+
url: 'https://hw.mail.163.com/js6/s?sid=abc&func=mbox:listMessages',
|
|
748
|
+
method: 'POST',
|
|
749
|
+
responseStatus: 200,
|
|
750
|
+
responseContentType: 'text/javascript',
|
|
751
|
+
responsePreview: JSON.stringify({ messages: [{ id: 'm1', subject: 'hello' }] }),
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
url: 'https://cdn.example.com/app.js',
|
|
755
|
+
method: 'GET',
|
|
756
|
+
responseStatus: 200,
|
|
757
|
+
responseContentType: 'application/javascript',
|
|
758
|
+
responsePreview: '// js',
|
|
759
|
+
},
|
|
760
|
+
]);
|
|
761
|
+
const program = createProgram('', '');
|
|
762
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'network']);
|
|
763
|
+
const out = lastJsonLog();
|
|
764
|
+
expect(out.count).toBe(1);
|
|
765
|
+
expect(out.filtered_out).toBe(1);
|
|
766
|
+
expect(out.entries[0].key).toBe('POST hw.mail.163.com/js6/s');
|
|
767
|
+
expect(out.entries[0].ct).toBe('text/javascript');
|
|
768
|
+
expect(out.entries[0].shape['$.messages']).toBe('array(1)');
|
|
769
|
+
});
|
|
507
770
|
it('--raw emits full bodies inline for every entry', async () => {
|
|
508
771
|
const program = createProgram('', '');
|
|
509
772
|
await program.parseAsync(['node', 'opencli', 'browser', 'network', '--raw']);
|
|
510
773
|
const out = lastJsonLog();
|
|
511
774
|
expect(out.entries[0].body).toEqual({ data: { user: { rest_id: '42' } } });
|
|
775
|
+
expect(out.entries[0].timestamp).toMatch(/T/);
|
|
512
776
|
});
|
|
513
777
|
it('--detail <key> returns the full body for the requested entry', async () => {
|
|
514
778
|
const program = createProgram('', '');
|
|
@@ -519,6 +783,7 @@ describe('browser network command', () => {
|
|
|
519
783
|
expect(out.key).toBe('UserTweets');
|
|
520
784
|
expect(out.body).toEqual({ data: { user: { rest_id: '42' } } });
|
|
521
785
|
expect(out.shape['$.data.user.rest_id']).toBe('string');
|
|
786
|
+
expect(out.timestamp).toMatch(/T/);
|
|
522
787
|
});
|
|
523
788
|
it('--detail reports key_not_found with the list of available keys', async () => {
|
|
524
789
|
const program = createProgram('', '');
|
|
@@ -793,6 +1058,42 @@ describe('browser network command', () => {
|
|
|
793
1058
|
});
|
|
794
1059
|
});
|
|
795
1060
|
});
|
|
1061
|
+
describe('browser console command', () => {
|
|
1062
|
+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
1063
|
+
beforeEach(() => {
|
|
1064
|
+
process.exitCode = undefined;
|
|
1065
|
+
consoleLogSpy.mockClear();
|
|
1066
|
+
mockBrowserConnect.mockClear();
|
|
1067
|
+
mockBrowserClose.mockReset().mockResolvedValue(undefined);
|
|
1068
|
+
const now = Date.now();
|
|
1069
|
+
browserState.page = {
|
|
1070
|
+
setActivePage: vi.fn(),
|
|
1071
|
+
getActivePage: vi.fn().mockReturnValue('tab-1'),
|
|
1072
|
+
tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
|
|
1073
|
+
consoleMessages: vi.fn().mockResolvedValue([
|
|
1074
|
+
{ type: 'error', text: 'boom', timestamp: now },
|
|
1075
|
+
{ type: 'log', text: 'ok', timestamp: now },
|
|
1076
|
+
{ type: 'warning', text: 'old warning', timestamp: now - 180_000 },
|
|
1077
|
+
]),
|
|
1078
|
+
};
|
|
1079
|
+
});
|
|
1080
|
+
function lastJsonLog() {
|
|
1081
|
+
const calls = consoleLogSpy.mock.calls;
|
|
1082
|
+
if (calls.length === 0)
|
|
1083
|
+
throw new Error('Expected at least one console.log call');
|
|
1084
|
+
const last = calls[calls.length - 1][0];
|
|
1085
|
+
if (typeof last !== 'string')
|
|
1086
|
+
throw new Error(`Expected string arg to console.log, got ${typeof last}`);
|
|
1087
|
+
return JSON.parse(last);
|
|
1088
|
+
}
|
|
1089
|
+
it('filters console messages by level and time window', async () => {
|
|
1090
|
+
const program = createProgram('', '');
|
|
1091
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'console', '--level', 'error', '--since', '120s']);
|
|
1092
|
+
const out = lastJsonLog();
|
|
1093
|
+
expect(out.count).toBe(1);
|
|
1094
|
+
expect(out.messages[0]).toMatchObject({ type: 'error', text: 'boom' });
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
796
1097
|
describe('browser get html command', () => {
|
|
797
1098
|
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
798
1099
|
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
|
});
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* CLI commands for daemon lifecycle:
|
|
3
3
|
* opencli daemon status — show daemon state
|
|
4
4
|
* opencli daemon stop — graceful shutdown
|
|
5
|
+
* opencli daemon restart — graceful shutdown, then start a fresh daemon
|
|
5
6
|
*/
|
|
6
7
|
export declare function daemonStatus(): Promise<void>;
|
|
7
8
|
export declare function daemonStop(): Promise<void>;
|
|
9
|
+
export declare function daemonRestart(): Promise<void>;
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* CLI commands for daemon lifecycle:
|
|
3
3
|
* opencli daemon status — show daemon state
|
|
4
4
|
* opencli daemon stop — graceful shutdown
|
|
5
|
+
* opencli daemon restart — graceful shutdown, then start a fresh daemon
|
|
5
6
|
*/
|
|
6
7
|
import { styleText } from 'node:util';
|
|
7
8
|
import { fetchDaemonStatus, requestDaemonShutdown } from '../browser/daemon-client.js';
|
|
9
|
+
import { restartDaemon } from '../browser/daemon-lifecycle.js';
|
|
8
10
|
import { formatDuration } from '../download/progress.js';
|
|
9
11
|
import { log } from '../logger.js';
|
|
12
|
+
import { PKG_VERSION } from '../version.js';
|
|
13
|
+
import { formatDaemonVersion, isDaemonStale } from '../browser/daemon-version.js';
|
|
10
14
|
export async function daemonStatus() {
|
|
11
15
|
const status = await fetchDaemonStatus();
|
|
12
16
|
if (!status) {
|
|
@@ -18,9 +22,18 @@ export async function daemonStatus() {
|
|
|
18
22
|
: status.extensionVersion
|
|
19
23
|
? `${styleText('green', 'connected')} ${styleText('dim', `(v${status.extensionVersion})`)}`
|
|
20
24
|
: `${styleText('yellow', 'connected')} ${styleText('dim', '(version unknown)')}`;
|
|
21
|
-
|
|
25
|
+
const daemonVersion = formatDaemonVersion(status);
|
|
26
|
+
const stale = isDaemonStale(status, PKG_VERSION);
|
|
27
|
+
console.log(`Daemon: ${stale ? styleText('yellow', 'stale') : styleText('green', 'running')} (PID ${status.pid})`);
|
|
28
|
+
console.log(`Version: ${daemonVersion}${stale ? styleText('yellow', ` (CLI v${PKG_VERSION}; run: opencli daemon restart)`) : ''}`);
|
|
22
29
|
console.log(`Uptime: ${formatDuration(Math.round(status.uptime * 1000))}`);
|
|
23
30
|
console.log(`Extension: ${extensionLabel}`);
|
|
31
|
+
if (status.profiles && status.profiles.length > 0) {
|
|
32
|
+
console.log(`Profiles: ${status.profiles.map((profile) => {
|
|
33
|
+
const version = profile.extensionVersion ? ` v${profile.extensionVersion}` : '';
|
|
34
|
+
return `${profile.contextId}${version}`;
|
|
35
|
+
}).join(', ')}`);
|
|
36
|
+
}
|
|
24
37
|
console.log(`Memory: ${status.memoryMB} MB`);
|
|
25
38
|
console.log(`Port: ${status.port}`);
|
|
26
39
|
}
|
|
@@ -39,3 +52,31 @@ export async function daemonStop() {
|
|
|
39
52
|
process.exitCode = 1;
|
|
40
53
|
}
|
|
41
54
|
}
|
|
55
|
+
export async function daemonRestart() {
|
|
56
|
+
const before = await fetchDaemonStatus();
|
|
57
|
+
if (before?.profiles && before.profiles.length > 0) {
|
|
58
|
+
log.warn(`Restarting daemon will disconnect ${before.profiles.length} browser profile(s); the extension should reconnect automatically.`);
|
|
59
|
+
}
|
|
60
|
+
const result = await restartDaemon();
|
|
61
|
+
if (!result.stopped) {
|
|
62
|
+
log.error('Failed to stop daemon before restart.');
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!result.status) {
|
|
67
|
+
log.error('Daemon restart timed out before the new daemon reported status.');
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const action = result.previousStatus ? 'restarted' : 'started';
|
|
72
|
+
const version = formatDaemonVersion(result.status);
|
|
73
|
+
log.success(`Daemon ${action} on port ${result.status.port} (${version}).`);
|
|
74
|
+
if (result.status.extensionConnected) {
|
|
75
|
+
const profiles = result.status.profiles?.length ?? 0;
|
|
76
|
+
const profileText = profiles > 0 ? `; profiles connected: ${profiles}` : '';
|
|
77
|
+
log.status(`Extension connected${profileText}.`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
log.warn('Daemon is running, but the Browser Bridge extension has not connected yet.');
|
|
81
|
+
}
|
|
82
|
+
}
|