@jackwener/opencli 1.7.3 → 1.7.5
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 +81 -59
- package/README.zh-CN.md +93 -67
- package/cli-manifest.json +5015 -2975
- package/clis/antigravity/serve.js +71 -25
- package/clis/baidu-scholar/search.js +87 -0
- package/clis/baidu-scholar/search.test.js +23 -0
- package/clis/bilibili/favorite.js +18 -13
- package/clis/binance/depth.js +3 -4
- package/clis/boss/utils.js +2 -3
- package/clis/chatgpt-app/ax.js +6 -3
- package/clis/deepseek/ask.js +74 -0
- package/clis/deepseek/history.js +25 -0
- package/clis/deepseek/new.js +20 -0
- package/clis/deepseek/read.js +22 -0
- package/clis/deepseek/status.js +24 -0
- package/clis/deepseek/utils.js +208 -0
- package/clis/douban/search.js +1 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +20 -93
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/utils.js +250 -8
- package/clis/douban/utils.test.js +179 -4
- package/clis/doubao/utils.js +319 -130
- package/clis/doubao/utils.test.js +241 -2
- package/clis/eastmoney/_secid.js +78 -0
- package/clis/eastmoney/announcement.js +52 -0
- package/clis/eastmoney/convertible.js +73 -0
- package/clis/eastmoney/etf.js +65 -0
- package/clis/eastmoney/holders.js +78 -0
- package/clis/eastmoney/hot-rank.js +50 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/eastmoney/index-board.js +96 -0
- package/clis/eastmoney/kline.js +87 -0
- package/clis/eastmoney/kuaixun.js +54 -0
- package/clis/eastmoney/longhu.js +67 -0
- package/clis/eastmoney/money-flow.js +78 -0
- package/clis/eastmoney/northbound.js +57 -0
- package/clis/eastmoney/quote.js +107 -0
- package/clis/eastmoney/rank.js +94 -0
- package/clis/eastmoney/sectors.js +76 -0
- package/clis/google-scholar/search.js +58 -0
- package/clis/google-scholar/search.test.js +23 -0
- package/clis/gov-law/commands.test.js +39 -0
- package/clis/gov-law/recent.js +22 -0
- package/clis/gov-law/search.js +41 -0
- package/clis/gov-law/shared.js +51 -0
- package/clis/gov-policy/commands.test.js +27 -0
- package/clis/gov-policy/recent.js +47 -0
- package/clis/gov-policy/search.js +48 -0
- package/clis/grok/image.test.ts +107 -0
- package/clis/grok/image.ts +356 -0
- package/clis/nowcoder/companies.js +23 -0
- package/clis/nowcoder/creators.js +27 -0
- package/clis/nowcoder/detail.js +61 -0
- package/clis/nowcoder/experience.js +36 -0
- package/clis/nowcoder/hot.js +24 -0
- package/clis/nowcoder/jobs.js +21 -0
- package/clis/nowcoder/notifications.js +29 -0
- package/clis/nowcoder/papers.js +40 -0
- package/clis/nowcoder/practice.js +37 -0
- package/clis/nowcoder/recommend.js +30 -0
- package/clis/nowcoder/referral.js +39 -0
- package/clis/nowcoder/salary.js +40 -0
- package/clis/nowcoder/search.js +49 -0
- package/clis/nowcoder/suggest.js +33 -0
- package/clis/nowcoder/topics.js +27 -0
- package/clis/nowcoder/trending.js +25 -0
- package/clis/tdx/hot-rank.js +47 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/ths/hot-rank.js +49 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/twitter/list-add.js +337 -0
- package/clis/twitter/list-add.test.js +15 -0
- package/clis/twitter/list-remove.js +297 -0
- package/clis/twitter/list-remove.test.js +14 -0
- package/clis/twitter/list-tweets.js +185 -0
- package/clis/twitter/list-tweets.test.js +108 -0
- package/clis/twitter/lists.js +134 -47
- package/clis/twitter/lists.test.js +105 -38
- package/clis/uiverse/_shared.js +368 -0
- package/clis/uiverse/_shared.test.js +55 -0
- package/clis/uiverse/code.js +47 -0
- package/clis/uiverse/preview.js +71 -0
- package/clis/wanfang/search.js +66 -0
- package/clis/wanfang/search.test.js +23 -0
- package/clis/web/read.js +1 -1
- package/clis/weixin/download.js +3 -2
- package/clis/xiaohongshu/comments.js +2 -2
- package/clis/xiaohongshu/comments.test.js +46 -25
- package/clis/xiaohongshu/download.js +6 -7
- package/clis/xiaohongshu/download.test.js +17 -5
- package/clis/xiaohongshu/note-helpers.js +46 -12
- package/clis/xiaohongshu/note.js +3 -5
- package/clis/xiaohongshu/note.test.js +52 -25
- package/clis/xiaohongshu/publish.js +149 -28
- package/clis/xiaohongshu/publish.test.js +319 -6
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +53 -0
- package/clis/xiaoyuzhou/download.test.js +135 -0
- package/clis/xiaoyuzhou/episode.js +9 -4
- package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
- package/clis/xiaoyuzhou/podcast.js +9 -4
- package/clis/xiaoyuzhou/transcript.js +76 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/xiaoyuzhou/utils.js +0 -40
- package/clis/xiaoyuzhou/utils.test.js +15 -75
- package/clis/youtube/feed.js +120 -0
- package/clis/youtube/history.js +118 -0
- package/clis/youtube/like.js +62 -0
- package/clis/youtube/playlist.js +97 -0
- package/clis/youtube/subscribe.js +71 -0
- package/clis/youtube/subscriptions.js +57 -0
- package/clis/youtube/unlike.js +62 -0
- package/clis/youtube/unsubscribe.js +71 -0
- package/clis/youtube/utils.js +122 -0
- package/clis/youtube/utils.test.js +32 -1
- package/clis/youtube/watch-later.js +76 -0
- package/clis/zsxq/dynamics.js +1 -1
- package/clis/zsxq/utils.js +6 -3
- package/clis/zsxq/utils.test.js +31 -0
- package/dist/src/browser/base-page.d.ts +1 -1
- package/dist/src/browser/base-page.js +25 -5
- package/dist/src/browser/bridge.d.ts +3 -0
- package/dist/src/browser/bridge.js +52 -15
- package/dist/src/browser/cdp.js +2 -1
- package/dist/src/browser/daemon-client.d.ts +7 -4
- package/dist/src/browser/daemon-client.js +6 -1
- package/dist/src/browser/daemon-client.test.js +40 -1
- package/dist/src/browser/dom-snapshot.js +20 -3
- package/dist/src/browser/page.d.ts +18 -5
- package/dist/src/browser/page.js +96 -15
- package/dist/src/browser/page.test.js +158 -1
- package/dist/src/browser/target-errors.d.ts +23 -0
- package/dist/src/browser/target-errors.js +29 -0
- package/dist/src/browser/target-errors.test.js +61 -0
- package/dist/src/browser/target-resolver.d.ts +57 -0
- package/dist/src/browser/target-resolver.js +298 -0
- package/dist/src/browser/target-resolver.test.js +43 -0
- package/dist/src/browser.test.js +38 -1
- package/dist/src/cli.js +272 -187
- package/dist/src/cli.test.js +167 -90
- package/dist/src/commanderAdapter.d.ts +0 -1
- package/dist/src/commanderAdapter.js +2 -16
- package/dist/src/commanderAdapter.test.js +1 -1
- package/dist/src/commands/daemon.d.ts +4 -2
- package/dist/src/commands/daemon.js +22 -2
- package/dist/src/commands/daemon.test.js +65 -2
- package/dist/src/completion-shared.js +2 -5
- package/dist/src/daemon.js +10 -0
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +32 -9
- package/dist/src/doctor.test.js +28 -12
- package/dist/src/download/article-download.d.ts +1 -0
- package/dist/src/download/article-download.js +3 -0
- package/dist/src/download/article-download.test.js +39 -0
- package/dist/src/external-clis.yaml +2 -2
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +3 -3
- package/dist/src/output.js +1 -5
- package/dist/src/output.test.js +0 -21
- package/dist/src/pipeline/steps/transform.js +1 -1
- package/dist/src/pipeline/template.d.ts +1 -0
- package/dist/src/pipeline/template.js +11 -3
- package/dist/src/pipeline/template.test.js +3 -0
- package/dist/src/pipeline/transform.test.js +14 -0
- package/dist/src/plugin.d.ts +8 -9
- package/dist/src/plugin.js +24 -28
- package/dist/src/plugin.test.js +16 -60
- package/dist/src/registry.d.ts +1 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/registry.test.js +22 -0
- package/dist/src/types.d.ts +15 -6
- package/package.json +1 -1
- package/clis/twitter/lists-parser.js +0 -77
- package/clis/twitter/lists.d.ts +0 -5
- package/dist/src/cascade.d.ts +0 -46
- package/dist/src/cascade.js +0 -135
- package/dist/src/explore.d.ts +0 -99
- package/dist/src/explore.js +0 -402
- package/dist/src/generate-verified.d.ts +0 -105
- package/dist/src/generate-verified.js +0 -696
- package/dist/src/generate-verified.test.js +0 -925
- package/dist/src/generate.d.ts +0 -46
- package/dist/src/generate.js +0 -117
- package/dist/src/record.d.ts +0 -96
- package/dist/src/record.js +0 -657
- package/dist/src/record.test.js +0 -293
- package/dist/src/skill-generate.d.ts +0 -30
- package/dist/src/skill-generate.js +0 -75
- package/dist/src/skill-generate.test.js +0 -173
- package/dist/src/synthesize.d.ts +0 -97
- package/dist/src/synthesize.js +0 -208
- /package/dist/src/{generate-verified.test.d.ts → browser/target-errors.test.d.ts} +0 -0
- /package/dist/src/{record.test.d.ts → browser/target-resolver.test.d.ts} +0 -0
- /package/dist/src/{skill-generate.test.d.ts → download/article-download.test.d.ts} +0 -0
package/dist/src/cli.test.js
CHANGED
|
@@ -1,97 +1,22 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
2
4
|
import * as path from 'node:path';
|
|
3
|
-
const {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
mockRenderGenerateVerifiedSummary: vi.fn(),
|
|
8
|
-
mockRecordSession: vi.fn(),
|
|
9
|
-
mockRenderRecordSummary: vi.fn(),
|
|
10
|
-
mockCascadeProbe: vi.fn(),
|
|
11
|
-
mockRenderCascadeResult: vi.fn(),
|
|
12
|
-
mockGetBrowserFactory: vi.fn(() => ({ name: 'BrowserFactory' })),
|
|
13
|
-
mockBrowserSession: vi.fn(),
|
|
5
|
+
const { mockBrowserConnect, mockBrowserClose, browserState, } = vi.hoisted(() => ({
|
|
6
|
+
mockBrowserConnect: vi.fn(),
|
|
7
|
+
mockBrowserClose: vi.fn(),
|
|
8
|
+
browserState: { page: null },
|
|
14
9
|
}));
|
|
15
|
-
vi.mock('./
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
vi.mock('./record.js', () => ({
|
|
24
|
-
recordSession: mockRecordSession,
|
|
25
|
-
renderRecordSummary: mockRenderRecordSummary,
|
|
26
|
-
}));
|
|
27
|
-
vi.mock('./cascade.js', () => ({
|
|
28
|
-
cascadeProbe: mockCascadeProbe,
|
|
29
|
-
renderCascadeResult: mockRenderCascadeResult,
|
|
30
|
-
}));
|
|
31
|
-
vi.mock('./runtime.js', () => ({
|
|
32
|
-
getBrowserFactory: mockGetBrowserFactory,
|
|
33
|
-
browserSession: mockBrowserSession,
|
|
34
|
-
}));
|
|
35
|
-
import { createProgram, findPackageRoot, resolveBrowserVerifyInvocation } from './cli.js';
|
|
36
|
-
describe('built-in browser commands verbose wiring', () => {
|
|
37
|
-
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
delete process.env.OPENCLI_VERBOSE;
|
|
40
|
-
process.exitCode = undefined;
|
|
41
|
-
mockExploreUrl.mockReset().mockResolvedValue({ ok: true });
|
|
42
|
-
mockRenderExploreSummary.mockReset().mockReturnValue('explore-summary');
|
|
43
|
-
mockGenerateVerifiedFromUrl.mockReset().mockResolvedValue({ status: 'success' });
|
|
44
|
-
mockRenderGenerateVerifiedSummary.mockReset().mockReturnValue('generate-summary');
|
|
45
|
-
mockRecordSession.mockReset().mockResolvedValue({ candidateCount: 1 });
|
|
46
|
-
mockRenderRecordSummary.mockReset().mockReturnValue('record-summary');
|
|
47
|
-
mockCascadeProbe.mockReset().mockResolvedValue({ ok: true });
|
|
48
|
-
mockRenderCascadeResult.mockReset().mockReturnValue('cascade-summary');
|
|
49
|
-
mockGetBrowserFactory.mockClear();
|
|
50
|
-
mockBrowserSession.mockReset().mockImplementation(async (_factory, fn) => {
|
|
51
|
-
const page = {
|
|
52
|
-
goto: vi.fn(),
|
|
53
|
-
wait: vi.fn(),
|
|
54
|
-
};
|
|
55
|
-
return fn(page);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
it('enables OPENCLI_VERBOSE for explore via the real CLI command', async () => {
|
|
59
|
-
const program = createProgram('', '');
|
|
60
|
-
await program.parseAsync(['node', 'opencli', 'explore', 'https://example.com', '-v']);
|
|
61
|
-
expect(process.env.OPENCLI_VERBOSE).toBe('1');
|
|
62
|
-
expect(mockExploreUrl).toHaveBeenCalledWith('https://example.com', expect.objectContaining({ workspace: 'explore:example.com' }));
|
|
63
|
-
});
|
|
64
|
-
it('enables OPENCLI_VERBOSE for generate via the real CLI command', async () => {
|
|
65
|
-
const program = createProgram('', '');
|
|
66
|
-
await program.parseAsync(['node', 'opencli', 'generate', 'https://example.com', '-v']);
|
|
67
|
-
expect(process.env.OPENCLI_VERBOSE).toBe('1');
|
|
68
|
-
expect(mockGenerateVerifiedFromUrl).toHaveBeenCalledWith(expect.objectContaining({ url: 'https://example.com', workspace: 'generate:example.com', noRegister: false }));
|
|
69
|
-
});
|
|
70
|
-
it('passes --no-register through the real CLI command', async () => {
|
|
71
|
-
const program = createProgram('', '');
|
|
72
|
-
await program.parseAsync(['node', 'opencli', 'generate', 'https://example.com', '--no-register']);
|
|
73
|
-
expect(mockGenerateVerifiedFromUrl).toHaveBeenCalledWith(expect.objectContaining({ url: 'https://example.com', workspace: 'generate:example.com', noRegister: true }));
|
|
74
|
-
});
|
|
75
|
-
it('enables OPENCLI_VERBOSE for record via the real CLI command', async () => {
|
|
76
|
-
const program = createProgram('', '');
|
|
77
|
-
await program.parseAsync(['node', 'opencli', 'record', 'https://example.com', '-v']);
|
|
78
|
-
expect(process.env.OPENCLI_VERBOSE).toBe('1');
|
|
79
|
-
expect(mockRecordSession).toHaveBeenCalledWith(expect.objectContaining({ url: 'https://example.com' }));
|
|
80
|
-
});
|
|
81
|
-
it('enables OPENCLI_VERBOSE for cascade via the real CLI command', async () => {
|
|
82
|
-
const program = createProgram('', '');
|
|
83
|
-
await program.parseAsync(['node', 'opencli', 'cascade', 'https://example.com', '-v']);
|
|
84
|
-
expect(process.env.OPENCLI_VERBOSE).toBe('1');
|
|
85
|
-
expect(mockBrowserSession).toHaveBeenCalled();
|
|
86
|
-
expect(mockCascadeProbe).toHaveBeenCalledWith(expect.any(Object), 'https://example.com');
|
|
87
|
-
});
|
|
88
|
-
it('leaves OPENCLI_VERBOSE unset when verbose is omitted', async () => {
|
|
89
|
-
const program = createProgram('', '');
|
|
90
|
-
await program.parseAsync(['node', 'opencli', 'explore', 'https://example.com']);
|
|
91
|
-
expect(process.env.OPENCLI_VERBOSE).toBeUndefined();
|
|
92
|
-
});
|
|
93
|
-
consoleLogSpy.mockClear();
|
|
10
|
+
vi.mock('./browser/index.js', () => {
|
|
11
|
+
mockBrowserConnect.mockImplementation(async () => browserState.page);
|
|
12
|
+
return {
|
|
13
|
+
BrowserBridge: class {
|
|
14
|
+
connect = mockBrowserConnect;
|
|
15
|
+
close = mockBrowserClose;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
94
18
|
});
|
|
19
|
+
import { createProgram, findPackageRoot, resolveBrowserVerifyInvocation } from './cli.js';
|
|
95
20
|
describe('resolveBrowserVerifyInvocation', () => {
|
|
96
21
|
it('prefers the built entry declared in package metadata', () => {
|
|
97
22
|
const projectRoot = path.join('repo-root');
|
|
@@ -156,6 +81,158 @@ describe('resolveBrowserVerifyInvocation', () => {
|
|
|
156
81
|
});
|
|
157
82
|
});
|
|
158
83
|
});
|
|
84
|
+
describe('browser tab targeting commands', () => {
|
|
85
|
+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
86
|
+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
87
|
+
function getBrowserStateFile(cacheDir) {
|
|
88
|
+
return path.join(cacheDir, 'browser-state', 'browser_default.json');
|
|
89
|
+
}
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
process.exitCode = undefined;
|
|
92
|
+
process.env.OPENCLI_CACHE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-browser-tab-state-'));
|
|
93
|
+
consoleLogSpy.mockClear();
|
|
94
|
+
stderrSpy.mockClear();
|
|
95
|
+
mockBrowserConnect.mockClear();
|
|
96
|
+
mockBrowserClose.mockReset().mockResolvedValue(undefined);
|
|
97
|
+
browserState.page = {
|
|
98
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
99
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
100
|
+
setActivePage: vi.fn(),
|
|
101
|
+
getActivePage: vi.fn().mockReturnValue('tab-1'),
|
|
102
|
+
getCurrentUrl: vi.fn().mockResolvedValue('https://one.example'),
|
|
103
|
+
startNetworkCapture: vi.fn().mockResolvedValue(true),
|
|
104
|
+
evaluate: vi.fn().mockResolvedValue({ ok: true }),
|
|
105
|
+
tabs: vi.fn().mockResolvedValue([
|
|
106
|
+
{ index: 0, page: 'tab-1', url: 'https://one.example', title: 'one', active: true },
|
|
107
|
+
{ index: 1, page: 'tab-2', url: 'https://two.example', title: 'two', active: false },
|
|
108
|
+
]),
|
|
109
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
110
|
+
newTab: vi.fn().mockResolvedValue('tab-3'),
|
|
111
|
+
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
112
|
+
frames: vi.fn().mockResolvedValue([
|
|
113
|
+
{ index: 0, frameId: 'frame-1', url: 'https://x.example/embed', name: 'x-embed' },
|
|
114
|
+
]),
|
|
115
|
+
evaluateInFrame: vi.fn().mockResolvedValue('inside frame'),
|
|
116
|
+
readNetworkCapture: vi.fn().mockResolvedValue([]),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
it('binds browser commands to an explicit target tab via --tab', async () => {
|
|
120
|
+
const program = createProgram('', '');
|
|
121
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'eval', '--tab', 'tab-2', 'document.title']);
|
|
122
|
+
expect(browserState.page?.setActivePage).toHaveBeenCalledWith('tab-2');
|
|
123
|
+
expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
|
|
124
|
+
});
|
|
125
|
+
it('rejects an explicit --tab target that is no longer in the current session', async () => {
|
|
126
|
+
browserState.page = {
|
|
127
|
+
setActivePage: vi.fn(),
|
|
128
|
+
getActivePage: vi.fn(),
|
|
129
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
130
|
+
evaluate: vi.fn(),
|
|
131
|
+
};
|
|
132
|
+
const program = createProgram('', '');
|
|
133
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'eval', '--tab', 'tab-stale', 'document.title']);
|
|
134
|
+
expect(process.exitCode).toBeDefined();
|
|
135
|
+
expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
|
|
136
|
+
expect(browserState.page?.evaluate).not.toHaveBeenCalled();
|
|
137
|
+
expect(stderrSpy.mock.calls.flat().join('\n')).toContain('Target tab tab-stale is not part of the current browser session');
|
|
138
|
+
});
|
|
139
|
+
it('lists tabs with target IDs via browser tab list', async () => {
|
|
140
|
+
const program = createProgram('', '');
|
|
141
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'list']);
|
|
142
|
+
expect(browserState.page?.tabs).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-1"');
|
|
144
|
+
expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-2"');
|
|
145
|
+
});
|
|
146
|
+
it('creates a new tab and prints its target ID', async () => {
|
|
147
|
+
const program = createProgram('', '');
|
|
148
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'new', 'https://three.example']);
|
|
149
|
+
expect(browserState.page?.newTab).toHaveBeenCalledWith('https://three.example');
|
|
150
|
+
expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-3"');
|
|
151
|
+
});
|
|
152
|
+
it('prints the resolved target ID when browser open creates or navigates a tab', async () => {
|
|
153
|
+
const program = createProgram('', '');
|
|
154
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'open', 'https://example.com']);
|
|
155
|
+
expect(browserState.page?.goto).toHaveBeenCalledWith('https://example.com');
|
|
156
|
+
expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"url": "https://one.example"');
|
|
157
|
+
expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-1"');
|
|
158
|
+
});
|
|
159
|
+
it('lists cross-origin frames via browser frames', async () => {
|
|
160
|
+
const program = createProgram('', '');
|
|
161
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'frames']);
|
|
162
|
+
expect(browserState.page?.frames).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"frameId": "frame-1"');
|
|
164
|
+
expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"url": "https://x.example/embed"');
|
|
165
|
+
});
|
|
166
|
+
it('routes browser eval --frame through frame-targeted evaluation', async () => {
|
|
167
|
+
const program = createProgram('', '');
|
|
168
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'eval', '--frame', '0', 'document.title']);
|
|
169
|
+
expect(browserState.page?.evaluateInFrame).toHaveBeenCalledWith('document.title', 0);
|
|
170
|
+
expect(browserState.page?.evaluate).not.toHaveBeenCalled();
|
|
171
|
+
expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('inside frame');
|
|
172
|
+
});
|
|
173
|
+
it('does not promote a newly created tab to the persisted default target', async () => {
|
|
174
|
+
const program = createProgram('', '');
|
|
175
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'new', 'https://three.example']);
|
|
176
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
|
|
177
|
+
expect(browserState.page?.newTab).toHaveBeenCalledWith('https://three.example');
|
|
178
|
+
expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
|
|
179
|
+
expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
|
|
180
|
+
});
|
|
181
|
+
it('persists an explicitly selected tab as the default target for later untargeted commands', async () => {
|
|
182
|
+
const program = createProgram('', '');
|
|
183
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'select', 'tab-2']);
|
|
184
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
|
|
185
|
+
expect(browserState.page?.selectTab).toHaveBeenCalledWith('tab-2');
|
|
186
|
+
expect(browserState.page?.setActivePage).toHaveBeenCalledWith('tab-2');
|
|
187
|
+
expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
|
|
188
|
+
expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"selected": "tab-2"');
|
|
189
|
+
});
|
|
190
|
+
it('clears a saved default target when it is no longer present in the current session', async () => {
|
|
191
|
+
const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
|
|
192
|
+
const program = createProgram('', '');
|
|
193
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'select', 'tab-2']);
|
|
194
|
+
expect(fs.existsSync(getBrowserStateFile(cacheDir))).toBe(true);
|
|
195
|
+
browserState.page = {
|
|
196
|
+
setActivePage: vi.fn(),
|
|
197
|
+
getActivePage: vi.fn(),
|
|
198
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
199
|
+
evaluate: vi.fn().mockResolvedValue({ ok: true }),
|
|
200
|
+
readNetworkCapture: vi.fn().mockResolvedValue([]),
|
|
201
|
+
};
|
|
202
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
|
|
203
|
+
expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
|
|
204
|
+
expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
|
|
205
|
+
expect(fs.existsSync(getBrowserStateFile(cacheDir))).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
it('clears the persisted default target when that tab is closed', async () => {
|
|
208
|
+
const program = createProgram('', '');
|
|
209
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'select', 'tab-2']);
|
|
210
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'close', 'tab-2']);
|
|
211
|
+
vi.mocked(browserState.page?.setActivePage).mockClear();
|
|
212
|
+
vi.mocked(browserState.page?.evaluate).mockClear();
|
|
213
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
|
|
214
|
+
expect(browserState.page?.closeTab).toHaveBeenCalledWith('tab-2');
|
|
215
|
+
expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
|
|
216
|
+
expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
|
|
217
|
+
});
|
|
218
|
+
it('closes a tab by target ID', async () => {
|
|
219
|
+
const program = createProgram('', '');
|
|
220
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'close', 'tab-2']);
|
|
221
|
+
expect(browserState.page?.closeTab).toHaveBeenCalledWith('tab-2');
|
|
222
|
+
expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"closed": "tab-2"');
|
|
223
|
+
});
|
|
224
|
+
it('rejects closing a stale tab target ID that is no longer in the current session', async () => {
|
|
225
|
+
browserState.page = {
|
|
226
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
227
|
+
closeTab: vi.fn(),
|
|
228
|
+
};
|
|
229
|
+
const program = createProgram('', '');
|
|
230
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'close', 'tab-stale']);
|
|
231
|
+
expect(process.exitCode).toBeDefined();
|
|
232
|
+
expect(browserState.page?.closeTab).not.toHaveBeenCalled();
|
|
233
|
+
expect(stderrSpy.mock.calls.flat().join('\n')).toContain('Target tab tab-stale is not part of the current browser session');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
159
236
|
describe('findPackageRoot', () => {
|
|
160
237
|
it('walks up from dist/src to the package root', () => {
|
|
161
238
|
const packageRoot = path.join('repo-root');
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { Command } from 'commander';
|
|
13
13
|
import { type CliCommand } from './registry.js';
|
|
14
|
-
export declare function normalizeArgValue(argType: string | undefined, value: unknown, name: string): unknown;
|
|
15
14
|
/**
|
|
16
15
|
* Register a single CliCommand as a Commander subcommand.
|
|
17
16
|
*/
|
|
@@ -15,22 +15,8 @@ import { fullName, getRegistry } from './registry.js';
|
|
|
15
15
|
import { formatRegistryHelpText } from './serialization.js';
|
|
16
16
|
import { render as renderOutput } from './output.js';
|
|
17
17
|
import { executeCommand, prepareCommandArgs } from './execution.js';
|
|
18
|
-
import { CliError, EXIT_CODES,
|
|
18
|
+
import { CliError, EXIT_CODES, toEnvelope, } from './errors.js';
|
|
19
19
|
import { isDiagnosticEnabled } from './diagnostic.js';
|
|
20
|
-
export function normalizeArgValue(argType, value, name) {
|
|
21
|
-
if (argType !== 'bool' && argType !== 'boolean')
|
|
22
|
-
return value;
|
|
23
|
-
if (typeof value === 'boolean')
|
|
24
|
-
return value;
|
|
25
|
-
if (value == null || value === '')
|
|
26
|
-
return false;
|
|
27
|
-
const normalized = String(value).trim().toLowerCase();
|
|
28
|
-
if (normalized === 'true')
|
|
29
|
-
return true;
|
|
30
|
-
if (normalized === 'false')
|
|
31
|
-
return false;
|
|
32
|
-
throw new ArgumentError(`"${name}" must be either "true" or "false".`);
|
|
33
|
-
}
|
|
34
20
|
/**
|
|
35
21
|
* Register a single CliCommand as a Commander subcommand.
|
|
36
22
|
*/
|
|
@@ -83,7 +69,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
83
69
|
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
|
|
84
70
|
const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
|
|
85
71
|
if (v !== undefined)
|
|
86
|
-
rawKwargs[arg.name] =
|
|
72
|
+
rawKwargs[arg.name] = v;
|
|
87
73
|
}
|
|
88
74
|
const kwargs = prepareCommandArgs(cmd, rawKwargs);
|
|
89
75
|
const verbose = optionsRecord.verbose === true;
|
|
@@ -61,7 +61,7 @@ describe('commanderAdapter arg passing', () => {
|
|
|
61
61
|
const siteCmd = program.command('paperreview');
|
|
62
62
|
registerCommandToProgram(siteCmd, cmd);
|
|
63
63
|
await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'maybe']);
|
|
64
|
-
//
|
|
64
|
+
// prepareCommandArgs validates bools before dispatch; executeCommand should not be reached
|
|
65
65
|
expect(mockExecuteCommand).not.toHaveBeenCalled();
|
|
66
66
|
});
|
|
67
67
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI
|
|
3
|
-
* opencli daemon
|
|
2
|
+
* CLI commands for daemon lifecycle:
|
|
3
|
+
* opencli daemon status — show daemon state
|
|
4
|
+
* opencli daemon stop — graceful shutdown
|
|
4
5
|
*/
|
|
6
|
+
export declare function daemonStatus(): Promise<void>;
|
|
5
7
|
export declare function daemonStop(): Promise<void>;
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI
|
|
3
|
-
* opencli daemon
|
|
2
|
+
* CLI commands for daemon lifecycle:
|
|
3
|
+
* opencli daemon status — show daemon state
|
|
4
|
+
* opencli daemon stop — graceful shutdown
|
|
4
5
|
*/
|
|
6
|
+
import { styleText } from 'node:util';
|
|
5
7
|
import { fetchDaemonStatus, requestDaemonShutdown } from '../browser/daemon-client.js';
|
|
8
|
+
import { formatDuration } from '../download/progress.js';
|
|
6
9
|
import { log } from '../logger.js';
|
|
10
|
+
export async function daemonStatus() {
|
|
11
|
+
const status = await fetchDaemonStatus();
|
|
12
|
+
if (!status) {
|
|
13
|
+
console.log(`Daemon: ${styleText('dim', 'not running')}`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const extensionLabel = !status.extensionConnected
|
|
17
|
+
? styleText('yellow', 'disconnected')
|
|
18
|
+
: status.extensionVersion
|
|
19
|
+
? `${styleText('green', 'connected')} ${styleText('dim', `(v${status.extensionVersion})`)}`
|
|
20
|
+
: `${styleText('yellow', 'connected')} ${styleText('dim', '(version unknown)')}`;
|
|
21
|
+
console.log(`Daemon: ${styleText('green', 'running')} (PID ${status.pid})`);
|
|
22
|
+
console.log(`Uptime: ${formatDuration(Math.round(status.uptime * 1000))}`);
|
|
23
|
+
console.log(`Extension: ${extensionLabel}`);
|
|
24
|
+
console.log(`Memory: ${status.memoryMB} MB`);
|
|
25
|
+
console.log(`Port: ${status.port}`);
|
|
26
|
+
}
|
|
7
27
|
export async function daemonStop() {
|
|
8
28
|
const status = await fetchDaemonStatus();
|
|
9
29
|
if (!status) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
const { fetchDaemonStatusMock, requestDaemonShutdownMock, } = vi.hoisted(() => ({
|
|
3
3
|
fetchDaemonStatusMock: vi.fn(),
|
|
4
4
|
requestDaemonShutdownMock: vi.fn(),
|
|
@@ -7,7 +7,70 @@ vi.mock('../browser/daemon-client.js', () => ({
|
|
|
7
7
|
fetchDaemonStatus: fetchDaemonStatusMock,
|
|
8
8
|
requestDaemonShutdown: requestDaemonShutdownMock,
|
|
9
9
|
}));
|
|
10
|
-
import { daemonStop } from './daemon.js';
|
|
10
|
+
import { daemonStatus, daemonStop } from './daemon.js';
|
|
11
|
+
describe('daemonStatus', () => {
|
|
12
|
+
let stdoutSpy;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
15
|
+
fetchDaemonStatusMock.mockReset();
|
|
16
|
+
requestDaemonShutdownMock.mockReset();
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.restoreAllMocks();
|
|
20
|
+
});
|
|
21
|
+
it('reports "not running" when daemon is unreachable', async () => {
|
|
22
|
+
fetchDaemonStatusMock.mockResolvedValue(null);
|
|
23
|
+
await daemonStatus();
|
|
24
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
|
|
25
|
+
});
|
|
26
|
+
it('shows daemon info when running', async () => {
|
|
27
|
+
fetchDaemonStatusMock.mockResolvedValue({
|
|
28
|
+
ok: true,
|
|
29
|
+
pid: 12345,
|
|
30
|
+
uptime: 3661,
|
|
31
|
+
extensionConnected: true,
|
|
32
|
+
extensionVersion: '1.6.8',
|
|
33
|
+
pending: 0,
|
|
34
|
+
memoryMB: 64,
|
|
35
|
+
port: 19825,
|
|
36
|
+
});
|
|
37
|
+
await daemonStatus();
|
|
38
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
|
|
39
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('PID 12345'));
|
|
40
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('1h 1m'));
|
|
41
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('connected'));
|
|
42
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('v1.6.8'));
|
|
43
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('64 MB'));
|
|
44
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('19825'));
|
|
45
|
+
});
|
|
46
|
+
it('shows disconnected when extension is not connected', async () => {
|
|
47
|
+
fetchDaemonStatusMock.mockResolvedValue({
|
|
48
|
+
ok: true,
|
|
49
|
+
pid: 99,
|
|
50
|
+
uptime: 120,
|
|
51
|
+
extensionConnected: false,
|
|
52
|
+
pending: 0,
|
|
53
|
+
memoryMB: 32,
|
|
54
|
+
port: 19825,
|
|
55
|
+
});
|
|
56
|
+
await daemonStatus();
|
|
57
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('disconnected'));
|
|
58
|
+
});
|
|
59
|
+
it('shows version unknown when the connected extension does not report one', async () => {
|
|
60
|
+
fetchDaemonStatusMock.mockResolvedValue({
|
|
61
|
+
ok: true,
|
|
62
|
+
pid: 99,
|
|
63
|
+
uptime: 120,
|
|
64
|
+
extensionConnected: true,
|
|
65
|
+
extensionVersion: undefined,
|
|
66
|
+
pending: 0,
|
|
67
|
+
memoryMB: 32,
|
|
68
|
+
port: 19825,
|
|
69
|
+
});
|
|
70
|
+
await daemonStatus();
|
|
71
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('version unknown'));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
11
74
|
describe('daemonStop', () => {
|
|
12
75
|
let stderrSpy;
|
|
13
76
|
beforeEach(() => {
|
package/dist/src/daemon.js
CHANGED
|
@@ -23,6 +23,7 @@ import { WebSocketServer, WebSocket } from 'ws';
|
|
|
23
23
|
import { DEFAULT_DAEMON_PORT } from './constants.js';
|
|
24
24
|
import { EXIT_CODES } from './errors.js';
|
|
25
25
|
import { log } from './logger.js';
|
|
26
|
+
import { PKG_VERSION } from './version.js';
|
|
26
27
|
const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
27
28
|
// ─── State ───────────────────────────────────────────────────────────
|
|
28
29
|
let extensionWs = null;
|
|
@@ -110,6 +111,7 @@ async function handleRequest(req, res) {
|
|
|
110
111
|
ok: true,
|
|
111
112
|
pid: process.pid,
|
|
112
113
|
uptime,
|
|
114
|
+
daemonVersion: PKG_VERSION,
|
|
113
115
|
extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
|
|
114
116
|
extensionVersion,
|
|
115
117
|
extensionCompatRange,
|
|
@@ -152,6 +154,14 @@ async function handleRequest(req, res) {
|
|
|
152
154
|
const timeoutMs = typeof body.timeout === 'number' && body.timeout > 0
|
|
153
155
|
? body.timeout * 1000
|
|
154
156
|
: 120000;
|
|
157
|
+
if (pending.has(body.id)) {
|
|
158
|
+
jsonResponse(res, 409, {
|
|
159
|
+
id: body.id,
|
|
160
|
+
ok: false,
|
|
161
|
+
error: 'Duplicate command id already pending; retry',
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
155
165
|
const result = await new Promise((resolve, reject) => {
|
|
156
166
|
const timer = setTimeout(() => {
|
|
157
167
|
pending.delete(body.id);
|
package/dist/src/doctor.d.ts
CHANGED
package/dist/src/doctor.js
CHANGED
|
@@ -87,6 +87,7 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
87
87
|
const sessions = opts.sessions && health.state === 'ready'
|
|
88
88
|
? await listSessions()
|
|
89
89
|
: undefined;
|
|
90
|
+
const extensionVersion = health.status?.extensionVersion;
|
|
90
91
|
const issues = [];
|
|
91
92
|
if (daemonFlaky) {
|
|
92
93
|
issues.push('Daemon connectivity is unstable. The live browser test succeeded, but the daemon was no longer running immediately afterward.\n' +
|
|
@@ -100,16 +101,33 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
100
101
|
'This usually means the Browser Bridge service worker is reconnecting slowly or Chrome suspended it.');
|
|
101
102
|
}
|
|
102
103
|
else if (daemonRunning && !extensionConnected) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
const daemonVersion = health.status?.daemonVersion;
|
|
105
|
+
const isStale = opts.cliVersion && (!daemonVersion || daemonVersion !== opts.cliVersion);
|
|
106
|
+
if (isStale) {
|
|
107
|
+
const reason = daemonVersion
|
|
108
|
+
? `daemon v${daemonVersion} ≠ CLI v${opts.cliVersion}`
|
|
109
|
+
: `daemon predates version reporting, CLI is v${opts.cliVersion}`;
|
|
110
|
+
issues.push(`Stale daemon detected: ${reason}.\n` +
|
|
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');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
issues.push('Daemon is running but the Chrome/Chromium extension is not connected.\n' +
|
|
116
|
+
'If the extension is already installed, try: opencli daemon stop && opencli doctor\n' +
|
|
117
|
+
'If the extension is not installed:\n' +
|
|
118
|
+
' 1. Download from https://github.com/jackwener/opencli/releases\n' +
|
|
119
|
+
' 2. Open chrome://extensions/ → Enable Developer Mode\n' +
|
|
120
|
+
' 3. Click "Load unpacked" → select the extension folder');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (extensionConnected && !extensionVersion) {
|
|
124
|
+
issues.push('Extension is connected but did not report a version.\n' +
|
|
125
|
+
' This usually means an outdated Browser Bridge extension.\n' +
|
|
126
|
+
' Reload or reinstall the extension from: https://github.com/jackwener/opencli/releases');
|
|
108
127
|
}
|
|
109
128
|
if (connectivity && !connectivity.ok) {
|
|
110
129
|
issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
|
|
111
130
|
}
|
|
112
|
-
const extensionVersion = health.status?.extensionVersion;
|
|
113
131
|
const extensionCompatRange = health.status?.extensionCompatRange;
|
|
114
132
|
if (extensionVersion && opts.cliVersion && extensionCompatRange) {
|
|
115
133
|
if (!satisfiesRange(opts.cliVersion, extensionCompatRange)) {
|
|
@@ -137,6 +155,7 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
137
155
|
cliVersion: opts.cliVersion,
|
|
138
156
|
daemonRunning,
|
|
139
157
|
daemonFlaky,
|
|
158
|
+
daemonVersion: health.status?.daemonVersion,
|
|
140
159
|
extensionConnected,
|
|
141
160
|
extensionFlaky,
|
|
142
161
|
extensionVersion,
|
|
@@ -154,16 +173,20 @@ export function renderBrowserDoctorReport(report) {
|
|
|
154
173
|
: report.daemonRunning ? styleText('green', '[OK]') : styleText('red', '[MISSING]');
|
|
155
174
|
const daemonLabel = report.daemonFlaky
|
|
156
175
|
? 'unstable (running during live check, then stopped)'
|
|
157
|
-
: report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` : 'not running';
|
|
176
|
+
: report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` + (report.daemonVersion ? ` (v${report.daemonVersion})` : '') : 'not running';
|
|
158
177
|
lines.push(`${daemonIcon} Daemon: ${daemonLabel}`);
|
|
159
178
|
// Extension status
|
|
160
|
-
const extIcon = report.extensionFlaky
|
|
179
|
+
const extIcon = report.extensionFlaky || (report.extensionConnected && !report.extensionVersion)
|
|
161
180
|
? styleText('yellow', '[WARN]')
|
|
162
181
|
: report.extensionConnected ? styleText('green', '[OK]') : styleText('yellow', '[MISSING]');
|
|
163
182
|
const extUpdateHint = report.extensionVersion && report.latestExtensionVersion && isNewerVersion(report.latestExtensionVersion, report.extensionVersion)
|
|
164
183
|
? styleText('yellow', ` → v${report.latestExtensionVersion} available`)
|
|
165
184
|
: '';
|
|
166
|
-
const extVersion = report.
|
|
185
|
+
const extVersion = !report.extensionConnected
|
|
186
|
+
? ''
|
|
187
|
+
: report.extensionVersion
|
|
188
|
+
? styleText('dim', ` (v${report.extensionVersion})`) + extUpdateHint
|
|
189
|
+
: styleText('dim', ' (version unknown)');
|
|
167
190
|
const extLabel = report.extensionFlaky
|
|
168
191
|
? 'unstable (connected during live check, then disconnected)'
|
|
169
192
|
: report.extensionConnected ? 'connected' : 'not connected';
|