@jackwener/opencli 1.0.3 → 1.0.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/.github/workflows/build-extension.yml +21 -3
- package/.github/workflows/docs.yml +52 -0
- package/README.md +28 -28
- package/README.zh-CN.md +28 -28
- package/dist/browser/cdp.d.ts +16 -1
- package/dist/browser/cdp.js +124 -80
- package/dist/browser/daemon-client.d.ts +3 -1
- package/dist/browser/daemon-client.js +4 -0
- package/dist/browser/dom-helpers.d.ts +20 -0
- package/dist/browser/dom-helpers.js +109 -0
- package/dist/browser/mcp.d.ts +1 -0
- package/dist/browser/mcp.js +10 -5
- package/dist/browser/page.d.ts +7 -0
- package/dist/browser/page.js +37 -100
- package/dist/browser.test.js +7 -0
- package/dist/build-manifest.js +3 -1
- package/dist/build-manifest.test.js +34 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +30 -0
- package/dist/capabilityRouting.test.d.ts +1 -0
- package/dist/capabilityRouting.test.js +42 -0
- package/dist/chaoxing.test.js +11 -4
- package/dist/cli-manifest.json +635 -1
- package/dist/cli.js +48 -8
- package/dist/clis/antigravity/serve.d.ts +14 -0
- package/dist/clis/antigravity/serve.js +263 -0
- package/dist/clis/bilibili/download.js +4 -14
- package/dist/clis/boss/resume.d.ts +1 -0
- package/dist/clis/boss/resume.js +249 -0
- package/dist/clis/hf/top.d.ts +1 -0
- package/dist/clis/hf/top.js +119 -0
- package/dist/clis/jike/comment.d.ts +1 -0
- package/dist/clis/jike/comment.js +107 -0
- package/dist/clis/jike/create.d.ts +1 -0
- package/dist/clis/jike/create.js +106 -0
- package/dist/clis/jike/feed.d.ts +1 -0
- package/dist/clis/jike/feed.js +67 -0
- package/dist/clis/jike/like.d.ts +1 -0
- package/dist/clis/jike/like.js +61 -0
- package/dist/clis/jike/notifications.d.ts +1 -0
- package/dist/clis/jike/notifications.js +169 -0
- package/dist/clis/jike/post.yaml +58 -0
- package/dist/clis/jike/repost.d.ts +1 -0
- package/dist/clis/jike/repost.js +103 -0
- package/dist/clis/jike/search.d.ts +1 -0
- package/dist/clis/jike/search.js +67 -0
- package/dist/clis/jike/shared.d.ts +19 -0
- package/dist/clis/jike/shared.js +25 -0
- package/dist/clis/jike/topic.yaml +52 -0
- package/dist/clis/jike/user.yaml +51 -0
- package/dist/clis/smzdm/search.js +28 -39
- package/dist/clis/stackoverflow/bounties.yaml +29 -0
- package/dist/clis/stackoverflow/hot.yaml +28 -0
- package/dist/clis/stackoverflow/search.yaml +32 -0
- package/dist/clis/stackoverflow/unanswered.yaml +28 -0
- package/dist/clis/twitter/download.js +6 -16
- package/dist/clis/xiaohongshu/download.js +3 -3
- package/dist/clis/zhihu/download.js +3 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +16 -0
- package/dist/download/index.d.ts +12 -8
- package/dist/download/index.js +11 -3
- package/dist/download/index.test.d.ts +1 -0
- package/dist/download/index.test.js +14 -0
- package/dist/engine.js +5 -5
- package/dist/explore.d.ts +1 -0
- package/dist/explore.js +3 -3
- package/dist/generate.js +1 -0
- package/dist/interceptor.js +3 -2
- package/dist/output.d.ts +1 -0
- package/dist/output.js +3 -1
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/download.js +14 -18
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +5 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +2 -2
- package/dist/types.d.ts +12 -0
- package/dist/verify.d.ts +6 -1
- package/dist/verify.js +54 -2
- package/docs/.vitepress/config.mts +193 -0
- package/docs/adapters/browser/apple-podcasts.md +28 -0
- package/docs/adapters/browser/bbc.md +26 -0
- package/docs/adapters/browser/bilibili.md +38 -0
- package/docs/adapters/browser/boss.md +28 -0
- package/docs/adapters/browser/coupang.md +28 -0
- package/docs/adapters/browser/ctrip.md +27 -0
- package/docs/adapters/browser/github.md +26 -0
- package/docs/adapters/browser/hackernews.md +26 -0
- package/docs/adapters/browser/linkedin.md +27 -0
- package/docs/adapters/browser/reddit.md +41 -0
- package/docs/adapters/browser/reuters.md +27 -0
- package/docs/adapters/browser/smzdm.md +27 -0
- package/docs/adapters/browser/twitter.md +47 -0
- package/docs/adapters/browser/v2ex.md +32 -0
- package/docs/adapters/browser/weibo.md +27 -0
- package/docs/adapters/browser/xiaohongshu.md +32 -0
- package/docs/adapters/browser/xiaoyuzhou.md +28 -0
- package/docs/adapters/browser/xueqiu.md +32 -0
- package/docs/adapters/browser/yahoo-finance.md +26 -0
- package/docs/adapters/browser/youtube.md +29 -0
- package/docs/adapters/browser/zhihu.md +30 -0
- package/docs/adapters/desktop/antigravity.md +46 -0
- package/docs/adapters/desktop/chatgpt.md +43 -0
- package/docs/adapters/desktop/chatwise.md +38 -0
- package/docs/adapters/desktop/codex.md +32 -0
- package/docs/adapters/desktop/cursor.md +33 -0
- package/docs/adapters/desktop/discord.md +28 -0
- package/docs/adapters/desktop/feishu.md +20 -0
- package/docs/adapters/desktop/neteasemusic.md +31 -0
- package/docs/adapters/desktop/notion.md +29 -0
- package/docs/adapters/desktop/wechat.md +28 -0
- package/docs/adapters/index.md +49 -0
- package/docs/advanced/cdp.md +103 -0
- package/docs/advanced/download.md +63 -0
- package/docs/advanced/electron.md +125 -0
- package/docs/advanced/remote-chrome.md +72 -0
- package/docs/developer/ai-workflow.md +66 -0
- package/docs/developer/architecture.md +90 -0
- package/docs/developer/contributing.md +136 -0
- package/docs/developer/testing.md +237 -0
- package/docs/developer/ts-adapter.md +87 -0
- package/docs/developer/yaml-adapter.md +108 -0
- package/docs/guide/browser-bridge.md +38 -0
- package/docs/guide/getting-started.md +56 -0
- package/docs/guide/installation.md +37 -0
- package/docs/guide/troubleshooting.md +56 -0
- package/docs/index.md +35 -0
- package/docs/zh/adapters/index.md +5 -0
- package/docs/zh/advanced/cdp.md +3 -0
- package/docs/zh/developer/contributing.md +24 -0
- package/docs/zh/guide/browser-bridge.md +25 -0
- package/docs/zh/guide/getting-started.md +40 -0
- package/docs/zh/guide/installation.md +37 -0
- package/docs/zh/index.md +29 -0
- package/extension/dist/background.js +92 -52
- package/extension/package-lock.json +1156 -0
- package/extension/src/background.test.ts +151 -0
- package/extension/src/background.ts +122 -51
- package/extension/src/protocol.ts +3 -1
- package/package.json +7 -3
- package/src/browser/cdp.ts +154 -82
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.ts +116 -0
- package/src/browser/mcp.ts +14 -6
- package/src/browser/page.ts +45 -100
- package/src/browser.test.ts +10 -0
- package/src/build-manifest.test.ts +36 -0
- package/src/build-manifest.ts +2 -1
- package/src/capabilityRouting.test.ts +47 -0
- package/src/capabilityRouting.ts +28 -0
- package/src/chaoxing.test.ts +12 -4
- package/src/cli.ts +30 -8
- package/src/clis/antigravity/serve.ts +329 -0
- package/src/clis/bilibili/download.ts +4 -15
- package/src/clis/boss/resume.ts +262 -0
- package/src/clis/hf/top.ts +141 -0
- package/src/clis/jike/comment.ts +113 -0
- package/src/clis/jike/create.ts +113 -0
- package/src/clis/jike/feed.ts +74 -0
- package/src/clis/jike/like.ts +65 -0
- package/src/clis/jike/notifications.ts +185 -0
- package/src/clis/jike/post.yaml +58 -0
- package/src/clis/jike/repost.ts +114 -0
- package/src/clis/jike/search.ts +74 -0
- package/src/clis/jike/shared.ts +36 -0
- package/src/clis/jike/topic.yaml +52 -0
- package/src/clis/jike/user.yaml +51 -0
- package/src/clis/smzdm/search.ts +30 -39
- package/src/clis/stackoverflow/bounties.yaml +29 -0
- package/src/clis/stackoverflow/hot.yaml +28 -0
- package/src/clis/stackoverflow/search.yaml +32 -0
- package/src/clis/stackoverflow/unanswered.yaml +28 -0
- package/src/clis/twitter/download.ts +6 -17
- package/src/clis/xiaohongshu/download.ts +3 -3
- package/src/clis/zhihu/download.ts +3 -3
- package/src/doctor.ts +18 -2
- package/src/download/index.test.ts +16 -0
- package/src/download/index.ts +22 -4
- package/src/engine.ts +4 -4
- package/src/explore.ts +4 -4
- package/src/generate.ts +1 -0
- package/src/interceptor.ts +3 -2
- package/src/output.ts +3 -1
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.ts +14 -17
- package/src/registry.ts +6 -2
- package/src/runtime.ts +3 -2
- package/src/types.ts +9 -0
- package/src/verify.ts +64 -3
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from 'node:fs';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { cli, Strategy } from '../../registry.js';
|
|
11
|
-
import { sanitizeFilename, httpDownload } from '../../download/index.js';
|
|
11
|
+
import { sanitizeFilename, httpDownload, formatCookieHeader } from '../../download/index.js';
|
|
12
12
|
import { formatBytes } from '../../download/progress.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -178,7 +178,7 @@ cli({
|
|
|
178
178
|
const imagesDir = path.join(output, 'images');
|
|
179
179
|
fs.mkdirSync(imagesDir, { recursive: true });
|
|
180
180
|
|
|
181
|
-
const cookies = await page.
|
|
181
|
+
const cookies = formatCookieHeader(await page.getCookies({ domain: 'zhihu.com' }));
|
|
182
182
|
|
|
183
183
|
for (let i = 0; i < data.images.length; i++) {
|
|
184
184
|
const imgUrl = data.images[i];
|
|
@@ -188,7 +188,7 @@ cli({
|
|
|
188
188
|
|
|
189
189
|
try {
|
|
190
190
|
await httpDownload(imgUrl, imgPath, {
|
|
191
|
-
cookies
|
|
191
|
+
cookies,
|
|
192
192
|
timeout: 30000,
|
|
193
193
|
});
|
|
194
194
|
|
package/src/doctor.ts
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
10
10
|
import { BrowserBridge } from './browser/index.js';
|
|
11
|
-
import {
|
|
11
|
+
import { listSessions } from './browser/daemon-client.js';
|
|
12
12
|
|
|
13
13
|
export type DoctorOptions = {
|
|
14
14
|
fix?: boolean;
|
|
15
15
|
yes?: boolean;
|
|
16
16
|
live?: boolean;
|
|
17
|
+
sessions?: boolean;
|
|
17
18
|
cliVersion?: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -28,6 +29,7 @@ export type DoctorReport = {
|
|
|
28
29
|
daemonRunning: boolean;
|
|
29
30
|
extensionConnected: boolean;
|
|
30
31
|
connectivity?: ConnectivityResult;
|
|
32
|
+
sessions?: Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>;
|
|
31
33
|
issues: string[];
|
|
32
34
|
};
|
|
33
35
|
|
|
@@ -55,6 +57,9 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
55
57
|
if (opts.live) {
|
|
56
58
|
connectivity = await checkConnectivity();
|
|
57
59
|
}
|
|
60
|
+
const sessions = opts.sessions && status.running && status.extensionConnected
|
|
61
|
+
? await listSessions() as Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>
|
|
62
|
+
: undefined;
|
|
58
63
|
|
|
59
64
|
const issues: string[] = [];
|
|
60
65
|
if (!status.running) {
|
|
@@ -78,6 +83,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
78
83
|
daemonRunning: status.running,
|
|
79
84
|
extensionConnected: status.extensionConnected,
|
|
80
85
|
connectivity,
|
|
86
|
+
sessions,
|
|
81
87
|
issues,
|
|
82
88
|
};
|
|
83
89
|
}
|
|
@@ -104,6 +110,17 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
|
104
110
|
lines.push(`${chalk.dim('[SKIP]')} Connectivity: not tested (use --live)`);
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
if (report.sessions) {
|
|
114
|
+
lines.push('', chalk.bold('Sessions:'));
|
|
115
|
+
if (report.sessions.length === 0) {
|
|
116
|
+
lines.push(chalk.dim(' • no active automation sessions'));
|
|
117
|
+
} else {
|
|
118
|
+
for (const session of report.sessions) {
|
|
119
|
+
lines.push(chalk.dim(` • ${session.workspace} → window ${session.windowId}, tabs=${session.tabCount}, idle=${Math.ceil(session.idleMsRemaining / 1000)}s`));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
107
124
|
if (report.issues.length) {
|
|
108
125
|
lines.push('', chalk.yellow('Issues:'));
|
|
109
126
|
for (const issue of report.issues) {
|
|
@@ -115,4 +132,3 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
|
115
132
|
|
|
116
133
|
return lines.join('\n');
|
|
117
134
|
}
|
|
118
|
-
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { formatCookieHeader, resolveRedirectUrl } from './index.js';
|
|
3
|
+
|
|
4
|
+
describe('download helpers', () => {
|
|
5
|
+
it('resolves relative redirects against the original URL', () => {
|
|
6
|
+
expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
|
|
7
|
+
expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('formats browser cookies into a Cookie header', () => {
|
|
11
|
+
expect(formatCookieHeader([
|
|
12
|
+
{ name: 'sid', value: 'abc', domain: 'example.com' },
|
|
13
|
+
{ name: 'ct0', value: 'def', domain: 'example.com' },
|
|
14
|
+
])).toBe('sid=abc; ct0=def');
|
|
15
|
+
});
|
|
16
|
+
});
|
package/src/download/index.ts
CHANGED
|
@@ -26,6 +26,16 @@ export interface YtdlpOptions {
|
|
|
26
26
|
onProgress?: (percent: number) => void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface BrowserCookie {
|
|
30
|
+
name: string;
|
|
31
|
+
value: string;
|
|
32
|
+
domain: string;
|
|
33
|
+
path?: string;
|
|
34
|
+
secure?: boolean;
|
|
35
|
+
httpOnly?: boolean;
|
|
36
|
+
expirationDate?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
/**
|
|
30
40
|
* Check if yt-dlp is available in PATH.
|
|
31
41
|
*/
|
|
@@ -142,14 +152,14 @@ export async function httpDownload(
|
|
|
142
152
|
// Handle redirects
|
|
143
153
|
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
144
154
|
file.close();
|
|
145
|
-
fs.unlinkSync(tempPath);
|
|
146
|
-
httpDownload(response.headers.location, destPath, options).then(resolve);
|
|
155
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
156
|
+
httpDownload(resolveRedirectUrl(url, response.headers.location), destPath, options).then(resolve);
|
|
147
157
|
return;
|
|
148
158
|
}
|
|
149
159
|
|
|
150
160
|
if (response.statusCode !== 200) {
|
|
151
161
|
file.close();
|
|
152
|
-
fs.unlinkSync(tempPath);
|
|
162
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
153
163
|
resolve({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
|
|
154
164
|
return;
|
|
155
165
|
}
|
|
@@ -187,11 +197,15 @@ export async function httpDownload(
|
|
|
187
197
|
});
|
|
188
198
|
}
|
|
189
199
|
|
|
200
|
+
export function resolveRedirectUrl(currentUrl: string, location: string): string {
|
|
201
|
+
return new URL(location, currentUrl).toString();
|
|
202
|
+
}
|
|
203
|
+
|
|
190
204
|
/**
|
|
191
205
|
* Export cookies to Netscape format for yt-dlp.
|
|
192
206
|
*/
|
|
193
207
|
export function exportCookiesToNetscape(
|
|
194
|
-
cookies:
|
|
208
|
+
cookies: BrowserCookie[],
|
|
195
209
|
filePath: string,
|
|
196
210
|
): void {
|
|
197
211
|
const lines = [
|
|
@@ -214,6 +228,10 @@ export function exportCookiesToNetscape(
|
|
|
214
228
|
fs.writeFileSync(filePath, lines.join('\n'));
|
|
215
229
|
}
|
|
216
230
|
|
|
231
|
+
export function formatCookieHeader(cookies: BrowserCookie[]): string {
|
|
232
|
+
return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; ');
|
|
233
|
+
}
|
|
234
|
+
|
|
217
235
|
/**
|
|
218
236
|
* Download video using yt-dlp.
|
|
219
237
|
*/
|
package/src/engine.ts
CHANGED
|
@@ -99,12 +99,12 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
|
99
99
|
async function discoverClisFromFs(dir: string): Promise<void> {
|
|
100
100
|
try { await fs.promises.access(dir); } catch { return; }
|
|
101
101
|
const promises: Promise<any>[] = [];
|
|
102
|
-
const
|
|
102
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
103
103
|
|
|
104
|
-
for (const
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (!entry.isDirectory()) continue;
|
|
106
|
+
const site = entry.name;
|
|
105
107
|
const siteDir = path.join(dir, site);
|
|
106
|
-
const stat = await fs.promises.stat(siteDir);
|
|
107
|
-
if (!stat.isDirectory()) continue;
|
|
108
108
|
const files = await fs.promises.readdir(siteDir);
|
|
109
109
|
for (const file of files) {
|
|
110
110
|
const filePath = path.join(siteDir, file);
|
package/src/explore.ts
CHANGED
|
@@ -231,7 +231,7 @@ export async function exploreUrl(
|
|
|
231
231
|
BrowserFactory: new () => any;
|
|
232
232
|
site?: string; goal?: string; authenticated?: boolean;
|
|
233
233
|
outDir?: string; waitSeconds?: number; query?: string;
|
|
234
|
-
clickLabels?: string[]; auto?: boolean;
|
|
234
|
+
clickLabels?: string[]; auto?: boolean; workspace?: string;
|
|
235
235
|
},
|
|
236
236
|
): Promise<Record<string, any>> {
|
|
237
237
|
const waitSeconds = opts.waitSeconds ?? 3.0;
|
|
@@ -280,7 +280,7 @@ export async function exploreUrl(
|
|
|
280
280
|
|
|
281
281
|
// Step 5: For JSON endpoints missing a body, carefully re-fetch in-browser via a pristine iframe
|
|
282
282
|
const jsonEndpoints = networkEntries.filter(e => e.contentType.includes('json') && e.method === 'GET' && e.status === 200 && !e.responseBody);
|
|
283
|
-
|
|
283
|
+
await Promise.allSettled(jsonEndpoints.slice(0, 5).map(async (ep) => {
|
|
284
284
|
try {
|
|
285
285
|
const body = await page.evaluate(`async () => {
|
|
286
286
|
let iframe = null;
|
|
@@ -302,7 +302,7 @@ export async function exploreUrl(
|
|
|
302
302
|
if (body && typeof body === 'string') { try { ep.responseBody = JSON.parse(body); } catch {} }
|
|
303
303
|
else if (body && typeof body === 'object') ep.responseBody = body;
|
|
304
304
|
} catch {}
|
|
305
|
-
}
|
|
305
|
+
}));
|
|
306
306
|
|
|
307
307
|
// Step 6: Detect framework
|
|
308
308
|
let framework: Record<string, boolean> = {};
|
|
@@ -438,7 +438,7 @@ export async function exploreUrl(
|
|
|
438
438
|
|
|
439
439
|
return { ...result, out_dir: targetDir };
|
|
440
440
|
})(), { timeout: exploreTimeout, label: `Explore ${url}` });
|
|
441
|
-
});
|
|
441
|
+
}, { workspace: opts.workspace });
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
export function renderExploreSummary(result: Record<string, any>): string {
|
package/src/generate.ts
CHANGED
package/src/interceptor.ts
CHANGED
|
@@ -27,6 +27,7 @@ export function generateInterceptorJs(
|
|
|
27
27
|
return `
|
|
28
28
|
() => {
|
|
29
29
|
window.${arr} = window.${arr} || [];
|
|
30
|
+
window.${arr}_errors = window.${arr}_errors || [];
|
|
30
31
|
const __pattern = ${patternExpr};
|
|
31
32
|
|
|
32
33
|
if (!window.${guard}) {
|
|
@@ -43,7 +44,7 @@ export function generateInterceptorJs(
|
|
|
43
44
|
const clone = response.clone();
|
|
44
45
|
const json = await clone.json();
|
|
45
46
|
window.${arr}.push(json);
|
|
46
|
-
} catch(e) {}
|
|
47
|
+
} catch(e) { window.${arr}_errors.push({ url: reqUrl, error: String(e) }); }
|
|
47
48
|
}
|
|
48
49
|
return response;
|
|
49
50
|
};
|
|
@@ -61,7 +62,7 @@ export function generateInterceptorJs(
|
|
|
61
62
|
this.addEventListener('load', function() {
|
|
62
63
|
try {
|
|
63
64
|
window.${arr}.push(JSON.parse(this.responseText));
|
|
64
|
-
} catch(e) {}
|
|
65
|
+
} catch(e) { window.${arr}_errors.push({ url: this.__opencli_url, error: String(e) }); }
|
|
65
66
|
});
|
|
66
67
|
}
|
|
67
68
|
return __origSend.apply(this, arguments);
|
package/src/output.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface RenderOptions {
|
|
|
12
12
|
title?: string;
|
|
13
13
|
elapsed?: number;
|
|
14
14
|
source?: string;
|
|
15
|
+
footerExtra?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function render(data: unknown, opts: RenderOptions = {}): void {
|
|
@@ -56,6 +57,7 @@ function renderTable(data: unknown, opts: RenderOptions): void {
|
|
|
56
57
|
footer.push(`${rows.length} items`);
|
|
57
58
|
if (opts.elapsed) footer.push(`${opts.elapsed.toFixed(1)}s`);
|
|
58
59
|
if (opts.source) footer.push(opts.source);
|
|
60
|
+
if (opts.footerExtra) footer.push(opts.footerExtra);
|
|
59
61
|
console.log(chalk.dim(footer.join(' · ')));
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -82,7 +84,7 @@ function renderCsv(data: unknown, opts: RenderOptions): void {
|
|
|
82
84
|
for (const row of rows) {
|
|
83
85
|
console.log(columns.map(c => {
|
|
84
86
|
const v = String((row as Record<string, unknown>)[c] ?? '');
|
|
85
|
-
return v.includes(',') || v.includes('"') || v.includes('\n')
|
|
87
|
+
return v.includes(',') || v.includes('"') || v.includes('\n') || v.includes('\r')
|
|
86
88
|
? `"${v.replace(/"/g, '""')}"` : v;
|
|
87
89
|
}).join(','));
|
|
88
90
|
}
|
|
@@ -11,6 +11,7 @@ function createMockPage(overrides: Partial<IPage> = {}): IPage {
|
|
|
11
11
|
return {
|
|
12
12
|
goto: vi.fn(),
|
|
13
13
|
evaluate: vi.fn().mockResolvedValue(null),
|
|
14
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
14
15
|
snapshot: vi.fn().mockResolvedValue(''),
|
|
15
16
|
click: vi.fn(),
|
|
16
17
|
typeText: vi.fn(),
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
generateFilename,
|
|
24
24
|
exportCookiesToNetscape,
|
|
25
25
|
getTempDir,
|
|
26
|
+
formatCookieHeader,
|
|
26
27
|
} from '../../download/index.js';
|
|
27
28
|
import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
|
|
28
29
|
|
|
@@ -62,9 +63,8 @@ async function mapConcurrent<T, R>(
|
|
|
62
63
|
*/
|
|
63
64
|
async function extractBrowserCookies(page: IPage, domain?: string): Promise<string> {
|
|
64
65
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return typeof cookieString === 'string' ? cookieString : '';
|
|
66
|
+
const cookies = await page.getCookies(domain ? { domain } : {});
|
|
67
|
+
return formatCookieHeader(cookies);
|
|
68
68
|
} catch {
|
|
69
69
|
return '';
|
|
70
70
|
}
|
|
@@ -78,20 +78,17 @@ async function extractCookiesArray(
|
|
|
78
78
|
domain: string,
|
|
79
79
|
): Promise<Array<{ name: string; value: string; domain: string; path: string; secure: boolean; httpOnly: boolean }>> {
|
|
80
80
|
try {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
httpOnly: false,
|
|
93
|
-
};
|
|
94
|
-
}).filter((c) => c.name);
|
|
81
|
+
const cookies = await page.getCookies({ domain });
|
|
82
|
+
return cookies
|
|
83
|
+
.filter((cookie) => cookie.name)
|
|
84
|
+
.map((cookie) => ({
|
|
85
|
+
name: cookie.name,
|
|
86
|
+
value: cookie.value,
|
|
87
|
+
domain: cookie.domain,
|
|
88
|
+
path: cookie.path ?? '/',
|
|
89
|
+
secure: cookie.secure ?? false,
|
|
90
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
91
|
+
}));
|
|
95
92
|
} catch {
|
|
96
93
|
return [];
|
|
97
94
|
}
|
package/src/registry.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface CliCommand {
|
|
|
35
35
|
pipeline?: Record<string, unknown>[];
|
|
36
36
|
timeoutSeconds?: number;
|
|
37
37
|
source?: string;
|
|
38
|
+
footerExtra?: (kwargs: Record<string, any>) => string | undefined;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/** Internal extension for lazy-loaded TS modules (not exposed in public API) */
|
|
@@ -51,18 +52,21 @@ export interface CliOptions extends Partial<Omit<CliCommand, 'args' | 'descripti
|
|
|
51
52
|
const _registry = new Map<string, CliCommand>();
|
|
52
53
|
|
|
53
54
|
export function cli(opts: CliOptions): CliCommand {
|
|
55
|
+
const strategy = opts.strategy ?? (opts.browser === false ? Strategy.PUBLIC : Strategy.COOKIE);
|
|
56
|
+
const browser = opts.browser ?? (strategy !== Strategy.PUBLIC);
|
|
54
57
|
const cmd: CliCommand = {
|
|
55
58
|
site: opts.site,
|
|
56
59
|
name: opts.name,
|
|
57
60
|
description: opts.description ?? '',
|
|
58
61
|
domain: opts.domain,
|
|
59
|
-
strategy
|
|
60
|
-
browser
|
|
62
|
+
strategy,
|
|
63
|
+
browser,
|
|
61
64
|
args: opts.args ?? [],
|
|
62
65
|
columns: opts.columns,
|
|
63
66
|
func: opts.func,
|
|
64
67
|
pipeline: opts.pipeline,
|
|
65
68
|
timeoutSeconds: opts.timeoutSeconds,
|
|
69
|
+
footerExtra: opts.footerExtra,
|
|
66
70
|
};
|
|
67
71
|
|
|
68
72
|
const key = fullName(cmd);
|
package/src/runtime.ts
CHANGED
|
@@ -30,17 +30,18 @@ export function withTimeoutMs<T>(promise: Promise<T>, timeoutMs: number, message
|
|
|
30
30
|
|
|
31
31
|
/** Interface for browser factory (BrowserBridge or test mocks) */
|
|
32
32
|
export interface IBrowserFactory {
|
|
33
|
-
connect(opts?: { timeout?: number }): Promise<IPage>;
|
|
33
|
+
connect(opts?: { timeout?: number; workspace?: string }): Promise<IPage>;
|
|
34
34
|
close(): Promise<void>;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export async function browserSession<T>(
|
|
38
38
|
BrowserFactory: new () => IBrowserFactory,
|
|
39
39
|
fn: (page: IPage) => Promise<T>,
|
|
40
|
+
opts: { workspace?: string } = {},
|
|
40
41
|
): Promise<T> {
|
|
41
42
|
const mcp = new BrowserFactory();
|
|
42
43
|
try {
|
|
43
|
-
const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT });
|
|
44
|
+
const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, workspace: opts.workspace });
|
|
44
45
|
return await fn(page);
|
|
45
46
|
} finally {
|
|
46
47
|
await mcp.close().catch(() => {});
|
package/src/types.ts
CHANGED
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
export interface IPage {
|
|
9
9
|
goto(url: string): Promise<void>;
|
|
10
10
|
evaluate(js: string): Promise<any>;
|
|
11
|
+
getCookies(opts?: { domain?: string; url?: string }): Promise<Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
value: string;
|
|
14
|
+
domain: string;
|
|
15
|
+
path?: string;
|
|
16
|
+
secure?: boolean;
|
|
17
|
+
httpOnly?: boolean;
|
|
18
|
+
expirationDate?: number;
|
|
19
|
+
}>>;
|
|
11
20
|
snapshot(opts?: { interactive?: boolean; compact?: boolean; maxDepth?: number; raw?: boolean }): Promise<any>;
|
|
12
21
|
click(ref: string): Promise<void>;
|
|
13
22
|
typeText(ref: string, text: string): Promise<void>;
|
package/src/verify.ts
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { validateClisWithTarget, renderValidationReport, type ValidationReport } from './validate.js';
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
10
13
|
|
|
11
14
|
export interface VerifyOptions {
|
|
12
15
|
builtinClis: string;
|
|
@@ -18,15 +21,73 @@ export interface VerifyOptions {
|
|
|
18
21
|
export interface VerifyReport {
|
|
19
22
|
ok: boolean;
|
|
20
23
|
validation: ValidationReport;
|
|
21
|
-
smoke: null
|
|
24
|
+
smoke: null | {
|
|
25
|
+
requested: boolean;
|
|
26
|
+
executed: boolean;
|
|
27
|
+
ok: boolean;
|
|
28
|
+
summary: string;
|
|
29
|
+
};
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
export async function verifyClis(opts: VerifyOptions): Promise<VerifyReport> {
|
|
25
33
|
const report = validateClisWithTarget([opts.builtinClis, opts.userClis], opts.target);
|
|
26
|
-
|
|
34
|
+
let smoke: VerifyReport['smoke'] = null;
|
|
35
|
+
if (opts.smoke) {
|
|
36
|
+
smoke = await runSmokeTests(opts.builtinClis);
|
|
37
|
+
}
|
|
38
|
+
return { ok: report.ok && (smoke?.ok ?? true), validation: report, smoke };
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
export function renderVerifyReport(report: VerifyReport): string {
|
|
30
|
-
|
|
42
|
+
const base = renderValidationReport(report.validation);
|
|
43
|
+
if (!report.smoke) return base;
|
|
44
|
+
const status = report.smoke.ok ? 'PASS' : 'FAIL';
|
|
45
|
+
const mode = report.smoke.executed ? 'executed' : 'skipped';
|
|
46
|
+
return `${base}\nSmoke: ${status} (${mode}) — ${report.smoke.summary}`;
|
|
31
47
|
}
|
|
32
48
|
|
|
49
|
+
async function runSmokeTests(builtinClis: string): Promise<NonNullable<VerifyReport['smoke']>> {
|
|
50
|
+
const projectRoot = path.resolve(builtinClis, '..', '..');
|
|
51
|
+
const smokeDir = path.join(projectRoot, 'tests', 'smoke');
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(smokeDir)) {
|
|
54
|
+
return {
|
|
55
|
+
requested: true,
|
|
56
|
+
executed: false,
|
|
57
|
+
ok: false,
|
|
58
|
+
summary: 'Smoke tests are unavailable in this package/environment.',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const child = spawn(npx, ['vitest', 'run', 'tests/smoke/', '--reporter=dot'], {
|
|
65
|
+
cwd: projectRoot,
|
|
66
|
+
env: { ...process.env },
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let stderr = '';
|
|
71
|
+
child.stderr.on('data', (chunk) => {
|
|
72
|
+
stderr += chunk.toString();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
child.on('error', (error) => {
|
|
76
|
+
resolve({
|
|
77
|
+
requested: true,
|
|
78
|
+
executed: false,
|
|
79
|
+
ok: false,
|
|
80
|
+
summary: `Failed to start smoke tests: ${error.message}`,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
child.on('close', (code) => {
|
|
85
|
+
resolve({
|
|
86
|
+
requested: true,
|
|
87
|
+
executed: true,
|
|
88
|
+
ok: code === 0,
|
|
89
|
+
summary: code === 0 ? 'tests/smoke passed' : (stderr.trim() || `vitest exited with code ${code}`),
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|