@jackwener/opencli 1.0.3 → 1.0.4

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.
Files changed (190) hide show
  1. package/.github/workflows/build-extension.yml +21 -3
  2. package/.github/workflows/docs.yml +52 -0
  3. package/README.md +28 -28
  4. package/README.zh-CN.md +28 -28
  5. package/dist/browser/cdp.d.ts +16 -1
  6. package/dist/browser/cdp.js +124 -80
  7. package/dist/browser/daemon-client.d.ts +3 -1
  8. package/dist/browser/daemon-client.js +4 -0
  9. package/dist/browser/dom-helpers.d.ts +20 -0
  10. package/dist/browser/dom-helpers.js +109 -0
  11. package/dist/browser/mcp.d.ts +1 -0
  12. package/dist/browser/mcp.js +10 -5
  13. package/dist/browser/page.d.ts +7 -0
  14. package/dist/browser/page.js +37 -100
  15. package/dist/browser.test.js +7 -0
  16. package/dist/build-manifest.js +3 -1
  17. package/dist/build-manifest.test.js +34 -0
  18. package/dist/capabilityRouting.d.ts +2 -0
  19. package/dist/capabilityRouting.js +30 -0
  20. package/dist/capabilityRouting.test.d.ts +1 -0
  21. package/dist/capabilityRouting.test.js +42 -0
  22. package/dist/chaoxing.test.js +11 -4
  23. package/dist/cli-manifest.json +635 -1
  24. package/dist/cli.js +45 -8
  25. package/dist/clis/antigravity/serve.d.ts +14 -0
  26. package/dist/clis/antigravity/serve.js +263 -0
  27. package/dist/clis/bilibili/download.js +4 -14
  28. package/dist/clis/boss/resume.d.ts +1 -0
  29. package/dist/clis/boss/resume.js +249 -0
  30. package/dist/clis/hf/top.d.ts +1 -0
  31. package/dist/clis/hf/top.js +119 -0
  32. package/dist/clis/jike/comment.d.ts +1 -0
  33. package/dist/clis/jike/comment.js +107 -0
  34. package/dist/clis/jike/create.d.ts +1 -0
  35. package/dist/clis/jike/create.js +106 -0
  36. package/dist/clis/jike/feed.d.ts +1 -0
  37. package/dist/clis/jike/feed.js +67 -0
  38. package/dist/clis/jike/like.d.ts +1 -0
  39. package/dist/clis/jike/like.js +61 -0
  40. package/dist/clis/jike/notifications.d.ts +1 -0
  41. package/dist/clis/jike/notifications.js +169 -0
  42. package/dist/clis/jike/post.yaml +58 -0
  43. package/dist/clis/jike/repost.d.ts +1 -0
  44. package/dist/clis/jike/repost.js +103 -0
  45. package/dist/clis/jike/search.d.ts +1 -0
  46. package/dist/clis/jike/search.js +67 -0
  47. package/dist/clis/jike/shared.d.ts +19 -0
  48. package/dist/clis/jike/shared.js +25 -0
  49. package/dist/clis/jike/topic.yaml +52 -0
  50. package/dist/clis/jike/user.yaml +51 -0
  51. package/dist/clis/smzdm/search.js +28 -39
  52. package/dist/clis/stackoverflow/bounties.yaml +29 -0
  53. package/dist/clis/stackoverflow/hot.yaml +28 -0
  54. package/dist/clis/stackoverflow/search.yaml +32 -0
  55. package/dist/clis/stackoverflow/unanswered.yaml +28 -0
  56. package/dist/clis/twitter/download.js +6 -16
  57. package/dist/clis/xiaohongshu/download.js +3 -3
  58. package/dist/clis/zhihu/download.js +3 -3
  59. package/dist/doctor.d.ts +7 -0
  60. package/dist/doctor.js +16 -0
  61. package/dist/download/index.d.ts +12 -8
  62. package/dist/download/index.js +11 -3
  63. package/dist/download/index.test.d.ts +1 -0
  64. package/dist/download/index.test.js +14 -0
  65. package/dist/engine.js +5 -5
  66. package/dist/explore.d.ts +1 -0
  67. package/dist/explore.js +3 -3
  68. package/dist/generate.js +1 -0
  69. package/dist/interceptor.js +3 -2
  70. package/dist/output.d.ts +1 -0
  71. package/dist/output.js +3 -1
  72. package/dist/pipeline/executor.test.js +1 -0
  73. package/dist/pipeline/steps/download.js +14 -18
  74. package/dist/registry.d.ts +1 -0
  75. package/dist/registry.js +5 -2
  76. package/dist/runtime.d.ts +4 -1
  77. package/dist/runtime.js +2 -2
  78. package/dist/types.d.ts +12 -0
  79. package/dist/verify.d.ts +6 -1
  80. package/dist/verify.js +54 -2
  81. package/docs/.vitepress/config.mts +193 -0
  82. package/docs/adapters/browser/apple-podcasts.md +28 -0
  83. package/docs/adapters/browser/bbc.md +26 -0
  84. package/docs/adapters/browser/bilibili.md +38 -0
  85. package/docs/adapters/browser/boss.md +28 -0
  86. package/docs/adapters/browser/coupang.md +28 -0
  87. package/docs/adapters/browser/ctrip.md +27 -0
  88. package/docs/adapters/browser/github.md +26 -0
  89. package/docs/adapters/browser/hackernews.md +26 -0
  90. package/docs/adapters/browser/linkedin.md +27 -0
  91. package/docs/adapters/browser/reddit.md +41 -0
  92. package/docs/adapters/browser/reuters.md +27 -0
  93. package/docs/adapters/browser/smzdm.md +27 -0
  94. package/docs/adapters/browser/twitter.md +47 -0
  95. package/docs/adapters/browser/v2ex.md +32 -0
  96. package/docs/adapters/browser/weibo.md +27 -0
  97. package/docs/adapters/browser/xiaohongshu.md +32 -0
  98. package/docs/adapters/browser/xiaoyuzhou.md +28 -0
  99. package/docs/adapters/browser/xueqiu.md +32 -0
  100. package/docs/adapters/browser/yahoo-finance.md +26 -0
  101. package/docs/adapters/browser/youtube.md +29 -0
  102. package/docs/adapters/browser/zhihu.md +30 -0
  103. package/docs/adapters/desktop/antigravity.md +46 -0
  104. package/docs/adapters/desktop/chatgpt.md +43 -0
  105. package/docs/adapters/desktop/chatwise.md +38 -0
  106. package/docs/adapters/desktop/codex.md +32 -0
  107. package/docs/adapters/desktop/cursor.md +33 -0
  108. package/docs/adapters/desktop/discord.md +28 -0
  109. package/docs/adapters/desktop/feishu.md +20 -0
  110. package/docs/adapters/desktop/neteasemusic.md +31 -0
  111. package/docs/adapters/desktop/notion.md +29 -0
  112. package/docs/adapters/desktop/wechat.md +28 -0
  113. package/docs/adapters/index.md +49 -0
  114. package/docs/advanced/cdp.md +103 -0
  115. package/docs/advanced/download.md +63 -0
  116. package/docs/advanced/electron.md +125 -0
  117. package/docs/advanced/remote-chrome.md +72 -0
  118. package/docs/developer/ai-workflow.md +66 -0
  119. package/docs/developer/architecture.md +90 -0
  120. package/docs/developer/contributing.md +136 -0
  121. package/docs/developer/testing.md +237 -0
  122. package/docs/developer/ts-adapter.md +87 -0
  123. package/docs/developer/yaml-adapter.md +108 -0
  124. package/docs/guide/browser-bridge.md +38 -0
  125. package/docs/guide/getting-started.md +56 -0
  126. package/docs/guide/installation.md +37 -0
  127. package/docs/guide/troubleshooting.md +56 -0
  128. package/docs/index.md +35 -0
  129. package/docs/zh/adapters/index.md +5 -0
  130. package/docs/zh/advanced/cdp.md +3 -0
  131. package/docs/zh/developer/contributing.md +24 -0
  132. package/docs/zh/guide/browser-bridge.md +25 -0
  133. package/docs/zh/guide/getting-started.md +40 -0
  134. package/docs/zh/guide/installation.md +37 -0
  135. package/docs/zh/index.md +29 -0
  136. package/extension/dist/background.js +92 -52
  137. package/extension/package-lock.json +1156 -0
  138. package/extension/src/background.test.ts +151 -0
  139. package/extension/src/background.ts +122 -51
  140. package/extension/src/protocol.ts +3 -1
  141. package/package.json +7 -3
  142. package/src/browser/cdp.ts +154 -82
  143. package/src/browser/daemon-client.ts +7 -1
  144. package/src/browser/dom-helpers.ts +116 -0
  145. package/src/browser/mcp.ts +14 -6
  146. package/src/browser/page.ts +45 -100
  147. package/src/browser.test.ts +10 -0
  148. package/src/build-manifest.test.ts +36 -0
  149. package/src/build-manifest.ts +2 -1
  150. package/src/capabilityRouting.test.ts +47 -0
  151. package/src/capabilityRouting.ts +28 -0
  152. package/src/chaoxing.test.ts +12 -4
  153. package/src/cli.ts +28 -8
  154. package/src/clis/antigravity/serve.ts +329 -0
  155. package/src/clis/bilibili/download.ts +4 -15
  156. package/src/clis/boss/resume.ts +262 -0
  157. package/src/clis/hf/top.ts +141 -0
  158. package/src/clis/jike/comment.ts +113 -0
  159. package/src/clis/jike/create.ts +113 -0
  160. package/src/clis/jike/feed.ts +74 -0
  161. package/src/clis/jike/like.ts +65 -0
  162. package/src/clis/jike/notifications.ts +185 -0
  163. package/src/clis/jike/post.yaml +58 -0
  164. package/src/clis/jike/repost.ts +114 -0
  165. package/src/clis/jike/search.ts +74 -0
  166. package/src/clis/jike/shared.ts +36 -0
  167. package/src/clis/jike/topic.yaml +52 -0
  168. package/src/clis/jike/user.yaml +51 -0
  169. package/src/clis/smzdm/search.ts +30 -39
  170. package/src/clis/stackoverflow/bounties.yaml +29 -0
  171. package/src/clis/stackoverflow/hot.yaml +28 -0
  172. package/src/clis/stackoverflow/search.yaml +32 -0
  173. package/src/clis/stackoverflow/unanswered.yaml +28 -0
  174. package/src/clis/twitter/download.ts +6 -17
  175. package/src/clis/xiaohongshu/download.ts +3 -3
  176. package/src/clis/zhihu/download.ts +3 -3
  177. package/src/doctor.ts +18 -2
  178. package/src/download/index.test.ts +16 -0
  179. package/src/download/index.ts +22 -4
  180. package/src/engine.ts +4 -4
  181. package/src/explore.ts +4 -4
  182. package/src/generate.ts +1 -0
  183. package/src/interceptor.ts +3 -2
  184. package/src/output.ts +3 -1
  185. package/src/pipeline/executor.test.ts +1 -0
  186. package/src/pipeline/steps/download.ts +14 -17
  187. package/src/registry.ts +6 -2
  188. package/src/runtime.ts +3 -2
  189. package/src/types.ts +9 -0
  190. 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.evaluate(`(() => document.cookie)()`);
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: typeof cookies === 'string' ? 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 { browserSession } from './runtime.js';
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
+ });
@@ -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: Array<{ name: string; value: string; domain: string; path?: string; secure?: boolean; httpOnly?: boolean }>,
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 sites = await fs.promises.readdir(dir);
102
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
103
103
 
