@jackwener/opencli 1.7.4 → 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 +71 -49
- package/README.zh-CN.md +73 -60
- package/cli-manifest.json +3261 -1758
- 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/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/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/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/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/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/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/publish.js +149 -28
- package/clis/xiaohongshu/publish.test.js +319 -6
- package/clis/xiaoyuzhou/download.js +8 -4
- package/clis/xiaoyuzhou/download.test.js +23 -13
- 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/utils.js +0 -40
- package/clis/xiaoyuzhou/utils.test.js +15 -75
- 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/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +1 -1
- package/dist/src/browser/cdp.js +1 -1
- package/dist/src/browser/daemon-client.d.ts +6 -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 +7 -2
- package/dist/src/browser/page.d.ts +14 -4
- package/dist/src/browser/page.js +48 -7
- package/dist/src/browser/page.test.js +97 -0
- package/dist/src/cli.js +227 -150
- 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/completion-shared.js +2 -5
- package/dist/src/daemon.js +8 -0
- 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/plugin.d.ts +1 -8
- package/dist/src/plugin.js +1 -27
- package/dist/src/plugin.test.js +1 -59
- 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 +14 -5
- 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.d.ts +0 -1
- 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.d.ts +0 -1
- 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 → 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
|
});
|
package/dist/src/daemon.js
CHANGED
|
@@ -154,6 +154,14 @@ async function handleRequest(req, res) {
|
|
|
154
154
|
const timeoutMs = typeof body.timeout === 'number' && body.timeout > 0
|
|
155
155
|
? body.timeout * 1000
|
|
156
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
|
+
}
|
|
157
165
|
const result = await new Promise((resolve, reject) => {
|
|
158
166
|
const timer = setTimeout(() => {
|
|
159
167
|
pending.delete(body.id);
|
|
@@ -129,6 +129,7 @@ export async function downloadArticle(data, options) {
|
|
|
129
129
|
publish_time: '-',
|
|
130
130
|
status: 'failed — no title',
|
|
131
131
|
size: '-',
|
|
132
|
+
saved: '-',
|
|
132
133
|
}];
|
|
133
134
|
}
|
|
134
135
|
if (!data.contentHtml) {
|
|
@@ -138,6 +139,7 @@ export async function downloadArticle(data, options) {
|
|
|
138
139
|
publish_time: data.publishTime || '-',
|
|
139
140
|
status: 'failed — no content',
|
|
140
141
|
size: '-',
|
|
142
|
+
saved: '-',
|
|
141
143
|
}];
|
|
142
144
|
}
|
|
143
145
|
// Convert HTML to Markdown
|
|
@@ -174,5 +176,6 @@ export async function downloadArticle(data, options) {
|
|
|
174
176
|
publish_time: data.publishTime || '-',
|
|
175
177
|
status: 'success',
|
|
176
178
|
size: formatBytes(size),
|
|
179
|
+
saved: filePath,
|
|
177
180
|
}];
|
|
178
181
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { downloadArticle } from './article-download.js';
|
|
6
|
+
const tempDirs = [];
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
for (const dir of tempDirs) {
|
|
9
|
+
try {
|
|
10
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// Ignore cleanup errors in tests.
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
tempDirs.length = 0;
|
|
17
|
+
});
|
|
18
|
+
describe('downloadArticle', () => {
|
|
19
|
+
it('returns the saved markdown file path on success', async () => {
|
|
20
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-article-'));
|
|
21
|
+
tempDirs.push(tempDir);
|
|
22
|
+
const result = await downloadArticle({
|
|
23
|
+
title: 'Test Article',
|
|
24
|
+
author: 'Author',
|
|
25
|
+
publishTime: '2026-04-20 12:00:00',
|
|
26
|
+
sourceUrl: 'https://example.com/article',
|
|
27
|
+
contentHtml: '<p>Hello world</p>',
|
|
28
|
+
}, {
|
|
29
|
+
output: tempDir,
|
|
30
|
+
downloadImages: false,
|
|
31
|
+
});
|
|
32
|
+
expect(result).toHaveLength(1);
|
|
33
|
+
expect(result[0].status).toBe('success');
|
|
34
|
+
expect(result[0].saved).toMatch(new RegExp(`^${tempDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
|
|
35
|
+
expect(path.extname(result[0].saved)).toBe('.md');
|
|
36
|
+
expect(fs.existsSync(result[0].saved)).toBe(true);
|
|
37
|
+
expect(fs.readFileSync(result[0].saved, 'utf8')).toContain('Hello world');
|
|
38
|
+
});
|
|
39
|
+
});
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -60,13 +60,6 @@ declare function resolveStoredPluginSource(lockEntry: LockEntry | undefined, plu
|
|
|
60
60
|
*/
|
|
61
61
|
type MoveDirFsOps = Pick<typeof fs, 'renameSync' | 'cpSync' | 'rmSync'>;
|
|
62
62
|
declare function moveDir(src: string, dest: string, fsOps?: MoveDirFsOps): void;
|
|
63
|
-
type PromoteDirFsOps = MoveDirFsOps & Pick<typeof fs, 'existsSync' | 'mkdirSync'>;
|
|
64
|
-
/**
|
|
65
|
-
* Promote a prepared staging directory into its final location.
|
|
66
|
-
* The final path is only exposed after the directory has been fully prepared.
|
|
67
|
-
*/
|
|
68
|
-
declare function promoteDir(stagingDir: string, dest: string, fsOps?: PromoteDirFsOps): void;
|
|
69
|
-
declare function replaceDir(stagingDir: string, dest: string, fsOps?: PromoteDirFsOps): void;
|
|
70
63
|
export interface ValidationResult {
|
|
71
64
|
valid: boolean;
|
|
72
65
|
errors: string[];
|
|
@@ -149,4 +142,4 @@ declare function parseSource(source: string): ParsedSource | null;
|
|
|
149
142
|
*/
|
|
150
143
|
export declare function resolveEsbuildBin(): string | null;
|
|
151
144
|
declare function resolveHostOpencliRoot(startFile?: string): string;
|
|
152
|
-
export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir,
|
|
145
|
+
export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
|
package/dist/src/plugin.js
CHANGED
|
@@ -159,32 +159,6 @@ function createSiblingTempPath(dest, kind) {
|
|
|
159
159
|
const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
160
160
|
return path.join(path.dirname(dest), `.${path.basename(dest)}.${kind}-${suffix}`);
|
|
161
161
|
}
|
|
162
|
-
/**
|
|
163
|
-
* Promote a prepared staging directory into its final location.
|
|
164
|
-
* The final path is only exposed after the directory has been fully prepared.
|
|
165
|
-
*/
|
|
166
|
-
function promoteDir(stagingDir, dest, fsOps = fs) {
|
|
167
|
-
if (fsOps.existsSync(dest)) {
|
|
168
|
-
throw new PluginError(`Destination already exists: ${dest}`);
|
|
169
|
-
}
|
|
170
|
-
fsOps.mkdirSync(path.dirname(dest), { recursive: true });
|
|
171
|
-
const tempDest = createSiblingTempPath(dest, 'tmp');
|
|
172
|
-
try {
|
|
173
|
-
moveDir(stagingDir, tempDest, fsOps);
|
|
174
|
-
fsOps.renameSync(tempDest, dest);
|
|
175
|
-
}
|
|
176
|
-
catch (err) {
|
|
177
|
-
try {
|
|
178
|
-
fsOps.rmSync(tempDest, { recursive: true, force: true });
|
|
179
|
-
}
|
|
180
|
-
catch { }
|
|
181
|
-
throw err;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
function replaceDir(stagingDir, dest, fsOps = fs) {
|
|
185
|
-
const replacement = beginReplaceDir(stagingDir, dest, fsOps);
|
|
186
|
-
replacement.finalize();
|
|
187
|
-
}
|
|
188
162
|
function cloneRepoToTemp(cloneUrl) {
|
|
189
163
|
const tmpCloneDir = path.join(os.tmpdir(), `opencli-clone-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
190
164
|
try {
|
|
@@ -1268,4 +1242,4 @@ function transpilePluginTs(pluginDir) {
|
|
|
1268
1242
|
log.warn(`TS transpilation setup failed: ${getErrorMessage(err)}`);
|
|
1269
1243
|
}
|
|
1270
1244
|
}
|
|
1271
|
-
export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir,
|
|
1245
|
+
export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
|
package/dist/src/plugin.test.js
CHANGED
|
@@ -12,7 +12,7 @@ const { mockExecFileSync, mockExecSync } = vi.hoisted(() => ({
|
|
|
12
12
|
mockExecFileSync: vi.fn(),
|
|
13
13
|
mockExecSync: vi.fn(),
|
|
14
14
|
}));
|
|
15
|
-
const { _getCommitHash, _installDependencies, _postInstallMonorepoLifecycle,
|
|
15
|
+
const { _getCommitHash, _installDependencies, _postInstallMonorepoLifecycle, installPlugin, listPlugins, _readLockFile, _readLockFileWithWriter, _resolveEsbuildBin, _resolveHostOpencliRoot, uninstallPlugin, updatePlugin, _parseSource, _updateAllPlugins, _validatePluginStructure, _writeLockFile, _writeLockFileWithFs, _isSymlinkSync, _getMonoreposDir, getLockFilePath, _installLocalPlugin, _isLocalPluginSource, _moveDir, _resolvePluginSource, _resolveStoredPluginSource, _toStoredPluginSource, _toLocalPluginSource, } = pluginModule;
|
|
16
16
|
describe('parseSource', () => {
|
|
17
17
|
it('parses github:user/repo format', () => {
|
|
18
18
|
const result = _parseSource('github:ByteYue/opencli-plugin-github-trending');
|
|
@@ -924,64 +924,6 @@ describe('moveDir', () => {
|
|
|
924
924
|
expect(rmSync).toHaveBeenCalledWith(dest, { recursive: true, force: true });
|
|
925
925
|
});
|
|
926
926
|
});
|
|
927
|
-
describe('promoteDir', () => {
|
|
928
|
-
it('cleans up temporary publish dir when final rename fails', () => {
|
|
929
|
-
const staging = path.join(os.tmpdir(), 'opencli-promote-stage');
|
|
930
|
-
const dest = path.join(os.tmpdir(), 'opencli-promote-dest');
|
|
931
|
-
const publishErr = new Error('publish failed');
|
|
932
|
-
const existsSync = vi.fn(() => false);
|
|
933
|
-
const mkdirSync = vi.fn(() => undefined);
|
|
934
|
-
const cpSync = vi.fn(() => undefined);
|
|
935
|
-
const rmSync = vi.fn(() => undefined);
|
|
936
|
-
const renameSync = vi.fn((src, _target) => {
|
|
937
|
-
if (String(src) === staging)
|
|
938
|
-
return;
|
|
939
|
-
throw publishErr;
|
|
940
|
-
});
|
|
941
|
-
expect(() => _promoteDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
|
|
942
|
-
const tempDest = renameSync.mock.calls[0][1];
|
|
943
|
-
expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
|
|
944
|
-
expect(renameSync).toHaveBeenNthCalledWith(2, tempDest, dest);
|
|
945
|
-
expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
|
|
946
|
-
});
|
|
947
|
-
});
|
|
948
|
-
describe('replaceDir', () => {
|
|
949
|
-
it('rolls back the original destination when swap fails', () => {
|
|
950
|
-
const staging = path.join(os.tmpdir(), 'opencli-replace-stage');
|
|
951
|
-
const dest = path.join(os.tmpdir(), 'opencli-replace-dest');
|
|
952
|
-
const publishErr = new Error('swap failed');
|
|
953
|
-
const existingPaths = new Set([dest]);
|
|
954
|
-
const existsSync = vi.fn((p) => existingPaths.has(String(p)));
|
|
955
|
-
const mkdirSync = vi.fn(() => undefined);
|
|
956
|
-
const cpSync = vi.fn(() => undefined);
|
|
957
|
-
const rmSync = vi.fn(() => undefined);
|
|
958
|
-
const renameSync = vi.fn((src, target) => {
|
|
959
|
-
if (String(src) === staging) {
|
|
960
|
-
existingPaths.add(String(target));
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
if (String(src) === dest) {
|
|
964
|
-
existingPaths.delete(dest);
|
|
965
|
-
existingPaths.add(String(target));
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
if (String(target) === dest)
|
|
969
|
-
throw publishErr;
|
|
970
|
-
if (existingPaths.has(String(src))) {
|
|
971
|
-
existingPaths.delete(String(src));
|
|
972
|
-
existingPaths.add(String(target));
|
|
973
|
-
}
|
|
974
|
-
});
|
|
975
|
-
expect(() => _replaceDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
|
|
976
|
-
const tempDest = renameSync.mock.calls[0][1];
|
|
977
|
-
const backupDest = renameSync.mock.calls[1][1];
|
|
978
|
-
expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
|
|
979
|
-
expect(renameSync).toHaveBeenNthCalledWith(2, dest, backupDest);
|
|
980
|
-
expect(renameSync).toHaveBeenNthCalledWith(3, tempDest, dest);
|
|
981
|
-
expect(renameSync).toHaveBeenNthCalledWith(4, backupDest, dest);
|
|
982
|
-
expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
|
|
983
|
-
});
|
|
984
|
-
});
|
|
985
927
|
describe('installPlugin transactional staging', () => {
|
|
986
928
|
const standaloneSource = 'github:user/opencli-plugin-__test-transactional-standalone__';
|
|
987
929
|
const standaloneName = '__test-transactional-standalone__';
|
package/dist/src/registry.d.ts
CHANGED
package/dist/src/registry.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export var Strategy;
|
|
5
5
|
(function (Strategy) {
|
|
6
6
|
Strategy["PUBLIC"] = "public";
|
|
7
|
+
Strategy["LOCAL"] = "local";
|
|
7
8
|
Strategy["COOKIE"] = "cookie";
|
|
8
9
|
Strategy["HEADER"] = "header";
|
|
9
10
|
Strategy["INTERCEPT"] = "intercept";
|
|
@@ -58,13 +59,13 @@ export function strategyLabel(cmd) {
|
|
|
58
59
|
*/
|
|
59
60
|
function normalizeCommand(cmd) {
|
|
60
61
|
const strategy = cmd.strategy ?? (cmd.browser === false ? Strategy.PUBLIC : Strategy.COOKIE);
|
|
61
|
-
const browser = cmd.browser ?? (strategy !== Strategy.PUBLIC);
|
|
62
|
+
const browser = cmd.browser ?? (strategy !== Strategy.PUBLIC && strategy !== Strategy.LOCAL);
|
|
62
63
|
let navigateBefore = cmd.navigateBefore;
|
|
63
64
|
if (navigateBefore === undefined) {
|
|
64
65
|
if ((strategy === Strategy.COOKIE || strategy === Strategy.HEADER) && cmd.domain) {
|
|
65
66
|
navigateBefore = `https://${cmd.domain}`;
|
|
66
67
|
}
|
|
67
|
-
else if (strategy !== Strategy.PUBLIC) {
|
|
68
|
+
else if (strategy !== Strategy.PUBLIC && strategy !== Strategy.LOCAL) {
|
|
68
69
|
// Non-PUBLIC without domain: needs authenticated browser context
|
|
69
70
|
// but no specific pre-navigation URL. `true` signals this to
|
|
70
71
|
// shouldUseBrowserSession without triggering resolvePreNav.
|
|
@@ -43,6 +43,17 @@ describe('cli() registration', () => {
|
|
|
43
43
|
});
|
|
44
44
|
expect(cmd.strategy).toBe(Strategy.PUBLIC);
|
|
45
45
|
});
|
|
46
|
+
it('preserves LOCAL strategy on registration', () => {
|
|
47
|
+
const cmd = cli({
|
|
48
|
+
site: 'test-registry',
|
|
49
|
+
name: 'local-strategy',
|
|
50
|
+
description: 'reads local credentials',
|
|
51
|
+
strategy: Strategy.LOCAL,
|
|
52
|
+
browser: false,
|
|
53
|
+
});
|
|
54
|
+
expect(cmd.strategy).toBe(Strategy.LOCAL);
|
|
55
|
+
expect(cmd.browser).toBe(false);
|
|
56
|
+
});
|
|
46
57
|
it('overwrites existing command on re-registration', () => {
|
|
47
58
|
cli({ site: 'test-registry', name: 'overwrite', description: 'v1' });
|
|
48
59
|
cli({ site: 'test-registry', name: 'overwrite', description: 'v2' });
|
|
@@ -148,6 +159,17 @@ describe('normalizeCommand (via registerCommand)', () => {
|
|
|
148
159
|
expect(cmd.browser).toBe(false);
|
|
149
160
|
expect(cmd.navigateBefore).toBeUndefined();
|
|
150
161
|
});
|
|
162
|
+
it('LOCAL → browser false, navigateBefore undefined', () => {
|
|
163
|
+
registerCommand({
|
|
164
|
+
site: 'test-norm', name: 'local', description: '', args: [],
|
|
165
|
+
strategy: Strategy.LOCAL,
|
|
166
|
+
});
|
|
167
|
+
const cmd = getRegistry().get('test-norm/local');
|
|
168
|
+
expect(cmd.strategy).toBe(Strategy.LOCAL);
|
|
169
|
+
expect(strategyLabel(cmd)).toBe('local');
|
|
170
|
+
expect(cmd.browser).toBe(false);
|
|
171
|
+
expect(cmd.navigateBefore).toBeUndefined();
|
|
172
|
+
});
|
|
151
173
|
it('explicit navigateBefore: false overrides COOKIE + domain', () => {
|
|
152
174
|
registerCommand({
|
|
153
175
|
site: 'test-norm', name: 'cookie-override', description: '', args: [],
|