@jackwener/opencli 1.5.8 → 1.6.0
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/CHANGELOG.md +42 -0
- package/README.md +35 -1
- package/README.zh-CN.md +17 -1
- package/SKILL.md +31 -851
- package/autoresearch/baseline-browse.txt +1 -0
- package/autoresearch/baseline-skill.txt +1 -0
- package/autoresearch/browse-tasks.json +688 -0
- package/autoresearch/eval-browse.ts +185 -0
- package/autoresearch/eval-skill.ts +248 -0
- package/autoresearch/run-browse.sh +9 -0
- package/autoresearch/run-skill.sh +9 -0
- package/dist/browser/base-page.d.ts +48 -0
- package/dist/browser/base-page.js +160 -0
- package/dist/browser/cdp.js +4 -106
- package/dist/browser/daemon-client.d.ts +20 -7
- package/dist/browser/daemon-client.js +39 -39
- package/dist/browser/daemon-client.test.js +77 -0
- package/dist/browser/discover.d.ts +1 -4
- package/dist/browser/discover.js +9 -23
- package/dist/browser/errors.d.ts +4 -0
- package/dist/browser/errors.js +20 -0
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/page.d.ts +10 -35
- package/dist/browser/page.js +55 -187
- package/dist/browser/tabs.js +5 -5
- package/dist/browser.test.js +15 -15
- package/dist/cli-manifest.json +294 -22
- package/dist/cli.js +392 -0
- package/dist/clis/amazon/bestsellers.d.ts +21 -0
- package/dist/clis/amazon/bestsellers.js +130 -0
- package/dist/clis/amazon/bestsellers.test.js +20 -0
- package/dist/clis/amazon/discussion.d.ts +20 -0
- package/dist/clis/amazon/discussion.js +91 -0
- package/dist/clis/amazon/discussion.test.d.ts +1 -0
- package/dist/clis/amazon/discussion.test.js +36 -0
- package/dist/clis/amazon/offer.d.ts +23 -0
- package/dist/clis/amazon/offer.js +140 -0
- package/dist/clis/amazon/offer.test.d.ts +1 -0
- package/dist/clis/amazon/offer.test.js +29 -0
- package/dist/clis/amazon/product.d.ts +18 -0
- package/dist/clis/amazon/product.js +92 -0
- package/dist/clis/amazon/product.test.d.ts +1 -0
- package/dist/clis/amazon/product.test.js +24 -0
- package/dist/clis/amazon/search.d.ts +18 -0
- package/dist/clis/amazon/search.js +87 -0
- package/dist/clis/amazon/search.test.d.ts +1 -0
- package/dist/clis/amazon/search.test.js +22 -0
- package/dist/clis/amazon/shared.d.ts +64 -0
- package/dist/clis/amazon/shared.js +255 -0
- package/dist/clis/amazon/shared.test.d.ts +1 -0
- package/dist/clis/amazon/shared.test.js +33 -0
- package/dist/clis/gemini/ask.d.ts +1 -0
- package/dist/clis/gemini/ask.js +40 -0
- package/dist/clis/gemini/image.d.ts +1 -0
- package/dist/clis/gemini/image.js +105 -0
- package/dist/clis/gemini/new.d.ts +1 -0
- package/dist/clis/gemini/new.js +20 -0
- package/dist/clis/gemini/utils.d.ts +34 -0
- package/dist/clis/gemini/utils.js +463 -0
- package/dist/clis/gemini/utils.test.d.ts +1 -0
- package/dist/clis/gemini/utils.test.js +31 -0
- package/dist/clis/notebooklm/compat.test.d.ts +1 -1
- package/dist/clis/notebooklm/compat.test.js +3 -3
- package/dist/clis/notebooklm/current.js +2 -3
- package/dist/clis/notebooklm/get.js +2 -3
- package/dist/clis/notebooklm/history.js +2 -3
- package/dist/clis/notebooklm/note-list.js +2 -3
- package/dist/clis/notebooklm/notes-get.js +2 -3
- package/dist/clis/notebooklm/open.d.ts +1 -0
- package/dist/clis/notebooklm/open.js +41 -0
- package/dist/clis/notebooklm/open.test.d.ts +1 -0
- package/dist/clis/notebooklm/open.test.js +63 -0
- package/dist/clis/notebooklm/source-fulltext.js +2 -3
- package/dist/clis/notebooklm/source-get.js +2 -3
- package/dist/clis/notebooklm/source-guide.js +2 -3
- package/dist/clis/notebooklm/source-list.js +2 -3
- package/dist/clis/notebooklm/status.js +1 -2
- package/dist/clis/notebooklm/summary.js +2 -3
- package/dist/clis/notebooklm/utils.d.ts +2 -1
- package/dist/clis/notebooklm/utils.js +20 -21
- package/dist/clis/twitter/article.js +28 -1
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
- package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
- package/dist/clis/xiaohongshu/note.js +11 -0
- package/dist/clis/xiaohongshu/note.test.js +49 -0
- package/dist/commanderAdapter.js +7 -4
- package/dist/commanderAdapter.test.js +76 -0
- package/dist/commands/daemon.js +8 -47
- package/dist/commands/daemon.test.js +45 -70
- package/dist/discovery.js +27 -0
- package/dist/doctor.d.ts +1 -2
- package/dist/doctor.js +7 -8
- package/dist/explore.js +1 -1
- package/dist/output.js +28 -0
- package/dist/output.test.js +15 -0
- package/dist/pipeline/executor.js +2 -7
- package/dist/pipeline/steps/browser.js +1 -1
- package/dist/pipeline/template.js +25 -3
- package/dist/record.d.ts +50 -0
- package/dist/record.js +298 -57
- package/dist/record.test.d.ts +1 -0
- package/dist/record.test.js +293 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -0
- package/dist/registry.test.js +10 -0
- package/dist/runtime.js +3 -3
- package/dist/snapshotFormatter.d.ts +1 -1
- package/dist/snapshotFormatter.js +4 -4
- package/dist/snapshotFormatter.test.d.ts +1 -1
- package/dist/snapshotFormatter.test.js +2 -2
- package/dist/types.d.ts +11 -1
- package/dist/types.js +1 -1
- package/docs/.vitepress/config.mts +2 -0
- package/docs/adapters/browser/amazon.md +53 -0
- package/docs/adapters/browser/gemini.md +72 -0
- package/docs/adapters/browser/notebooklm.md +5 -5
- package/docs/adapters/index.md +3 -1
- package/docs/guide/getting-started.md +21 -0
- package/docs/superpowers/specs/2026-04-02-browse-skill-testing-design.md +144 -0
- package/docs/zh/guide/getting-started.md +21 -0
- package/extension/package-lock.json +2 -2
- package/extension/src/background.test.ts +7 -163
- package/extension/src/background.ts +58 -161
- package/extension/src/cdp.ts +77 -124
- package/extension/src/protocol.ts +5 -5
- package/package.json +1 -1
- package/skills/opencli-explorer/SKILL.md +853 -0
- package/skills/opencli-oneshot/SKILL.md +222 -0
- package/skills/opencli-operate/SKILL.md +213 -0
- package/skills/opencli-usage/SKILL.md +152 -0
- package/skills/opencli-usage/browser.md +429 -0
- package/skills/opencli-usage/desktop.md +118 -0
- package/skills/opencli-usage/plugins.md +82 -0
- package/skills/opencli-usage/public-api.md +149 -0
- package/src/browser/base-page.ts +197 -0
- package/src/browser/cdp.ts +7 -131
- package/src/browser/daemon-client.test.ts +103 -0
- package/src/browser/daemon-client.ts +55 -43
- package/src/browser/discover.ts +9 -21
- package/src/browser/errors.ts +22 -0
- package/src/browser/index.ts +1 -1
- package/src/browser/page.ts +57 -209
- package/src/browser/tabs.ts +5 -5
- package/src/browser.test.ts +15 -15
- package/src/cli.ts +392 -0
- package/src/clis/amazon/bestsellers.test.ts +22 -0
- package/src/clis/amazon/bestsellers.ts +180 -0
- package/src/clis/amazon/discussion.test.ts +38 -0
- package/src/clis/amazon/discussion.ts +131 -0
- package/src/clis/amazon/offer.test.ts +35 -0
- package/src/clis/amazon/offer.ts +185 -0
- package/src/clis/amazon/product.test.ts +26 -0
- package/src/clis/amazon/product.ts +131 -0
- package/src/clis/amazon/search.test.ts +24 -0
- package/src/clis/amazon/search.ts +128 -0
- package/src/clis/amazon/shared.test.ts +37 -0
- package/src/clis/amazon/shared.ts +316 -0
- package/src/clis/gemini/ask.ts +46 -0
- package/src/clis/gemini/image.ts +115 -0
- package/src/clis/gemini/new.ts +22 -0
- package/src/clis/gemini/utils.test.ts +36 -0
- package/src/clis/gemini/utils.ts +523 -0
- package/src/clis/notebooklm/compat.test.ts +3 -3
- package/src/clis/notebooklm/current.ts +2 -3
- package/src/clis/notebooklm/get.ts +1 -3
- package/src/clis/notebooklm/history.ts +1 -3
- package/src/clis/notebooklm/note-list.ts +1 -3
- package/src/clis/notebooklm/notes-get.ts +1 -3
- package/src/clis/notebooklm/open.test.ts +78 -0
- package/src/clis/notebooklm/open.ts +61 -0
- package/src/clis/notebooklm/source-fulltext.ts +1 -3
- package/src/clis/notebooklm/source-get.ts +1 -3
- package/src/clis/notebooklm/source-guide.ts +1 -3
- package/src/clis/notebooklm/source-list.ts +1 -3
- package/src/clis/notebooklm/status.ts +1 -2
- package/src/clis/notebooklm/summary.ts +1 -3
- package/src/clis/notebooklm/utils.ts +29 -20
- package/src/clis/twitter/article.ts +31 -1
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
- package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
- package/src/clis/xiaohongshu/note.test.ts +51 -0
- package/src/clis/xiaohongshu/note.ts +18 -0
- package/src/commanderAdapter.test.ts +109 -0
- package/src/commanderAdapter.ts +8 -4
- package/src/commands/daemon.test.ts +50 -84
- package/src/commands/daemon.ts +8 -56
- package/src/discovery.ts +22 -0
- package/src/doctor.ts +8 -9
- package/src/explore.ts +1 -1
- package/src/output.test.ts +17 -0
- package/src/output.ts +27 -0
- package/src/pipeline/executor.ts +2 -7
- package/src/pipeline/steps/browser.ts +1 -1
- package/src/pipeline/template.ts +27 -4
- package/src/record.test.ts +362 -0
- package/src/record.ts +341 -62
- package/src/registry.test.ts +12 -0
- package/src/registry.ts +3 -0
- package/src/runtime.ts +3 -3
- package/src/snapshotFormatter.test.ts +2 -2
- package/src/snapshotFormatter.ts +4 -4
- package/src/types.ts +11 -1
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
- package/.agents/workflows/cross-project-adapter-migration.md +0 -54
- package/dist/clis/notebooklm/bind-current.js +0 -29
- package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
- package/dist/clis/notebooklm/bind-current.test.js +0 -35
- package/dist/clis/notebooklm/binding.test.js +0 -44
- package/extension/dist/background.js +0 -819
- package/src/clis/notebooklm/bind-current.test.ts +0 -43
- package/src/clis/notebooklm/bind-current.ts +0 -36
- package/src/clis/notebooklm/binding.test.ts +0 -53
- /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
- /package/dist/browser/{mcp.js → bridge.js} +0 -0
- /package/dist/{clis/notebooklm/bind-current.d.ts → browser/daemon-client.test.d.ts} +0 -0
- /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/bestsellers.test.d.ts} +0 -0
- /package/src/browser/{mcp.ts → bridge.ts} +0 -0
|
@@ -137,6 +137,57 @@ describe('xiaohongshu note', () => {
|
|
|
137
137
|
await expect(command!.func!(page, { 'note-id': 'abc123' })).rejects.toThrow('returned no data');
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
it('throws a token hint when the note page renders as an empty shell', async () => {
|
|
141
|
+
const page = createPageMock({
|
|
142
|
+
loginWall: false,
|
|
143
|
+
notFound: false,
|
|
144
|
+
title: '',
|
|
145
|
+
desc: '',
|
|
146
|
+
author: '',
|
|
147
|
+
likes: '',
|
|
148
|
+
collects: '',
|
|
149
|
+
comments: '',
|
|
150
|
+
tags: [],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await command!.func!(page, { 'note-id': '69ca3927000000001a020fd5' });
|
|
155
|
+
throw new Error('expected xiaohongshu note to fail on an empty shell page');
|
|
156
|
+
} catch (error) {
|
|
157
|
+
expect(error).toMatchObject({
|
|
158
|
+
code: 'EMPTY_RESULT',
|
|
159
|
+
hint: expect.stringMatching(/xsec_token|full url|search_result/i),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('keeps the empty-shell hint generic when the user already passed a full URL', async () => {
|
|
165
|
+
const page = createPageMock({
|
|
166
|
+
loginWall: false,
|
|
167
|
+
notFound: false,
|
|
168
|
+
title: '',
|
|
169
|
+
desc: '',
|
|
170
|
+
author: '',
|
|
171
|
+
likes: '',
|
|
172
|
+
collects: '',
|
|
173
|
+
comments: '',
|
|
174
|
+
tags: [],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
await command!.func!(page, {
|
|
179
|
+
'note-id': 'https://www.xiaohongshu.com/search_result/69ca3927000000001a020fd5?xsec_token=abc',
|
|
180
|
+
});
|
|
181
|
+
throw new Error('expected xiaohongshu note to fail on an empty shell page');
|
|
182
|
+
} catch (error) {
|
|
183
|
+
expect(error).toMatchObject({
|
|
184
|
+
code: 'EMPTY_RESULT',
|
|
185
|
+
hint: expect.stringContaining('loaded without visible content'),
|
|
186
|
+
});
|
|
187
|
+
expect((error as { hint?: string }).hint).not.toContain('bare note ID');
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
140
191
|
it('normalizes placeholder text to 0 for zero-count metrics', async () => {
|
|
141
192
|
const page = createPageMock({
|
|
142
193
|
loginWall: false, notFound: false,
|
|
@@ -21,6 +21,7 @@ cli({
|
|
|
21
21
|
columns: ['field', 'value'],
|
|
22
22
|
func: async (page, kwargs) => {
|
|
23
23
|
const raw = String(kwargs['note-id']);
|
|
24
|
+
const isBareNoteId = !/^https?:\/\//.test(raw.trim());
|
|
24
25
|
const noteId = parseNoteId(raw);
|
|
25
26
|
const url = buildNoteUrl(raw);
|
|
26
27
|
|
|
@@ -68,6 +69,23 @@ cli({
|
|
|
68
69
|
// XHS renders placeholder text like "赞"/"收藏"/"评论" when count is 0;
|
|
69
70
|
// normalize to '0' unless the value looks numeric.
|
|
70
71
|
const numOrZero = (v: string) => /^\d+/.test(v) ? v : '0';
|
|
72
|
+
|
|
73
|
+
// XHS sometimes renders an empty shell page for bare /explore/<id> visits
|
|
74
|
+
// when the request lacks a valid xsec_token. Title + author are always
|
|
75
|
+
// present on a real note, so their absence is the simplest reliable signal.
|
|
76
|
+
const emptyShell = !d.title && !d.author;
|
|
77
|
+
if (emptyShell) {
|
|
78
|
+
if (isBareNoteId) {
|
|
79
|
+
throw new EmptyResultError(
|
|
80
|
+
'xiaohongshu/note',
|
|
81
|
+
'Pass the full search_result URL with xsec_token, for example from `opencli xiaohongshu search`, instead of a bare note ID.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
throw new EmptyResultError(
|
|
85
|
+
'xiaohongshu/note',
|
|
86
|
+
'The note page loaded without visible content. Retry with a fresh URL or run with --verbose; if it persists, the page structure may have changed.',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
71
89
|
const rows = [
|
|
72
90
|
{ field: 'title', value: d.title || '' },
|
|
73
91
|
{ field: 'author', value: d.author || '' },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import type { CliCommand } from './registry.js';
|
|
4
|
+
import { EmptyResultError, SelectorError } from './errors.js';
|
|
4
5
|
|
|
5
6
|
const { mockExecuteCommand, mockRenderOutput } = vi.hoisted(() => ({
|
|
6
7
|
mockExecuteCommand: vi.fn(),
|
|
@@ -153,3 +154,111 @@ describe('commanderAdapter command aliases', () => {
|
|
|
153
154
|
expect(mockExecuteCommand).toHaveBeenCalledWith(cmd, {}, false);
|
|
154
155
|
});
|
|
155
156
|
});
|
|
157
|
+
|
|
158
|
+
describe('commanderAdapter default formats', () => {
|
|
159
|
+
const cmd: CliCommand = {
|
|
160
|
+
site: 'gemini',
|
|
161
|
+
name: 'ask',
|
|
162
|
+
description: 'Ask Gemini',
|
|
163
|
+
browser: false,
|
|
164
|
+
args: [],
|
|
165
|
+
columns: ['response'],
|
|
166
|
+
defaultFormat: 'plain',
|
|
167
|
+
func: vi.fn(),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
mockExecuteCommand.mockReset();
|
|
172
|
+
mockExecuteCommand.mockResolvedValue([{ response: 'hello' }]);
|
|
173
|
+
mockRenderOutput.mockReset();
|
|
174
|
+
delete process.env.OPENCLI_VERBOSE;
|
|
175
|
+
process.exitCode = undefined;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('uses the command defaultFormat when the user keeps the default table format', async () => {
|
|
179
|
+
const program = new Command();
|
|
180
|
+
const siteCmd = program.command('gemini');
|
|
181
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
182
|
+
|
|
183
|
+
await program.parseAsync(['node', 'opencli', 'gemini', 'ask']);
|
|
184
|
+
|
|
185
|
+
expect(mockRenderOutput).toHaveBeenCalledWith(
|
|
186
|
+
[{ response: 'hello' }],
|
|
187
|
+
expect.objectContaining({ fmt: 'plain' }),
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('respects an explicit user format over the command defaultFormat', async () => {
|
|
192
|
+
const program = new Command();
|
|
193
|
+
const siteCmd = program.command('gemini');
|
|
194
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
195
|
+
|
|
196
|
+
await program.parseAsync(['node', 'opencli', 'gemini', 'ask', '--format', 'json']);
|
|
197
|
+
|
|
198
|
+
expect(mockRenderOutput).toHaveBeenCalledWith(
|
|
199
|
+
[{ response: 'hello' }],
|
|
200
|
+
expect.objectContaining({ fmt: 'json' }),
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('commanderAdapter empty result hints', () => {
|
|
206
|
+
const cmd: CliCommand = {
|
|
207
|
+
site: 'xiaohongshu',
|
|
208
|
+
name: 'note',
|
|
209
|
+
description: 'Read one note',
|
|
210
|
+
browser: false,
|
|
211
|
+
args: [
|
|
212
|
+
{ name: 'note-id', positional: true, required: true, help: 'Note ID' },
|
|
213
|
+
],
|
|
214
|
+
func: vi.fn(),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
beforeEach(() => {
|
|
218
|
+
mockExecuteCommand.mockReset();
|
|
219
|
+
mockRenderOutput.mockReset();
|
|
220
|
+
delete process.env.OPENCLI_VERBOSE;
|
|
221
|
+
process.exitCode = undefined;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('prints the adapter hint instead of the generic outdated-adapter message', async () => {
|
|
225
|
+
const program = new Command();
|
|
226
|
+
const siteCmd = program.command('xiaohongshu');
|
|
227
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
228
|
+
|
|
229
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
230
|
+
mockExecuteCommand.mockRejectedValueOnce(
|
|
231
|
+
new EmptyResultError(
|
|
232
|
+
'xiaohongshu/note',
|
|
233
|
+
'Pass the full search_result URL with xsec_token instead of a bare note ID.',
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
await program.parseAsync(['node', 'opencli', 'xiaohongshu', 'note', '69ca3927000000001a020fd5']);
|
|
238
|
+
|
|
239
|
+
const output = errorSpy.mock.calls.flat().join('\n');
|
|
240
|
+
expect(output).toContain('xsec_token');
|
|
241
|
+
expect(output).not.toContain('this adapter may be outdated');
|
|
242
|
+
|
|
243
|
+
errorSpy.mockRestore();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('prints selector-specific hints too', async () => {
|
|
247
|
+
const program = new Command();
|
|
248
|
+
const siteCmd = program.command('xiaohongshu');
|
|
249
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
250
|
+
|
|
251
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
252
|
+
mockExecuteCommand.mockRejectedValueOnce(
|
|
253
|
+
new SelectorError('.note-title', 'The note title selector no longer matches the current page.'),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
await program.parseAsync(['node', 'opencli', 'xiaohongshu', 'note', '69ca3927000000001a020fd5']);
|
|
257
|
+
|
|
258
|
+
const output = errorSpy.mock.calls.flat().join('\n');
|
|
259
|
+
expect(output).toContain('selector no longer matches');
|
|
260
|
+
expect(output).not.toContain('this adapter may be outdated');
|
|
261
|
+
|
|
262
|
+
errorSpy.mockRestore();
|
|
263
|
+
});
|
|
264
|
+
});
|
package/src/commanderAdapter.ts
CHANGED
|
@@ -69,7 +69,7 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
subCmd
|
|
72
|
-
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
|
|
72
|
+
.option('-f, --format <fmt>', 'Output format: table, plain, json, yaml, md, csv', 'table')
|
|
73
73
|
.option('-v, --verbose', 'Debug output', false);
|
|
74
74
|
|
|
75
75
|
subCmd.addHelpText('after', formatRegistryHelpText(cmd));
|
|
@@ -95,7 +95,7 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
const verbose = optionsRecord.verbose === true;
|
|
98
|
-
|
|
98
|
+
let format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
|
|
99
99
|
if (verbose) process.env.OPENCLI_VERBOSE = '1';
|
|
100
100
|
if (cmd.deprecated) {
|
|
101
101
|
const message = typeof cmd.deprecated === 'string' ? cmd.deprecated : `${fullName(cmd)} is deprecated.`;
|
|
@@ -108,10 +108,14 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
|
|
112
|
+
if (format === 'table' && resolved.defaultFormat) {
|
|
113
|
+
format = resolved.defaultFormat;
|
|
114
|
+
}
|
|
115
|
+
|
|
111
116
|
if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
|
|
112
117
|
console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
|
|
113
118
|
}
|
|
114
|
-
const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
|
|
115
119
|
renderOutput(result, {
|
|
116
120
|
fmt: format,
|
|
117
121
|
columns: resolved.columns,
|
|
@@ -221,7 +225,7 @@ async function renderError(err: unknown, cmdName: string, verbose: boolean): Pro
|
|
|
221
225
|
if (err instanceof SelectorError || err instanceof EmptyResultError) {
|
|
222
226
|
const icon = ERROR_ICONS[err.code] ?? '⚠️';
|
|
223
227
|
console.error(chalk.red(`${icon} ${err.message}`));
|
|
224
|
-
console.error(chalk.yellow('
|
|
228
|
+
console.error(chalk.yellow(`→ ${err.hint ?? 'The page structure may have changed — this adapter may be outdated.'}`));
|
|
225
229
|
console.error(chalk.dim(` Debug: ${cmdName} --verbose`));
|
|
226
230
|
console.error(chalk.dim(` Report: ${ISSUES_URL}`));
|
|
227
231
|
return;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
|
|
3
|
+
const {
|
|
4
|
+
fetchDaemonStatusMock,
|
|
5
|
+
requestDaemonShutdownMock,
|
|
6
|
+
} = vi.hoisted(() => ({
|
|
7
|
+
fetchDaemonStatusMock: vi.fn(),
|
|
8
|
+
requestDaemonShutdownMock: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
3
11
|
vi.mock('chalk', () => ({
|
|
4
12
|
default: {
|
|
5
13
|
green: (s: string) => s,
|
|
@@ -10,12 +18,17 @@ vi.mock('chalk', () => ({
|
|
|
10
18
|
}));
|
|
11
19
|
|
|
12
20
|
const mockConnect = vi.fn();
|
|
13
|
-
vi.mock('../browser/
|
|
21
|
+
vi.mock('../browser/bridge.js', () => ({
|
|
14
22
|
BrowserBridge: class {
|
|
15
23
|
connect = mockConnect;
|
|
16
24
|
},
|
|
17
25
|
}));
|
|
18
26
|
|
|
27
|
+
vi.mock('../browser/daemon-client.js', () => ({
|
|
28
|
+
fetchDaemonStatus: fetchDaemonStatusMock,
|
|
29
|
+
requestDaemonShutdown: requestDaemonShutdownMock,
|
|
30
|
+
}));
|
|
31
|
+
|
|
19
32
|
import { daemonStatus, daemonStop, daemonRestart } from './daemon.js';
|
|
20
33
|
|
|
21
34
|
describe('daemon commands', () => {
|
|
@@ -25,6 +38,8 @@ describe('daemon commands', () => {
|
|
|
25
38
|
beforeEach(() => {
|
|
26
39
|
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
27
40
|
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
41
|
+
fetchDaemonStatusMock.mockReset();
|
|
42
|
+
requestDaemonShutdownMock.mockReset();
|
|
28
43
|
});
|
|
29
44
|
|
|
30
45
|
afterEach(() => {
|
|
@@ -34,7 +49,7 @@ describe('daemon commands', () => {
|
|
|
34
49
|
|
|
35
50
|
describe('daemonStatus', () => {
|
|
36
51
|
it('shows "not running" when daemon is unreachable', async () => {
|
|
37
|
-
|
|
52
|
+
fetchDaemonStatusMock.mockResolvedValue(null);
|
|
38
53
|
|
|
39
54
|
await daemonStatus();
|
|
40
55
|
|
|
@@ -42,7 +57,7 @@ describe('daemon commands', () => {
|
|
|
42
57
|
});
|
|
43
58
|
|
|
44
59
|
it('shows "not running" when daemon returns non-ok response', async () => {
|
|
45
|
-
|
|
60
|
+
fetchDaemonStatusMock.mockResolvedValue(null);
|
|
46
61
|
|
|
47
62
|
await daemonStatus();
|
|
48
63
|
|
|
@@ -61,13 +76,7 @@ describe('daemon commands', () => {
|
|
|
61
76
|
port: 19825,
|
|
62
77
|
};
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
'fetch',
|
|
66
|
-
vi.fn().mockResolvedValue({
|
|
67
|
-
ok: true,
|
|
68
|
-
json: () => Promise.resolve(status),
|
|
69
|
-
}),
|
|
70
|
-
);
|
|
79
|
+
fetchDaemonStatusMock.mockResolvedValue(status);
|
|
71
80
|
|
|
72
81
|
await daemonStatus();
|
|
73
82
|
|
|
@@ -91,13 +100,7 @@ describe('daemon commands', () => {
|
|
|
91
100
|
port: 19825,
|
|
92
101
|
};
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
'fetch',
|
|
96
|
-
vi.fn().mockResolvedValue({
|
|
97
|
-
ok: true,
|
|
98
|
-
json: () => Promise.resolve(status),
|
|
99
|
-
}),
|
|
100
|
-
);
|
|
103
|
+
fetchDaemonStatusMock.mockResolvedValue(status);
|
|
101
104
|
|
|
102
105
|
await daemonStatus();
|
|
103
106
|
|
|
@@ -107,7 +110,7 @@ describe('daemon commands', () => {
|
|
|
107
110
|
|
|
108
111
|
describe('daemonStop', () => {
|
|
109
112
|
it('reports "not running" when daemon is unreachable', async () => {
|
|
110
|
-
|
|
113
|
+
fetchDaemonStatusMock.mockResolvedValue(null);
|
|
111
114
|
|
|
112
115
|
await daemonStop();
|
|
113
116
|
|
|
@@ -115,59 +118,36 @@ describe('daemon commands', () => {
|
|
|
115
118
|
});
|
|
116
119
|
|
|
117
120
|
it('sends shutdown and reports success', async () => {
|
|
118
|
-
|
|
121
|
+
fetchDaemonStatusMock.mockResolvedValue({
|
|
119
122
|
ok: true,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
port: 19825,
|
|
130
|
-
}),
|
|
131
|
-
};
|
|
132
|
-
const shutdownResponse = { ok: true };
|
|
133
|
-
|
|
134
|
-
const mockFetch = vi.fn()
|
|
135
|
-
.mockResolvedValueOnce(statusResponse)
|
|
136
|
-
.mockResolvedValueOnce(shutdownResponse);
|
|
137
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
123
|
+
pid: 12345,
|
|
124
|
+
uptime: 100,
|
|
125
|
+
extensionConnected: true,
|
|
126
|
+
pending: 0,
|
|
127
|
+
lastCliRequestTime: Date.now(),
|
|
128
|
+
memoryMB: 50,
|
|
129
|
+
port: 19825,
|
|
130
|
+
});
|
|
131
|
+
requestDaemonShutdownMock.mockResolvedValue(true);
|
|
138
132
|
|
|
139
133
|
await daemonStop();
|
|
140
134
|
|
|
141
|
-
|
|
142
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
143
|
-
const shutdownCall = mockFetch.mock.calls[1];
|
|
144
|
-
expect(shutdownCall[0]).toContain('/shutdown');
|
|
145
|
-
expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
|
|
146
|
-
|
|
135
|
+
expect(requestDaemonShutdownMock).toHaveBeenCalledTimes(1);
|
|
147
136
|
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon stopped'));
|
|
148
137
|
});
|
|
149
138
|
|
|
150
139
|
it('reports failure when shutdown request fails', async () => {
|
|
151
|
-
|
|
140
|
+
fetchDaemonStatusMock.mockResolvedValue({
|
|
152
141
|
ok: true,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
port: 19825,
|
|
163
|
-
}),
|
|
164
|
-
};
|
|
165
|
-
const shutdownResponse = { ok: false };
|
|
166
|
-
|
|
167
|
-
const mockFetch = vi.fn()
|
|
168
|
-
.mockResolvedValueOnce(statusResponse)
|
|
169
|
-
.mockResolvedValueOnce(shutdownResponse);
|
|
170
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
142
|
+
pid: 12345,
|
|
143
|
+
uptime: 100,
|
|
144
|
+
extensionConnected: true,
|
|
145
|
+
pending: 0,
|
|
146
|
+
lastCliRequestTime: Date.now(),
|
|
147
|
+
memoryMB: 50,
|
|
148
|
+
port: 19825,
|
|
149
|
+
});
|
|
150
|
+
requestDaemonShutdownMock.mockResolvedValue(false);
|
|
171
151
|
|
|
172
152
|
await daemonStop();
|
|
173
153
|
|
|
@@ -188,7 +168,7 @@ describe('daemon commands', () => {
|
|
|
188
168
|
};
|
|
189
169
|
|
|
190
170
|
it('starts daemon directly when not running', async () => {
|
|
191
|
-
|
|
171
|
+
fetchDaemonStatusMock.mockResolvedValue(null);
|
|
192
172
|
mockConnect.mockResolvedValue(undefined);
|
|
193
173
|
|
|
194
174
|
await daemonRestart();
|
|
@@ -198,36 +178,22 @@ describe('daemon commands', () => {
|
|
|
198
178
|
});
|
|
199
179
|
|
|
200
180
|
it('stops then starts when daemon is running', async () => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.mockResolvedValueOnce(
|
|
204
|
-
|
|
205
|
-
.mockResolvedValueOnce({ ok: true })
|
|
206
|
-
// Subsequent calls: polling fetchStatus until unreachable
|
|
207
|
-
.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
208
|
-
|
|
209
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
181
|
+
fetchDaemonStatusMock
|
|
182
|
+
.mockResolvedValueOnce(statusData)
|
|
183
|
+
.mockResolvedValueOnce(null);
|
|
184
|
+
requestDaemonShutdownMock.mockResolvedValue(true);
|
|
210
185
|
mockConnect.mockResolvedValue(undefined);
|
|
211
186
|
|
|
212
187
|
await daemonRestart();
|
|
213
188
|
|
|
214
|
-
|
|
215
|
-
const shutdownCall = mockFetch.mock.calls[1];
|
|
216
|
-
expect(shutdownCall[0]).toContain('/shutdown');
|
|
217
|
-
expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
|
|
218
|
-
|
|
189
|
+
expect(requestDaemonShutdownMock).toHaveBeenCalledTimes(1);
|
|
219
190
|
expect(mockConnect).toHaveBeenCalledWith({ timeout: 10 });
|
|
220
191
|
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon restarted'));
|
|
221
192
|
});
|
|
222
193
|
|
|
223
194
|
it('aborts when shutdown fails', async () => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(statusData) })
|
|
227
|
-
// requestShutdown — failure
|
|
228
|
-
.mockResolvedValueOnce({ ok: false });
|
|
229
|
-
|
|
230
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
195
|
+
fetchDaemonStatusMock.mockResolvedValue(statusData);
|
|
196
|
+
requestDaemonShutdownMock.mockResolvedValue(false);
|
|
231
197
|
|
|
232
198
|
await daemonRestart();
|
|
233
199
|
|
package/src/commands/daemon.ts
CHANGED
|
@@ -6,55 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
12
|
-
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
13
|
-
|
|
14
|
-
interface DaemonStatus {
|
|
15
|
-
ok: boolean;
|
|
16
|
-
pid: number;
|
|
17
|
-
uptime: number;
|
|
18
|
-
extensionConnected: boolean;
|
|
19
|
-
pending: number;
|
|
20
|
-
lastCliRequestTime: number;
|
|
21
|
-
memoryMB: number;
|
|
22
|
-
port: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function fetchStatus(): Promise<DaemonStatus | null> {
|
|
26
|
-
const controller = new AbortController();
|
|
27
|
-
const timer = setTimeout(() => controller.abort(), 2000);
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch(`${DAEMON_URL}/status`, {
|
|
30
|
-
headers: { 'X-OpenCLI': '1' },
|
|
31
|
-
signal: controller.signal,
|
|
32
|
-
});
|
|
33
|
-
if (!res.ok) return null;
|
|
34
|
-
return await res.json() as DaemonStatus;
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
} finally {
|
|
38
|
-
clearTimeout(timer);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function requestShutdown(): Promise<boolean> {
|
|
43
|
-
const controller = new AbortController();
|
|
44
|
-
const timer = setTimeout(() => controller.abort(), 5000);
|
|
45
|
-
try {
|
|
46
|
-
const res = await fetch(`${DAEMON_URL}/shutdown`, {
|
|
47
|
-
method: 'POST',
|
|
48
|
-
headers: { 'X-OpenCLI': '1' },
|
|
49
|
-
signal: controller.signal,
|
|
50
|
-
});
|
|
51
|
-
return res.ok;
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
} finally {
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
9
|
+
import { fetchDaemonStatus, requestDaemonShutdown } from '../browser/daemon-client.js';
|
|
58
10
|
|
|
59
11
|
function formatUptime(seconds: number): string {
|
|
60
12
|
const h = Math.floor(seconds / 3600);
|
|
@@ -74,7 +26,7 @@ function formatTimeSince(timestampMs: number): string {
|
|
|
74
26
|
}
|
|
75
27
|
|
|
76
28
|
export async function daemonStatus(): Promise<void> {
|
|
77
|
-
const status = await
|
|
29
|
+
const status = await fetchDaemonStatus();
|
|
78
30
|
if (!status) {
|
|
79
31
|
console.log(`Daemon: ${chalk.dim('not running')}`);
|
|
80
32
|
return;
|
|
@@ -89,13 +41,13 @@ export async function daemonStatus(): Promise<void> {
|
|
|
89
41
|
}
|
|
90
42
|
|
|
91
43
|
export async function daemonStop(): Promise<void> {
|
|
92
|
-
const status = await
|
|
44
|
+
const status = await fetchDaemonStatus();
|
|
93
45
|
if (!status) {
|
|
94
46
|
console.log(chalk.dim('Daemon is not running.'));
|
|
95
47
|
return;
|
|
96
48
|
}
|
|
97
49
|
|
|
98
|
-
const ok = await
|
|
50
|
+
const ok = await requestDaemonShutdown();
|
|
99
51
|
if (ok) {
|
|
100
52
|
console.log(chalk.green('Daemon stopped.'));
|
|
101
53
|
} else {
|
|
@@ -105,9 +57,9 @@ export async function daemonStop(): Promise<void> {
|
|
|
105
57
|
}
|
|
106
58
|
|
|
107
59
|
export async function daemonRestart(): Promise<void> {
|
|
108
|
-
const status = await
|
|
60
|
+
const status = await fetchDaemonStatus();
|
|
109
61
|
if (status) {
|
|
110
|
-
const ok = await
|
|
62
|
+
const ok = await requestDaemonShutdown();
|
|
111
63
|
if (!ok) {
|
|
112
64
|
console.error(chalk.red('Failed to stop daemon.'));
|
|
113
65
|
process.exitCode = 1;
|
|
@@ -117,12 +69,12 @@ export async function daemonRestart(): Promise<void> {
|
|
|
117
69
|
const deadline = Date.now() + 5000;
|
|
118
70
|
while (Date.now() < deadline) {
|
|
119
71
|
await new Promise(r => setTimeout(r, 200));
|
|
120
|
-
if (!(await
|
|
72
|
+
if (!(await fetchDaemonStatus())) break;
|
|
121
73
|
}
|
|
122
74
|
}
|
|
123
75
|
|
|
124
76
|
// Import BrowserBridge to spawn a new daemon
|
|
125
|
-
const { BrowserBridge } = await import('../browser/
|
|
77
|
+
const { BrowserBridge } = await import('../browser/bridge.js');
|
|
126
78
|
const bridge = new BrowserBridge();
|
|
127
79
|
try {
|
|
128
80
|
console.log('Starting daemon...');
|
package/src/discovery.ts
CHANGED
|
@@ -76,6 +76,28 @@ export async function ensureUserCliCompatShims(baseDir: string = USER_OPENCLI_DI
|
|
|
76
76
|
`${JSON.stringify({ name: 'opencli-user-runtime', private: true, type: 'module' }, null, 2)}\n`,
|
|
77
77
|
),
|
|
78
78
|
]);
|
|
79
|
+
|
|
80
|
+
// Create node_modules/@jackwener/opencli symlink so user TS CLIs can import
|
|
81
|
+
// from '@jackwener/opencli/registry' (the package export).
|
|
82
|
+
// This is needed because ~/.opencli/clis/ is outside opencli's node_modules tree.
|
|
83
|
+
const opencliRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
84
|
+
const symlinkDir = path.join(baseDir, 'node_modules', '@jackwener');
|
|
85
|
+
const symlinkPath = path.join(symlinkDir, 'opencli');
|
|
86
|
+
try {
|
|
87
|
+
// Only recreate if symlink is missing or points to wrong target
|
|
88
|
+
let needsUpdate = true;
|
|
89
|
+
try {
|
|
90
|
+
const existing = await fs.promises.readlink(symlinkPath);
|
|
91
|
+
if (existing === opencliRoot) needsUpdate = false;
|
|
92
|
+
} catch { /* doesn't exist */ }
|
|
93
|
+
if (needsUpdate) {
|
|
94
|
+
await fs.promises.mkdir(symlinkDir, { recursive: true });
|
|
95
|
+
try { await fs.promises.unlink(symlinkPath); } catch { /* doesn't exist */ }
|
|
96
|
+
await fs.promises.symlink(opencliRoot, symlinkPath, 'dir');
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Non-fatal: npm-linked installs or permission issues may prevent this
|
|
100
|
+
}
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
/**
|
package/src/doctor.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* opencli doctor — diagnose browser connectivity.
|
|
3
3
|
*
|
|
4
|
-
* Simplified for the daemon-based architecture.
|
|
5
|
-
* MCP path discovery, or config file scanning.
|
|
4
|
+
* Simplified for the daemon-based architecture.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import chalk from 'chalk';
|
|
@@ -26,6 +25,7 @@ export type ConnectivityResult = {
|
|
|
26
25
|
durationMs: number;
|
|
27
26
|
};
|
|
28
27
|
|
|
28
|
+
|
|
29
29
|
export type DoctorReport = {
|
|
30
30
|
cliVersion?: string;
|
|
31
31
|
daemonRunning: boolean;
|
|
@@ -42,11 +42,11 @@ export type DoctorReport = {
|
|
|
42
42
|
export async function checkConnectivity(opts?: { timeout?: number }): Promise<ConnectivityResult> {
|
|
43
43
|
const start = Date.now();
|
|
44
44
|
try {
|
|
45
|
-
const
|
|
46
|
-
const page = await
|
|
45
|
+
const bridge = new BrowserBridge();
|
|
46
|
+
const page = await bridge.connect({ timeout: opts?.timeout ?? 8 });
|
|
47
47
|
// Try a simple eval to verify end-to-end connectivity
|
|
48
48
|
await page.evaluate('1 + 1');
|
|
49
|
-
await
|
|
49
|
+
await bridge.close();
|
|
50
50
|
return { ok: true, durationMs: Date.now() - start };
|
|
51
51
|
} catch (err) {
|
|
52
52
|
return { ok: false, error: getErrorMessage(err), durationMs: Date.now() - start };
|
|
@@ -58,9 +58,9 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
58
58
|
let initialStatus = await checkDaemonStatus();
|
|
59
59
|
if (!initialStatus.running) {
|
|
60
60
|
try {
|
|
61
|
-
const
|
|
62
|
-
await
|
|
63
|
-
await
|
|
61
|
+
const bridge = new BrowserBridge();
|
|
62
|
+
await bridge.connect({ timeout: 5 });
|
|
63
|
+
await bridge.close();
|
|
64
64
|
} catch {
|
|
65
65
|
// Auto-start failed; we'll report it below.
|
|
66
66
|
}
|
|
@@ -94,7 +94,6 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
94
94
|
if (connectivity && !connectivity.ok) {
|
|
95
95
|
issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
|
|
96
96
|
}
|
|
97
|
-
|
|
98
97
|
if (status.extensionVersion && opts.cliVersion) {
|
|
99
98
|
const extMajor = status.extensionVersion.split('.')[0];
|
|
100
99
|
const cliMajor = opts.cliVersion.split('.')[0];
|
package/src/explore.ts
CHANGED
|
@@ -135,7 +135,7 @@ export interface ExploreBundle {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
* Parse raw network output from
|
|
138
|
+
* Parse raw network output from browser page.
|
|
139
139
|
* Handles text format: [GET] url => [200]
|
|
140
140
|
*/
|
|
141
141
|
function parseNetworkRequests(raw: unknown): NetworkEntry[] {
|