@jackwener/opencli 1.7.7 → 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 +782 -55
- package/clis/36kr/news.js +1 -1
- package/clis/amazon/discussion.js +37 -6
- package/clis/amazon/discussion.test.js +147 -32
- 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 +4 -20
- package/clis/chatgpt-app/ax.js +135 -2
- package/clis/chatgpt-app/ax.test.js +35 -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 +3 -22
- 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 +49 -10
- package/clis/deepseek/ask.test.js +150 -3
- package/clis/deepseek/utils.js +60 -22
- 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/powerchina/search.js +250 -0
- package/clis/powerchina/search.test.js +67 -0
- 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 +6 -3
- package/clis/sinafinance/stock.test.js +59 -0
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +80 -0
- package/clis/toutiao/articles.test.js +30 -0
- 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/weixin/create-draft.js +225 -0
- package/clis/weixin/drafts.js +65 -0
- package/clis/weixin/drafts.test.js +65 -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 +29 -9
- package/dist/src/commanderAdapter.test.js +78 -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) => {
|
|
@@ -71,6 +71,18 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
71
71
|
if (v !== undefined)
|
|
72
72
|
rawKwargs[arg.name] = v;
|
|
73
73
|
}
|
|
74
|
+
const optionSources = {};
|
|
75
|
+
for (const arg of cmd.args) {
|
|
76
|
+
if (arg.positional)
|
|
77
|
+
continue;
|
|
78
|
+
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
|
|
79
|
+
const source = subCmd.getOptionValueSource(camelName) ?? subCmd.getOptionValueSource(arg.name);
|
|
80
|
+
if (source === 'cli')
|
|
81
|
+
optionSources[arg.name] = source;
|
|
82
|
+
}
|
|
83
|
+
if (Object.keys(optionSources).length > 0) {
|
|
84
|
+
rawKwargs.__opencliOptionSources = optionSources;
|
|
85
|
+
}
|
|
74
86
|
const kwargs = prepareCommandArgs(cmd, rawKwargs);
|
|
75
87
|
const verbose = optionsRecord.verbose === true;
|
|
76
88
|
let format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
|
|
@@ -82,7 +94,12 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
82
94
|
const replacement = cmd.replacedBy ? ` Use ${cmd.replacedBy} instead.` : '';
|
|
83
95
|
log.warn(`Deprecated: ${message}${replacement}`);
|
|
84
96
|
}
|
|
85
|
-
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
|
+
});
|
|
86
103
|
if (result === null || result === undefined) {
|
|
87
104
|
return;
|
|
88
105
|
}
|
|
@@ -104,7 +121,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
104
121
|
});
|
|
105
122
|
}
|
|
106
123
|
catch (err) {
|
|
107
|
-
renderError(err, fullName(cmd), optionsRecord.verbose === true);
|
|
124
|
+
renderError(err, fullName(cmd), optionsRecord.verbose === true, optionsRecord.trace);
|
|
108
125
|
process.exitCode = resolveExitCode(err);
|
|
109
126
|
}
|
|
110
127
|
});
|
|
@@ -116,13 +133,16 @@ function resolveExitCode(err) {
|
|
|
116
133
|
return EXIT_CODES.GENERIC_ERROR;
|
|
117
134
|
}
|
|
118
135
|
// ── Error rendering ─────────────────────────────────────────────────────────
|
|
119
|
-
/** Emit AutoFix hint for repairable adapter errors (skipped if already
|
|
120
|
-
function emitAutoFixHint(envelope, cmdName) {
|
|
121
|
-
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')
|
|
122
139
|
return envelope;
|
|
123
|
-
|
|
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`;
|
|
124
144
|
}
|
|
125
|
-
function renderError(err, cmdName, verbose) {
|
|
145
|
+
function renderError(err, cmdName, verbose, traceMode) {
|
|
126
146
|
const envelope = toEnvelope(err);
|
|
127
147
|
// In verbose mode, include stack trace for debugging
|
|
128
148
|
if (verbose && err instanceof Error && err.stack) {
|
|
@@ -132,7 +152,7 @@ function renderError(err, cmdName, verbose) {
|
|
|
132
152
|
// Append AutoFix hint for repairable errors
|
|
133
153
|
const code = envelope.error.code;
|
|
134
154
|
if (code === 'SELECTOR' || code === 'EMPTY_RESULT' || code === 'ADAPTER_LOAD' || code === 'UNKNOWN') {
|
|
135
|
-
output = emitAutoFixHint(output, cmdName);
|
|
155
|
+
output = emitAutoFixHint(output, cmdName, traceMode);
|
|
136
156
|
}
|
|
137
157
|
process.stderr.write(output);
|
|
138
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(),
|
|
@@ -56,6 +56,24 @@ describe('commanderAdapter arg passing', () => {
|
|
|
56
56
|
expect(kwargs.pdf).toBe('./paper.pdf');
|
|
57
57
|
expect(kwargs['prepare-only']).toBe(true);
|
|
58
58
|
});
|
|
59
|
+
it('passes option value sources through for adapters that need explicit-vs-default semantics', async () => {
|
|
60
|
+
const program = new Command();
|
|
61
|
+
const siteCmd = program.command('paperreview');
|
|
62
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
63
|
+
await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--prepare-only']);
|
|
64
|
+
expect(mockExecuteCommand).toHaveBeenCalled();
|
|
65
|
+
const kwargs = mockExecuteCommand.mock.calls[0][1];
|
|
66
|
+
expect(kwargs.__opencliOptionSources).toMatchObject({
|
|
67
|
+
'prepare-only': 'cli',
|
|
68
|
+
});
|
|
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
|
+
});
|
|
59
77
|
it('rejects invalid bool values before calling executeCommand', async () => {
|
|
60
78
|
const program = new Command();
|
|
61
79
|
const siteCmd = program.command('paperreview');
|
|
@@ -258,6 +276,9 @@ describe('commanderAdapter error envelope output', () => {
|
|
|
258
276
|
expect(output).toContain('ok: false');
|
|
259
277
|
expect(output).toContain('code: EMPTY_RESULT');
|
|
260
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');
|
|
261
282
|
stderrSpy.mockRestore();
|
|
262
283
|
});
|
|
263
284
|
it('outputs YAML error envelope for selector errors', async () => {
|
|
@@ -265,12 +286,67 @@ describe('commanderAdapter error envelope output', () => {
|
|
|
265
286
|
const siteCmd = program.command('xiaohongshu');
|
|
266
287
|
registerCommandToProgram(siteCmd, cmd);
|
|
267
288
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
268
|
-
mockExecuteCommand.mockRejectedValueOnce(
|
|
289
|
+
mockExecuteCommand.mockRejectedValueOnce(selectorError('.note-title', 'The note title selector no longer matches the current page.'));
|
|
269
290
|
await program.parseAsync(['node', 'opencli', 'xiaohongshu', 'note', '69ca3927000000001a020fd5']);
|
|
270
291
|
const output = stderrSpy.mock.calls.map(c => String(c[0])).join('');
|
|
271
292
|
expect(output).toContain('ok: false');
|
|
272
293
|
expect(output).toContain('code: SELECTOR');
|
|
273
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');
|
|
274
350
|
stderrSpy.mockRestore();
|
|
275
351
|
});
|
|
276
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', () => {
|