@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
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
const { fetchDaemonStatusMock, requestDaemonShutdownMock, } = vi.hoisted(() => ({
|
|
2
|
+
const { fetchDaemonStatusMock, requestDaemonShutdownMock, restartDaemonMock, } = vi.hoisted(() => ({
|
|
3
3
|
fetchDaemonStatusMock: vi.fn(),
|
|
4
4
|
requestDaemonShutdownMock: vi.fn(),
|
|
5
|
+
restartDaemonMock: vi.fn(),
|
|
5
6
|
}));
|
|
6
7
|
vi.mock('../browser/daemon-client.js', () => ({
|
|
7
8
|
fetchDaemonStatus: fetchDaemonStatusMock,
|
|
8
9
|
requestDaemonShutdown: requestDaemonShutdownMock,
|
|
9
10
|
}));
|
|
10
|
-
|
|
11
|
+
vi.mock('../browser/daemon-lifecycle.js', () => ({
|
|
12
|
+
restartDaemon: restartDaemonMock,
|
|
13
|
+
}));
|
|
14
|
+
import { daemonRestart, daemonStatus, daemonStop } from './daemon.js';
|
|
15
|
+
import { PKG_VERSION } from '../version.js';
|
|
11
16
|
describe('daemonStatus', () => {
|
|
12
17
|
let stdoutSpy;
|
|
13
18
|
beforeEach(() => {
|
|
@@ -28,6 +33,7 @@ describe('daemonStatus', () => {
|
|
|
28
33
|
ok: true,
|
|
29
34
|
pid: 12345,
|
|
30
35
|
uptime: 3661,
|
|
36
|
+
daemonVersion: PKG_VERSION,
|
|
31
37
|
extensionConnected: true,
|
|
32
38
|
extensionVersion: '1.6.8',
|
|
33
39
|
pending: 0,
|
|
@@ -37,6 +43,7 @@ describe('daemonStatus', () => {
|
|
|
37
43
|
await daemonStatus();
|
|
38
44
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
|
|
39
45
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('PID 12345'));
|
|
46
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining(`v${PKG_VERSION}`));
|
|
40
47
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('1h 1m'));
|
|
41
48
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('connected'));
|
|
42
49
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('v1.6.8'));
|
|
@@ -48,6 +55,7 @@ describe('daemonStatus', () => {
|
|
|
48
55
|
ok: true,
|
|
49
56
|
pid: 99,
|
|
50
57
|
uptime: 120,
|
|
58
|
+
daemonVersion: PKG_VERSION,
|
|
51
59
|
extensionConnected: false,
|
|
52
60
|
pending: 0,
|
|
53
61
|
memoryMB: 32,
|
|
@@ -61,6 +69,7 @@ describe('daemonStatus', () => {
|
|
|
61
69
|
ok: true,
|
|
62
70
|
pid: 99,
|
|
63
71
|
uptime: 120,
|
|
72
|
+
daemonVersion: PKG_VERSION,
|
|
64
73
|
extensionConnected: true,
|
|
65
74
|
extensionVersion: undefined,
|
|
66
75
|
pending: 0,
|
|
@@ -91,6 +100,7 @@ describe('daemonStop', () => {
|
|
|
91
100
|
ok: true,
|
|
92
101
|
pid: 12345,
|
|
93
102
|
uptime: 100,
|
|
103
|
+
daemonVersion: PKG_VERSION,
|
|
94
104
|
extensionConnected: true,
|
|
95
105
|
pending: 0,
|
|
96
106
|
memoryMB: 50,
|
|
@@ -106,6 +116,7 @@ describe('daemonStop', () => {
|
|
|
106
116
|
ok: true,
|
|
107
117
|
pid: 12345,
|
|
108
118
|
uptime: 100,
|
|
119
|
+
daemonVersion: PKG_VERSION,
|
|
109
120
|
extensionConnected: true,
|
|
110
121
|
pending: 0,
|
|
111
122
|
memoryMB: 50,
|
|
@@ -116,3 +127,93 @@ describe('daemonStop', () => {
|
|
|
116
127
|
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon'));
|
|
117
128
|
});
|
|
118
129
|
});
|
|
130
|
+
describe('daemonRestart', () => {
|
|
131
|
+
let stderrSpy;
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
134
|
+
fetchDaemonStatusMock.mockReset();
|
|
135
|
+
requestDaemonShutdownMock.mockReset();
|
|
136
|
+
restartDaemonMock.mockReset();
|
|
137
|
+
process.exitCode = undefined;
|
|
138
|
+
});
|
|
139
|
+
afterEach(() => {
|
|
140
|
+
vi.restoreAllMocks();
|
|
141
|
+
process.exitCode = undefined;
|
|
142
|
+
});
|
|
143
|
+
it('restarts a running daemon and reports the new version', async () => {
|
|
144
|
+
fetchDaemonStatusMock.mockResolvedValue({
|
|
145
|
+
ok: true,
|
|
146
|
+
pid: 12345,
|
|
147
|
+
uptime: 100,
|
|
148
|
+
daemonVersion: '1.7.6',
|
|
149
|
+
extensionConnected: true,
|
|
150
|
+
profiles: [{ contextId: 'work', extensionConnected: true, pending: 0 }],
|
|
151
|
+
pending: 0,
|
|
152
|
+
memoryMB: 50,
|
|
153
|
+
port: 19825,
|
|
154
|
+
});
|
|
155
|
+
restartDaemonMock.mockResolvedValue({
|
|
156
|
+
previousStatus: { daemonVersion: '1.7.6' },
|
|
157
|
+
stopped: true,
|
|
158
|
+
spawned: true,
|
|
159
|
+
status: {
|
|
160
|
+
ok: true,
|
|
161
|
+
pid: 12346,
|
|
162
|
+
uptime: 1,
|
|
163
|
+
daemonVersion: PKG_VERSION,
|
|
164
|
+
extensionConnected: true,
|
|
165
|
+
profiles: [{ contextId: 'work', extensionConnected: true, pending: 0 }],
|
|
166
|
+
pending: 0,
|
|
167
|
+
memoryMB: 51,
|
|
168
|
+
port: 19825,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
await daemonRestart();
|
|
172
|
+
expect(restartDaemonMock).toHaveBeenCalledTimes(1);
|
|
173
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('will disconnect 1 browser profile'));
|
|
174
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon restarted on port 19825 (v${PKG_VERSION})`));
|
|
175
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Extension connected; profiles connected: 1'));
|
|
176
|
+
});
|
|
177
|
+
it('starts a new daemon when none was running', async () => {
|
|
178
|
+
fetchDaemonStatusMock.mockResolvedValue(null);
|
|
179
|
+
restartDaemonMock.mockResolvedValue({
|
|
180
|
+
previousStatus: null,
|
|
181
|
+
stopped: true,
|
|
182
|
+
spawned: true,
|
|
183
|
+
status: {
|
|
184
|
+
ok: true,
|
|
185
|
+
pid: 12346,
|
|
186
|
+
uptime: 1,
|
|
187
|
+
daemonVersion: PKG_VERSION,
|
|
188
|
+
extensionConnected: false,
|
|
189
|
+
pending: 0,
|
|
190
|
+
memoryMB: 51,
|
|
191
|
+
port: 19825,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
await daemonRestart();
|
|
195
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon started on port 19825 (v${PKG_VERSION})`));
|
|
196
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('extension has not connected yet'));
|
|
197
|
+
});
|
|
198
|
+
it('reports failure when the daemon cannot stop', async () => {
|
|
199
|
+
fetchDaemonStatusMock.mockResolvedValue({
|
|
200
|
+
ok: true,
|
|
201
|
+
pid: 12345,
|
|
202
|
+
uptime: 100,
|
|
203
|
+
daemonVersion: '1.7.6',
|
|
204
|
+
extensionConnected: true,
|
|
205
|
+
pending: 0,
|
|
206
|
+
memoryMB: 50,
|
|
207
|
+
port: 19825,
|
|
208
|
+
});
|
|
209
|
+
restartDaemonMock.mockResolvedValue({
|
|
210
|
+
previousStatus: { daemonVersion: '1.7.6' },
|
|
211
|
+
status: { daemonVersion: '1.7.6' },
|
|
212
|
+
stopped: false,
|
|
213
|
+
spawned: false,
|
|
214
|
+
});
|
|
215
|
+
await daemonRestart();
|
|
216
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon before restart'));
|
|
217
|
+
expect(process.exitCode).toBe(1);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -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', () => {
|
package/dist/src/daemon.js
CHANGED
|
@@ -25,19 +25,96 @@ import { DEFAULT_DAEMON_PORT } from './constants.js';
|
|
|
25
25
|
import { EXIT_CODES } from './errors.js';
|
|
26
26
|
import { log } from './logger.js';
|
|
27
27
|
import { PKG_VERSION } from './version.js';
|
|
28
|
+
import { DEFAULT_CONTEXT_ID } from './browser/profile.js';
|
|
29
|
+
import { recordExtensionVersion } from './update-check.js';
|
|
28
30
|
const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
29
|
-
|
|
30
|
-
let extensionWs = null;
|
|
31
|
-
let extensionVersion = null;
|
|
32
|
-
let extensionCompatRange = null;
|
|
31
|
+
const extensionProfiles = new Map();
|
|
33
32
|
const pending = new Map();
|
|
34
33
|
const LOG_BUFFER_SIZE = 200;
|
|
35
34
|
const logBuffer = [];
|
|
35
|
+
class DaemonCommandFailure extends Error {
|
|
36
|
+
errorCode;
|
|
37
|
+
errorHint;
|
|
38
|
+
status;
|
|
39
|
+
constructor(message, errorCode, errorHint, status = 400) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.errorCode = errorCode;
|
|
42
|
+
this.errorHint = errorHint;
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.name = 'DaemonCommandFailure';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
36
47
|
function pushLog(entry) {
|
|
37
48
|
logBuffer.push(entry);
|
|
38
49
|
if (logBuffer.length > LOG_BUFFER_SIZE)
|
|
39
50
|
logBuffer.shift();
|
|
40
51
|
}
|
|
52
|
+
function activeProfiles() {
|
|
53
|
+
return [...extensionProfiles.values()].filter((entry) => entry.ws.readyState === WebSocket.OPEN);
|
|
54
|
+
}
|
|
55
|
+
function resolveExtensionConnection(contextId) {
|
|
56
|
+
const requestedContextId = typeof contextId === 'string' && contextId.trim() ? contextId.trim() : undefined;
|
|
57
|
+
if (requestedContextId) {
|
|
58
|
+
const connection = extensionProfiles.get(requestedContextId);
|
|
59
|
+
if (connection?.ws.readyState === WebSocket.OPEN)
|
|
60
|
+
return { connection };
|
|
61
|
+
return {
|
|
62
|
+
errorCode: 'profile_disconnected',
|
|
63
|
+
error: `Browser profile "${requestedContextId}" is not connected.`,
|
|
64
|
+
errorHint: 'Open that Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const connected = activeProfiles();
|
|
68
|
+
if (connected.length === 1)
|
|
69
|
+
return { connection: connected[0] };
|
|
70
|
+
if (connected.length > 1) {
|
|
71
|
+
return {
|
|
72
|
+
errorCode: 'profile_required',
|
|
73
|
+
error: 'Multiple Browser Bridge profiles are connected; choose one with --profile.',
|
|
74
|
+
errorHint: 'Run opencli profile list, then use opencli --profile <name> ... or opencli profile use <name>.',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
errorCode: 'extension_not_connected',
|
|
79
|
+
error: 'Extension not connected. Please install the opencli Browser Bridge extension.',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function registerExtensionConnection(ws, rawContextId) {
|
|
83
|
+
const contextId = typeof rawContextId === 'string' && rawContextId.trim()
|
|
84
|
+
? rawContextId.trim()
|
|
85
|
+
: DEFAULT_CONTEXT_ID;
|
|
86
|
+
const previous = extensionProfiles.get(contextId);
|
|
87
|
+
if (previous && previous.ws !== ws) {
|
|
88
|
+
previous.ws.close();
|
|
89
|
+
}
|
|
90
|
+
const existing = [...extensionProfiles.entries()].find(([, entry]) => entry.ws === ws);
|
|
91
|
+
if (existing && existing[0] !== contextId)
|
|
92
|
+
extensionProfiles.delete(existing[0]);
|
|
93
|
+
const current = extensionProfiles.get(contextId);
|
|
94
|
+
const connection = {
|
|
95
|
+
contextId,
|
|
96
|
+
ws,
|
|
97
|
+
extensionVersion: current?.ws === ws ? current.extensionVersion : null,
|
|
98
|
+
extensionCompatRange: current?.ws === ws ? current.extensionCompatRange : null,
|
|
99
|
+
lastSeenAt: Date.now(),
|
|
100
|
+
};
|
|
101
|
+
extensionProfiles.set(contextId, connection);
|
|
102
|
+
return connection;
|
|
103
|
+
}
|
|
104
|
+
function unregisterExtensionConnection(ws) {
|
|
105
|
+
for (const [contextId, connection] of extensionProfiles.entries()) {
|
|
106
|
+
if (connection.ws !== ws)
|
|
107
|
+
continue;
|
|
108
|
+
extensionProfiles.delete(contextId);
|
|
109
|
+
for (const [id, p] of pending) {
|
|
110
|
+
if (p.contextId !== contextId)
|
|
111
|
+
continue;
|
|
112
|
+
clearTimeout(p.timer);
|
|
113
|
+
p.reject(new DaemonCommandFailure(`Browser profile "${contextId}" disconnected`, 'profile_disconnected', 'Open that Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.', 503));
|
|
114
|
+
pending.delete(id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
41
118
|
// ─── HTTP Server ─────────────────────────────────────────────────────
|
|
42
119
|
const MAX_BODY = 1024 * 1024; // 1 MB — commands are tiny; this prevents OOM
|
|
43
120
|
function readBody(req) {
|
|
@@ -118,14 +195,29 @@ async function handleRequest(req, res) {
|
|
|
118
195
|
if (req.method === 'GET' && pathname === '/status') {
|
|
119
196
|
const uptime = process.uptime();
|
|
120
197
|
const mem = process.memoryUsage();
|
|
198
|
+
const params = new URL(url, `http://localhost:${PORT}`).searchParams;
|
|
199
|
+
const requestedContextId = params.get('contextId')?.trim() || undefined;
|
|
200
|
+
const route = resolveExtensionConnection(requestedContextId);
|
|
201
|
+
const profiles = activeProfiles().map((profile) => ({
|
|
202
|
+
contextId: profile.contextId,
|
|
203
|
+
extensionConnected: true,
|
|
204
|
+
extensionVersion: profile.extensionVersion ?? undefined,
|
|
205
|
+
extensionCompatRange: profile.extensionCompatRange ?? undefined,
|
|
206
|
+
pending: [...pending.values()].filter((entry) => entry.contextId === profile.contextId).length,
|
|
207
|
+
lastSeenAt: profile.lastSeenAt,
|
|
208
|
+
}));
|
|
121
209
|
jsonResponse(res, 200, {
|
|
122
210
|
ok: true,
|
|
123
211
|
pid: process.pid,
|
|
124
212
|
uptime,
|
|
125
213
|
daemonVersion: PKG_VERSION,
|
|
126
|
-
extensionConnected:
|
|
127
|
-
extensionVersion,
|
|
128
|
-
extensionCompatRange,
|
|
214
|
+
extensionConnected: !!route.connection,
|
|
215
|
+
extensionVersion: route.connection?.extensionVersion ?? undefined,
|
|
216
|
+
extensionCompatRange: route.connection?.extensionCompatRange ?? undefined,
|
|
217
|
+
contextId: route.connection?.contextId ?? requestedContextId,
|
|
218
|
+
profileRequired: route.errorCode === 'profile_required',
|
|
219
|
+
profileDisconnected: route.errorCode === 'profile_disconnected',
|
|
220
|
+
profiles,
|
|
129
221
|
pending: pending.size,
|
|
130
222
|
memoryMB: Math.round(mem.rss / 1024 / 1024 * 10) / 10,
|
|
131
223
|
port: PORT,
|
|
@@ -158,8 +250,15 @@ async function handleRequest(req, res) {
|
|
|
158
250
|
jsonResponse(res, 400, { ok: false, error: 'Missing command id' });
|
|
159
251
|
return;
|
|
160
252
|
}
|
|
161
|
-
|
|
162
|
-
|
|
253
|
+
const route = resolveExtensionConnection(typeof body.contextId === 'string' ? body.contextId : undefined);
|
|
254
|
+
if (!route.connection) {
|
|
255
|
+
jsonResponse(res, route.errorCode === 'profile_required' ? 409 : 503, {
|
|
256
|
+
id: body.id,
|
|
257
|
+
ok: false,
|
|
258
|
+
errorCode: route.errorCode,
|
|
259
|
+
error: route.error,
|
|
260
|
+
...(route.errorHint ? { errorHint: route.errorHint } : {}),
|
|
261
|
+
});
|
|
163
262
|
return;
|
|
164
263
|
}
|
|
165
264
|
const timeoutMs = typeof body.timeout === 'number' && body.timeout > 0
|
|
@@ -178,15 +277,18 @@ async function handleRequest(req, res) {
|
|
|
178
277
|
pending.delete(body.id);
|
|
179
278
|
reject(new Error(`Command timeout (${timeoutMs / 1000}s)`));
|
|
180
279
|
}, timeoutMs);
|
|
181
|
-
pending.set(body.id, { resolve, reject, timer });
|
|
182
|
-
|
|
280
|
+
pending.set(body.id, { contextId: route.connection.contextId, resolve, reject, timer });
|
|
281
|
+
route.connection.ws.send(JSON.stringify(body));
|
|
183
282
|
});
|
|
184
283
|
jsonResponse(res, 200, result);
|
|
185
284
|
}
|
|
186
285
|
catch (err) {
|
|
187
|
-
|
|
286
|
+
const commandFailure = err instanceof DaemonCommandFailure ? err : null;
|
|
287
|
+
jsonResponse(res, commandFailure?.status ?? (err instanceof Error && err.message.includes('timeout') ? 408 : 400), {
|
|
188
288
|
ok: false,
|
|
189
289
|
error: err instanceof Error ? err.message : 'Invalid request',
|
|
290
|
+
...(commandFailure?.errorCode ? { errorCode: commandFailure.errorCode } : {}),
|
|
291
|
+
...(commandFailure?.errorHint ? { errorHint: commandFailure.errorHint } : {}),
|
|
190
292
|
});
|
|
191
293
|
}
|
|
192
294
|
return;
|
|
@@ -209,9 +311,6 @@ const wss = new WebSocketServer({
|
|
|
209
311
|
});
|
|
210
312
|
wss.on('connection', (ws) => {
|
|
211
313
|
log.info('[daemon] Extension connected');
|
|
212
|
-
extensionWs = ws;
|
|
213
|
-
extensionVersion = null; // cleared until hello message arrives
|
|
214
|
-
extensionCompatRange = null;
|
|
215
314
|
// ── Heartbeat: ping every 15s, close if 2 pongs missed ──
|
|
216
315
|
let missedPongs = 0;
|
|
217
316
|
const heartbeatInterval = setInterval(() => {
|
|
@@ -236,8 +335,13 @@ wss.on('connection', (ws) => {
|
|
|
236
335
|
const msg = JSON.parse(data.toString());
|
|
237
336
|
// Handle hello message from extension (version handshake)
|
|
238
337
|
if (msg.type === 'hello') {
|
|
239
|
-
|
|
240
|
-
|
|
338
|
+
const connection = registerExtensionConnection(ws, msg.contextId);
|
|
339
|
+
connection.extensionVersion = typeof msg.version === 'string' ? msg.version : null;
|
|
340
|
+
connection.extensionCompatRange = typeof msg.compatRange === 'string' ? msg.compatRange : null;
|
|
341
|
+
connection.lastSeenAt = Date.now();
|
|
342
|
+
if (connection.extensionVersion)
|
|
343
|
+
recordExtensionVersion(connection.extensionVersion);
|
|
344
|
+
log.info(`[daemon] Extension profile connected: ${connection.contextId}`);
|
|
241
345
|
return;
|
|
242
346
|
}
|
|
243
347
|
// Handle log messages from extension
|
|
@@ -266,31 +370,11 @@ wss.on('connection', (ws) => {
|
|
|
266
370
|
ws.on('close', () => {
|
|
267
371
|
log.info('[daemon] Extension disconnected');
|
|
268
372
|
clearInterval(heartbeatInterval);
|
|
269
|
-
|
|
270
|
-
extensionWs = null;
|
|
271
|
-
extensionVersion = null;
|
|
272
|
-
extensionCompatRange = null;
|
|
273
|
-
// Reject all pending requests since the extension is gone
|
|
274
|
-
for (const [id, p] of pending) {
|
|
275
|
-
clearTimeout(p.timer);
|
|
276
|
-
p.reject(new Error('Extension disconnected'));
|
|
277
|
-
}
|
|
278
|
-
pending.clear();
|
|
279
|
-
}
|
|
373
|
+
unregisterExtensionConnection(ws);
|
|
280
374
|
});
|
|
281
375
|
ws.on('error', () => {
|
|
282
376
|
clearInterval(heartbeatInterval);
|
|
283
|
-
|
|
284
|
-
extensionWs = null;
|
|
285
|
-
extensionVersion = null;
|
|
286
|
-
extensionCompatRange = null;
|
|
287
|
-
// Reject pending requests in case 'close' does not follow this 'error'
|
|
288
|
-
for (const [, p] of pending) {
|
|
289
|
-
clearTimeout(p.timer);
|
|
290
|
-
p.reject(new Error('Extension disconnected'));
|
|
291
|
-
}
|
|
292
|
-
pending.clear();
|
|
293
|
-
}
|
|
377
|
+
unregisterExtensionConnection(ws);
|
|
294
378
|
});
|
|
295
379
|
});
|
|
296
380
|
// ─── Start ───────────────────────────────────────────────────────────
|
|
@@ -313,8 +397,8 @@ function shutdown() {
|
|
|
313
397
|
p.reject(new Error('Daemon shutting down'));
|
|
314
398
|
}
|
|
315
399
|
pending.clear();
|
|
316
|
-
|
|
317
|
-
|
|
400
|
+
for (const profile of extensionProfiles.values())
|
|
401
|
+
profile.ws.close();
|
|
318
402
|
httpServer.close();
|
|
319
403
|
process.exit(EXIT_CODES.SUCCESS);
|
|
320
404
|
}
|
package/dist/src/doctor.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Simplified for the daemon-based architecture.
|
|
5
5
|
*/
|
|
6
|
+
import type { BrowserSessionInfo } from './types.js';
|
|
7
|
+
import type { BrowserProfileStatus } from './browser/daemon-client.js';
|
|
6
8
|
export type DoctorOptions = {
|
|
7
9
|
yes?: boolean;
|
|
8
10
|
live?: boolean;
|
|
@@ -18,18 +20,15 @@ export type DoctorReport = {
|
|
|
18
20
|
cliVersion?: string;
|
|
19
21
|
daemonRunning: boolean;
|
|
20
22
|
daemonFlaky?: boolean;
|
|
23
|
+
daemonStale?: boolean;
|
|
21
24
|
daemonVersion?: string;
|
|
22
25
|
extensionConnected: boolean;
|
|
23
26
|
extensionFlaky?: boolean;
|
|
24
27
|
extensionVersion?: string;
|
|
25
28
|
latestExtensionVersion?: string;
|
|
26
29
|
connectivity?: ConnectivityResult;
|
|
27
|
-
sessions?:
|
|
28
|
-
|
|
29
|
-
windowId: number;
|
|
30
|
-
tabCount: number;
|
|
31
|
-
idleMsRemaining: number;
|
|
32
|
-
}>;
|
|
30
|
+
sessions?: BrowserSessionInfo[];
|
|
31
|
+
profiles?: BrowserProfileStatus[];
|
|
33
32
|
issues: string[];
|
|
34
33
|
};
|
|
35
34
|
/**
|
package/dist/src/doctor.js
CHANGED
|
@@ -10,6 +10,8 @@ import { getDaemonHealth, listSessions } from './browser/daemon-client.js';
|
|
|
10
10
|
import { getErrorMessage } from './errors.js';
|
|
11
11
|
import { getRuntimeLabel } from './runtime-detect.js';
|
|
12
12
|
import { getCachedLatestExtensionVersion } from './update-check.js';
|
|
13
|
+
import { aliasForContextId, loadProfileConfig } from './browser/profile.js';
|
|
14
|
+
import { formatDaemonVersion, isDaemonStale, staleDaemonIssue } from './browser/daemon-version.js';
|
|
13
15
|
const DOCTOR_LIVE_TIMEOUT_SECONDS = 8;
|
|
14
16
|
/** Parse a semver string into [major, minor, patch]. Returns null on invalid input. */
|
|
15
17
|
function parseSemver(v) {
|
|
@@ -48,9 +50,14 @@ export async function checkConnectivity(opts) {
|
|
|
48
50
|
try {
|
|
49
51
|
const bridge = new BrowserBridge();
|
|
50
52
|
const page = await bridge.connect({ timeout: opts?.timeout ?? DOCTOR_LIVE_TIMEOUT_SECONDS });
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
try {
|
|
54
|
+
// Try a simple eval to verify end-to-end connectivity.
|
|
55
|
+
await page.evaluate('1 + 1');
|
|
56
|
+
await page.closeWindow?.();
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await bridge.close();
|
|
60
|
+
}
|
|
54
61
|
return { ok: true, durationMs: Date.now() - start };
|
|
55
62
|
}
|
|
56
63
|
catch (err) {
|
|
@@ -84,9 +91,21 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
84
91
|
const extensionConnected = health.state === 'ready';
|
|
85
92
|
const daemonFlaky = !!(connectivity?.ok && !daemonRunning);
|
|
86
93
|
const extensionFlaky = !!(connectivity?.ok && daemonRunning && !extensionConnected);
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
const daemonStale = isDaemonStale(health.status, opts.cliVersion);
|
|
95
|
+
const profiles = health.status?.profiles;
|
|
96
|
+
let sessions;
|
|
97
|
+
if (opts.sessions) {
|
|
98
|
+
if (profiles && profiles.length > 0) {
|
|
99
|
+
const grouped = await Promise.all(profiles.map(async (profile) => {
|
|
100
|
+
const rows = await listSessions({ contextId: profile.contextId }).catch(() => []);
|
|
101
|
+
return rows.map((row) => ({ ...row, contextId: row.contextId ?? profile.contextId }));
|
|
102
|
+
}));
|
|
103
|
+
sessions = grouped.flat();
|
|
104
|
+
}
|
|
105
|
+
else if (health.state === 'ready') {
|
|
106
|
+
sessions = await listSessions();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
90
109
|
const extensionVersion = health.status?.extensionVersion;
|
|
91
110
|
const issues = [];
|
|
92
111
|
if (daemonFlaky) {
|
|
@@ -96,24 +115,25 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
96
115
|
else if (!daemonRunning) {
|
|
97
116
|
issues.push('Daemon is not running. It should start automatically when you run an opencli browser command.');
|
|
98
117
|
}
|
|
118
|
+
if (daemonStale && opts.cliVersion) {
|
|
119
|
+
issues.push(staleDaemonIssue(health.status, opts.cliVersion));
|
|
120
|
+
}
|
|
99
121
|
if (extensionFlaky) {
|
|
100
122
|
issues.push('Extension connection is unstable. The live browser test succeeded, but the daemon reported the extension disconnected immediately afterward.\n' +
|
|
101
123
|
'This usually means the Browser Bridge service worker is reconnecting slowly or Chrome suspended it.');
|
|
102
124
|
}
|
|
103
125
|
else if (daemonRunning && !extensionConnected) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
'The daemon was started by an older CLI version and may have missed the extension registration.\n' +
|
|
112
|
-
' Quick fix: opencli daemon stop && opencli doctor');
|
|
126
|
+
if (health.state === 'profile-required') {
|
|
127
|
+
issues.push('Multiple Chrome profiles are connected to the daemon, but no default profile was selected.\n' +
|
|
128
|
+
' Run opencli profile list, then opencli profile use <name>, or pass --profile <name>.');
|
|
129
|
+
}
|
|
130
|
+
else if (health.state === 'profile-disconnected') {
|
|
131
|
+
issues.push(`Selected browser profile is not connected: ${health.status?.contextId ?? 'unknown'}.\n` +
|
|
132
|
+
' Open that Chrome profile and make sure the OpenCLI extension is enabled.');
|
|
113
133
|
}
|
|
114
134
|
else {
|
|
115
135
|
issues.push('Daemon is running but the Chrome/Chromium extension is not connected.\n' +
|
|
116
|
-
'If the extension is already installed, try: opencli daemon
|
|
136
|
+
'If the extension is already installed, try: opencli daemon restart\n' +
|
|
117
137
|
'If the extension is not installed:\n' +
|
|
118
138
|
' 1. Download from https://github.com/jackwener/opencli/releases\n' +
|
|
119
139
|
' 2. Open chrome://extensions/ → Enable Developer Mode\n' +
|
|
@@ -155,6 +175,7 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
155
175
|
cliVersion: opts.cliVersion,
|
|
156
176
|
daemonRunning,
|
|
157
177
|
daemonFlaky,
|
|
178
|
+
daemonStale,
|
|
158
179
|
daemonVersion: health.status?.daemonVersion,
|
|
159
180
|
extensionConnected,
|
|
160
181
|
extensionFlaky,
|
|
@@ -162,6 +183,7 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
162
183
|
latestExtensionVersion,
|
|
163
184
|
connectivity,
|
|
164
185
|
sessions,
|
|
186
|
+
profiles,
|
|
165
187
|
issues,
|
|
166
188
|
};
|
|
167
189
|
}
|
|
@@ -170,10 +192,16 @@ export function renderBrowserDoctorReport(report) {
|
|
|
170
192
|
// Daemon status
|
|
171
193
|
const daemonIcon = report.daemonFlaky
|
|
172
194
|
? styleText('yellow', '[WARN]')
|
|
173
|
-
: report.
|
|
195
|
+
: report.daemonStale
|
|
196
|
+
? styleText('yellow', '[WARN]')
|
|
197
|
+
: report.daemonRunning ? styleText('green', '[OK]') : styleText('red', '[MISSING]');
|
|
174
198
|
const daemonLabel = report.daemonFlaky
|
|
175
199
|
? 'unstable (running during live check, then stopped)'
|
|
176
|
-
: report.daemonRunning
|
|
200
|
+
: report.daemonRunning
|
|
201
|
+
? `running on port ${DEFAULT_DAEMON_PORT} (${report.daemonStale
|
|
202
|
+
? `${formatDaemonVersion(report)}, stale; CLI v${report.cliVersion ?? 'unknown'}`
|
|
203
|
+
: formatDaemonVersion(report)})`
|
|
204
|
+
: 'not running';
|
|
177
205
|
lines.push(`${daemonIcon} Daemon: ${daemonLabel}`);
|
|
178
206
|
// Extension status
|
|
179
207
|
const extIcon = report.extensionFlaky || (report.extensionConnected && !report.extensionVersion)
|
|
@@ -191,6 +219,17 @@ export function renderBrowserDoctorReport(report) {
|
|
|
191
219
|
? 'unstable (connected during live check, then disconnected)'
|
|
192
220
|
: report.extensionConnected ? 'connected' : 'not connected';
|
|
193
221
|
lines.push(`${extIcon} Extension: ${extLabel}${extVersion}`);
|
|
222
|
+
if (report.profiles && report.profiles.length > 0) {
|
|
223
|
+
const config = loadProfileConfig();
|
|
224
|
+
lines.push('', styleText('bold', 'Profiles:'));
|
|
225
|
+
for (const profile of report.profiles) {
|
|
226
|
+
const alias = aliasForContextId(config, profile.contextId);
|
|
227
|
+
const aliasText = alias ? ` (${alias})` : '';
|
|
228
|
+
const defaultText = config.defaultContextId === profile.contextId ? ', default' : '';
|
|
229
|
+
const version = profile.extensionVersion ? `v${profile.extensionVersion}` : 'version unknown';
|
|
230
|
+
lines.push(styleText('dim', ` • ${profile.contextId}${aliasText}: connected ${version}${defaultText}`));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
194
233
|
// Connectivity
|
|
195
234
|
if (report.connectivity) {
|
|
196
235
|
const connIcon = report.connectivity.ok ? styleText('green', '[OK]') : styleText('red', '[FAIL]');
|
|
@@ -208,8 +247,27 @@ export function renderBrowserDoctorReport(report) {
|
|
|
208
247
|
lines.push(styleText('dim', ' • no active automation sessions'));
|
|
209
248
|
}
|
|
210
249
|
else {
|
|
250
|
+
const byContext = new Map();
|
|
211
251
|
for (const session of report.sessions) {
|
|
212
|
-
|
|
252
|
+
const contextId = typeof session.contextId === 'string' && session.contextId ? session.contextId : 'default';
|
|
253
|
+
const rows = byContext.get(contextId) ?? [];
|
|
254
|
+
rows.push(session);
|
|
255
|
+
byContext.set(contextId, rows);
|
|
256
|
+
}
|
|
257
|
+
for (const [contextId, rows] of byContext) {
|
|
258
|
+
if (byContext.size > 1)
|
|
259
|
+
lines.push(styleText('dim', ` [profile: ${contextId}]`));
|
|
260
|
+
for (const session of rows) {
|
|
261
|
+
const idle = session.idleMsRemaining == null
|
|
262
|
+
? 'none'
|
|
263
|
+
: `${Math.ceil(session.idleMsRemaining / 1000)}s`;
|
|
264
|
+
const target = session.preferredTabId != null
|
|
265
|
+
? `tab ${session.preferredTabId}`
|
|
266
|
+
: `window ${session.windowId ?? 'unknown'}`;
|
|
267
|
+
const mode = session.ownership ?? (session.owned === false ? 'borrowed' : 'owned');
|
|
268
|
+
const surface = session.surface ? `, surface=${session.surface}` : '';
|
|
269
|
+
lines.push(styleText('dim', ` • ${session.workspace ?? 'default'} → ${target}, mode=${mode}${surface}, tabs=${session.tabCount ?? 0}, idle=${idle}`));
|
|
270
|
+
}
|
|
213
271
|
}
|
|
214
272
|
}
|
|
215
273
|
}
|