104
- for (const site of sites) {
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
- for (const ep of jsonEndpoints.slice(0, 5)) {
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
@@ -65,6 +65,7 @@ export async function generateCliFromUrl(opts: any): Promise<any> {
65
65
  site: opts.site,
66
66
  goal: normalizeGoal(opts.goal) ?? opts.goal,
67
67
  waitSeconds: opts.waitSeconds ?? 3,
68
+ workspace: opts.workspace,
68
69
  });
69
70
 
70
71
  // Step 2: Synthesize candidates
@@ -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
- // Use browser evaluate to get document.cookie
66
- const cookieString = await page.evaluate(`(() => document.cookie)()`);
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 cookieString = await extractBrowserCookies(page);
82
- if (!cookieString) return [];
83
-
84
- return cookieString.split(';').map((c) => {
85
- const [name, ...rest] = c.trim().split('=');
86
- return {
87
- name: name || '',
88
- value: rest.join('=') || '',
89
- domain,
90
- path: '/',
91
- secure: true,
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: opts.strategy ?? (opts.browser === false ? Strategy.PUBLIC : Strategy.COOKIE),
60
- browser: opts.browser ?? (opts.strategy === Strategy.PUBLIC ? false : true),
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
- return { ok: report.ok, validation: report, smoke: null };
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
- return renderValidationReport(report.validation);
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
+ }