@jackwener/opencli 0.9.5 → 0.9.8
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/ISSUE_TEMPLATE/bug_report.yml +83 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
- package/.github/ISSUE_TEMPLATE/new_site_adapter.yml +57 -0
- package/.github/dependabot.yml +27 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/workflows/ci.yml +14 -8
- package/.github/workflows/e2e-headed.yml +6 -2
- package/.github/workflows/pkg-pr-new.yml +2 -2
- package/.github/workflows/release-please.yml +25 -0
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/security.yml +36 -0
- package/CLI-ELECTRON.md +89 -36
- package/CONTRIBUTING.md +167 -0
- package/README.md +98 -32
- package/README.zh-CN.md +99 -33
- package/dist/browser/discover.js +22 -7
- package/dist/browser.test.js +23 -0
- package/dist/build-manifest.d.ts +26 -0
- package/dist/build-manifest.js +132 -60
- package/dist/build-manifest.test.d.ts +1 -0
- package/dist/build-manifest.test.js +26 -0
- package/dist/cli-manifest.json +1875 -271
- package/dist/clis/antigravity/model.js +2 -2
- package/dist/clis/antigravity/send.js +2 -2
- package/dist/clis/bilibili/download.d.ts +10 -0
- package/dist/clis/bilibili/download.js +135 -0
- package/dist/clis/chatgpt/ask.d.ts +1 -0
- package/dist/clis/chatgpt/ask.js +68 -0
- package/dist/clis/chatgpt/send.js +11 -0
- package/dist/clis/chatwise/ask.d.ts +1 -0
- package/dist/clis/chatwise/ask.js +76 -0
- package/dist/clis/chatwise/export.d.ts +1 -0
- package/dist/clis/chatwise/export.js +46 -0
- package/dist/clis/chatwise/history.d.ts +1 -0
- package/dist/clis/chatwise/history.js +43 -0
- package/dist/clis/chatwise/model.d.ts +1 -0
- package/dist/clis/chatwise/model.js +81 -0
- package/dist/clis/chatwise/new.d.ts +1 -0
- package/dist/clis/chatwise/new.js +18 -0
- package/dist/clis/chatwise/read.d.ts +1 -0
- package/dist/clis/chatwise/read.js +39 -0
- package/dist/clis/chatwise/screenshot.d.ts +1 -0
- package/dist/clis/chatwise/screenshot.js +27 -0
- package/dist/clis/chatwise/send.d.ts +1 -0
- package/dist/clis/chatwise/send.js +45 -0
- package/dist/clis/chatwise/status.d.ts +1 -0
- package/dist/clis/chatwise/status.js +22 -0
- package/dist/clis/codex/ask.d.ts +1 -0
- package/dist/clis/codex/ask.js +67 -0
- package/dist/clis/codex/export.d.ts +1 -0
- package/dist/clis/codex/export.js +37 -0
- package/dist/clis/codex/history.d.ts +1 -0
- package/dist/clis/codex/history.js +43 -0
- package/dist/clis/codex/read.js +3 -5
- package/dist/clis/codex/screenshot.d.ts +1 -0
- package/dist/clis/codex/screenshot.js +27 -0
- package/dist/clis/codex/send.js +3 -6
- package/dist/clis/codex/status.js +2 -1
- package/dist/clis/cursor/ask.d.ts +1 -0
- package/dist/clis/cursor/ask.js +69 -0
- package/dist/clis/cursor/composer.js +9 -28
- package/dist/clis/cursor/export.d.ts +1 -0
- package/dist/clis/cursor/export.js +51 -0
- package/dist/clis/cursor/history.d.ts +1 -0
- package/dist/clis/cursor/history.js +43 -0
- package/dist/clis/cursor/new.js +4 -13
- package/dist/clis/cursor/screenshot.d.ts +1 -0
- package/dist/clis/cursor/screenshot.js +31 -0
- package/dist/clis/discord-app/channels.d.ts +1 -0
- package/dist/clis/discord-app/channels.js +45 -0
- package/dist/clis/discord-app/members.d.ts +1 -0
- package/dist/clis/discord-app/members.js +38 -0
- package/dist/clis/discord-app/read.d.ts +1 -0
- package/dist/clis/discord-app/read.js +45 -0
- package/dist/clis/discord-app/search.d.ts +1 -0
- package/dist/clis/discord-app/search.js +56 -0
- package/dist/clis/discord-app/send.d.ts +1 -0
- package/dist/clis/discord-app/send.js +27 -0
- package/dist/clis/discord-app/servers.d.ts +1 -0
- package/dist/clis/discord-app/servers.js +36 -0
- package/dist/clis/discord-app/status.d.ts +1 -0
- package/dist/clis/discord-app/status.js +16 -0
- package/dist/clis/feishu/new.d.ts +1 -0
- package/dist/clis/feishu/new.js +27 -0
- package/dist/clis/feishu/read.d.ts +1 -0
- package/dist/clis/feishu/read.js +40 -0
- package/dist/clis/feishu/search.d.ts +1 -0
- package/dist/clis/feishu/search.js +30 -0
- package/dist/clis/feishu/send.d.ts +1 -0
- package/dist/clis/feishu/send.js +39 -0
- package/dist/clis/feishu/status.d.ts +1 -0
- package/dist/clis/feishu/status.js +28 -0
- package/dist/clis/grok/ask.d.ts +1 -0
- package/dist/clis/grok/ask.js +82 -0
- package/dist/clis/grok/debug.d.ts +1 -0
- package/dist/clis/grok/debug.js +45 -0
- package/dist/clis/jimeng/generate.yaml +84 -0
- package/dist/clis/jimeng/history.yaml +47 -0
- package/dist/clis/linux-do/categories.yaml +41 -0
- package/dist/clis/linux-do/category.yaml +49 -0
- package/dist/clis/linux-do/hot.yaml +50 -0
- package/dist/clis/linux-do/latest.yaml +40 -0
- package/dist/clis/linux-do/search.yaml +45 -0
- package/dist/clis/linux-do/topic.yaml +38 -0
- package/dist/clis/notion/export.d.ts +1 -0
- package/dist/clis/notion/export.js +31 -0
- package/dist/clis/notion/favorites.d.ts +1 -0
- package/dist/clis/notion/favorites.js +84 -0
- package/dist/clis/notion/new.d.ts +1 -0
- package/dist/clis/notion/new.js +34 -0
- package/dist/clis/notion/read.d.ts +1 -0
- package/dist/clis/notion/read.js +30 -0
- package/dist/clis/notion/search.d.ts +1 -0
- package/dist/clis/notion/search.js +46 -0
- package/dist/clis/notion/sidebar.d.ts +1 -0
- package/dist/clis/notion/sidebar.js +41 -0
- package/dist/clis/notion/status.d.ts +1 -0
- package/dist/clis/notion/status.js +16 -0
- package/dist/clis/notion/write.d.ts +1 -0
- package/dist/clis/notion/write.js +40 -0
- package/dist/clis/twitter/download.d.ts +8 -0
- package/dist/clis/twitter/download.js +204 -0
- package/dist/clis/wechat/chats.d.ts +1 -0
- package/dist/clis/wechat/chats.js +28 -0
- package/dist/clis/wechat/contacts.d.ts +1 -0
- package/dist/clis/wechat/contacts.js +28 -0
- package/dist/clis/wechat/read.d.ts +1 -0
- package/dist/clis/wechat/read.js +58 -0
- package/dist/clis/wechat/search.d.ts +1 -0
- package/dist/clis/wechat/search.js +31 -0
- package/dist/clis/wechat/send.d.ts +1 -0
- package/dist/clis/wechat/send.js +42 -0
- package/dist/clis/wechat/status.d.ts +1 -0
- package/dist/clis/wechat/status.js +29 -0
- package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
- package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
- package/dist/clis/xiaohongshu/creator-notes.js +109 -0
- package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-profile.js +54 -0
- package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-stats.js +74 -0
- package/dist/clis/xiaohongshu/download.d.ts +7 -0
- package/dist/clis/xiaohongshu/download.js +155 -0
- package/dist/clis/xiaohongshu/search.js +1 -1
- package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
- package/dist/clis/xiaohongshu/user-helpers.js +67 -0
- package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
- package/dist/clis/xiaohongshu/user.js +46 -29
- package/dist/clis/zhihu/download.d.ts +11 -0
- package/dist/clis/zhihu/download.js +186 -0
- package/dist/clis/zhihu/download.test.d.ts +1 -0
- package/dist/clis/zhihu/download.test.js +10 -0
- package/dist/download/index.d.ts +79 -0
- package/dist/download/index.js +325 -0
- package/dist/download/progress.d.ts +36 -0
- package/dist/download/progress.js +111 -0
- package/dist/engine.test.js +15 -0
- package/dist/main.js +16 -3
- package/dist/pipeline/registry.js +2 -0
- package/dist/pipeline/steps/download.d.ts +34 -0
- package/dist/pipeline/steps/download.js +251 -0
- package/dist/pipeline/template.js +28 -0
- package/package.json +4 -3
- package/scripts/test-site.mjs +70 -0
- package/src/browser/discover.ts +23 -7
- package/src/browser.test.ts +23 -0
- package/src/build-manifest.test.ts +28 -0
- package/src/build-manifest.ts +147 -57
- package/src/clis/antigravity/README.md +2 -3
- package/src/clis/antigravity/README.zh-CN.md +2 -3
- package/src/clis/antigravity/SKILL.md +1 -1
- package/src/clis/antigravity/model.ts +2 -2
- package/src/clis/antigravity/send.ts +2 -2
- package/src/clis/bilibili/download.ts +161 -0
- package/src/clis/chatgpt/README.md +25 -16
- package/src/clis/chatgpt/README.zh-CN.md +27 -18
- package/src/clis/chatgpt/ask.ts +77 -0
- package/src/clis/chatgpt/send.ts +12 -0
- package/src/clis/chatwise/README.md +38 -0
- package/src/clis/chatwise/README.zh-CN.md +38 -0
- package/src/clis/chatwise/ask.ts +87 -0
- package/src/clis/chatwise/export.ts +51 -0
- package/src/clis/chatwise/history.ts +47 -0
- package/src/clis/chatwise/model.ts +87 -0
- package/src/clis/chatwise/new.ts +21 -0
- package/src/clis/chatwise/read.ts +42 -0
- package/src/clis/chatwise/screenshot.ts +33 -0
- package/src/clis/chatwise/send.ts +50 -0
- package/src/clis/chatwise/status.ts +25 -0
- package/src/clis/codex/ask.ts +77 -0
- package/src/clis/codex/export.ts +42 -0
- package/src/clis/codex/extract-diff.ts +1 -0
- package/src/clis/codex/history.ts +47 -0
- package/src/clis/codex/read.ts +5 -6
- package/src/clis/codex/screenshot.ts +33 -0
- package/src/clis/codex/send.ts +6 -7
- package/src/clis/codex/status.ts +4 -2
- package/src/clis/cursor/ask.ts +81 -0
- package/src/clis/cursor/composer.ts +9 -30
- package/src/clis/cursor/export.ts +57 -0
- package/src/clis/cursor/history.ts +47 -0
- package/src/clis/cursor/new.ts +4 -15
- package/src/clis/cursor/screenshot.ts +38 -0
- package/src/clis/discord-app/README.md +28 -0
- package/src/clis/discord-app/README.zh-CN.md +28 -0
- package/src/clis/discord-app/channels.ts +48 -0
- package/src/clis/discord-app/members.ts +41 -0
- package/src/clis/discord-app/read.ts +49 -0
- package/src/clis/discord-app/search.ts +64 -0
- package/src/clis/discord-app/send.ts +32 -0
- package/src/clis/discord-app/servers.ts +39 -0
- package/src/clis/discord-app/status.ts +18 -0
- package/src/clis/feishu/README.md +20 -0
- package/src/clis/feishu/README.zh-CN.md +20 -0
- package/src/clis/feishu/new.ts +32 -0
- package/src/clis/feishu/read.ts +48 -0
- package/src/clis/feishu/search.ts +35 -0
- package/src/clis/feishu/send.ts +46 -0
- package/src/clis/feishu/status.ts +34 -0
- package/src/clis/grok/ask.ts +90 -0
- package/src/clis/grok/debug.ts +49 -0
- package/src/clis/jimeng/generate.yaml +84 -0
- package/src/clis/jimeng/history.yaml +47 -0
- package/src/clis/linux-do/categories.yaml +41 -0
- package/src/clis/linux-do/category.yaml +49 -0
- package/src/clis/linux-do/hot.yaml +50 -0
- package/src/clis/linux-do/latest.yaml +40 -0
- package/src/clis/linux-do/search.yaml +45 -0
- package/src/clis/linux-do/topic.yaml +38 -0
- package/src/clis/notion/README.md +29 -0
- package/src/clis/notion/README.zh-CN.md +29 -0
- package/src/clis/notion/export.ts +36 -0
- package/src/clis/notion/favorites.ts +87 -0
- package/src/clis/notion/new.ts +39 -0
- package/src/clis/notion/read.ts +33 -0
- package/src/clis/notion/search.ts +54 -0
- package/src/clis/notion/sidebar.ts +44 -0
- package/src/clis/notion/status.ts +18 -0
- package/src/clis/notion/write.ts +45 -0
- package/src/clis/twitter/download.ts +227 -0
- package/src/clis/wechat/README.md +28 -0
- package/src/clis/wechat/README.zh-CN.md +28 -0
- package/src/clis/wechat/chats.ts +33 -0
- package/src/clis/wechat/contacts.ts +33 -0
- package/src/clis/wechat/read.ts +72 -0
- package/src/clis/wechat/search.ts +36 -0
- package/src/clis/wechat/send.ts +49 -0
- package/src/clis/wechat/status.ts +35 -0
- package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
- package/src/clis/xiaohongshu/creator-notes.ts +116 -0
- package/src/clis/xiaohongshu/creator-profile.ts +60 -0
- package/src/clis/xiaohongshu/creator-stats.ts +81 -0
- package/src/clis/xiaohongshu/download.ts +173 -0
- package/src/clis/xiaohongshu/search.ts +1 -1
- package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
- package/src/clis/xiaohongshu/user-helpers.ts +85 -0
- package/src/clis/xiaohongshu/user.ts +52 -32
- package/src/clis/zhihu/download.test.ts +12 -0
- package/src/clis/zhihu/download.ts +223 -0
- package/src/download/index.ts +395 -0
- package/src/download/progress.ts +125 -0
- package/src/engine.test.ts +17 -0
- package/src/main.ts +12 -3
- package/src/pipeline/registry.ts +2 -0
- package/src/pipeline/steps/download.ts +310 -0
- package/src/pipeline/template.ts +26 -0
- package/tests/e2e/browser-auth.test.ts +25 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: download — file download with concurrency and progress.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Direct HTTP downloads (images, documents)
|
|
6
|
+
* - yt-dlp integration for video platforms
|
|
7
|
+
* - Browser cookie forwarding for authenticated downloads
|
|
8
|
+
* - Filename templating and deduplication
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { render } from '../template.js';
|
|
13
|
+
import { httpDownload, ytdlpDownload, saveDocument, detectContentType, requiresYtdlp, sanitizeFilename, generateFilename, exportCookiesToNetscape, getTempDir, } from '../../download/index.js';
|
|
14
|
+
import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
|
|
15
|
+
/**
|
|
16
|
+
* Simple async concurrency limiter for downloads.
|
|
17
|
+
*/
|
|
18
|
+
async function mapConcurrent(items, limit, fn) {
|
|
19
|
+
const results = new Array(items.length);
|
|
20
|
+
let index = 0;
|
|
21
|
+
async function worker() {
|
|
22
|
+
while (index < items.length) {
|
|
23
|
+
const i = index++;
|
|
24
|
+
results[i] = await fn(items[i], i);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
28
|
+
await Promise.all(workers);
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extract cookies from browser page.
|
|
33
|
+
*/
|
|
34
|
+
async function extractBrowserCookies(page, domain) {
|
|
35
|
+
try {
|
|
36
|
+
// Use browser evaluate to get document.cookie
|
|
37
|
+
const cookieString = await page.evaluate(`(() => document.cookie)()`);
|
|
38
|
+
return typeof cookieString === 'string' ? cookieString : '';
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extract cookies as array for yt-dlp Netscape format.
|
|
46
|
+
*/
|
|
47
|
+
async function extractCookiesArray(page, domain) {
|
|
48
|
+
try {
|
|
49
|
+
const cookieString = await extractBrowserCookies(page);
|
|
50
|
+
if (!cookieString)
|
|
51
|
+
return [];
|
|
52
|
+
return cookieString.split(';').map((c) => {
|
|
53
|
+
const [name, ...rest] = c.trim().split('=');
|
|
54
|
+
return {
|
|
55
|
+
name: name || '',
|
|
56
|
+
value: rest.join('=') || '',
|
|
57
|
+
domain,
|
|
58
|
+
path: '/',
|
|
59
|
+
secure: true,
|
|
60
|
+
httpOnly: false,
|
|
61
|
+
};
|
|
62
|
+
}).filter((c) => c.name);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Download step handler for YAML pipelines.
|
|
70
|
+
*
|
|
71
|
+
* Usage in YAML:
|
|
72
|
+
* ```yaml
|
|
73
|
+
* pipeline:
|
|
74
|
+
* - download:
|
|
75
|
+
* url: ${{ item.imageUrl }}
|
|
76
|
+
* dir: ./downloads
|
|
77
|
+
* filename: ${{ item.title }}.jpg
|
|
78
|
+
* concurrency: 5
|
|
79
|
+
* skip_existing: true
|
|
80
|
+
* use_ytdlp: false
|
|
81
|
+
* type: auto
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export async function stepDownload(page, params, data, args) {
|
|
85
|
+
// Parse parameters with defaults
|
|
86
|
+
const urlTemplate = typeof params === 'string' ? params : (params?.url ?? '');
|
|
87
|
+
const dirTemplate = params?.dir ?? './downloads';
|
|
88
|
+
const filenameTemplate = params?.filename ?? '';
|
|
89
|
+
const concurrency = typeof params?.concurrency === 'number' ? params.concurrency : 3;
|
|
90
|
+
const skipExisting = params?.skip_existing !== false;
|
|
91
|
+
const timeout = typeof params?.timeout === 'number' ? params.timeout * 1000 : 30000;
|
|
92
|
+
const useYtdlp = params?.use_ytdlp ?? false;
|
|
93
|
+
const ytdlpArgs = Array.isArray(params?.ytdlp_args) ? params.ytdlp_args : [];
|
|
94
|
+
const contentType = params?.type ?? 'auto';
|
|
95
|
+
const showProgress = params?.progress !== false;
|
|
96
|
+
const contentTemplate = params?.content;
|
|
97
|
+
const metadataTemplate = params?.metadata;
|
|
98
|
+
// Resolve output directory
|
|
99
|
+
const dir = String(render(dirTemplate, { args, data }));
|
|
100
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
101
|
+
// Normalize data to array
|
|
102
|
+
const items = Array.isArray(data) ? data : data ? [data] : [];
|
|
103
|
+
if (items.length === 0) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
// Create progress tracker
|
|
107
|
+
const tracker = new DownloadProgressTracker(items.length, showProgress);
|
|
108
|
+
// Extract cookies if browser is available
|
|
109
|
+
let cookies = '';
|
|
110
|
+
let cookiesFile;
|
|
111
|
+
if (page) {
|
|
112
|
+
cookies = await extractBrowserCookies(page);
|
|
113
|
+
// For yt-dlp, we need to export cookies to Netscape format
|
|
114
|
+
if (useYtdlp || items.some((item, index) => {
|
|
115
|
+
const url = String(render(urlTemplate, { args, data, item, index }));
|
|
116
|
+
return requiresYtdlp(url);
|
|
117
|
+
})) {
|
|
118
|
+
try {
|
|
119
|
+
// Try to get domain from first URL
|
|
120
|
+
const firstUrl = String(render(urlTemplate, { args, data, item: items[0], index: 0 }));
|
|
121
|
+
const domain = new URL(firstUrl).hostname;
|
|
122
|
+
const cookiesArray = await extractCookiesArray(page, domain);
|
|
123
|
+
if (cookiesArray.length > 0) {
|
|
124
|
+
const tempDir = getTempDir();
|
|
125
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
126
|
+
cookiesFile = path.join(tempDir, `cookies_${Date.now()}.txt`);
|
|
127
|
+
exportCookiesToNetscape(cookiesArray, cookiesFile);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Ignore cookie extraction errors
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Process downloads with concurrency
|
|
136
|
+
const results = await mapConcurrent(items, concurrency, async (item, index) => {
|
|
137
|
+
const startTime = Date.now();
|
|
138
|
+
// Render URL
|
|
139
|
+
const url = String(render(urlTemplate, { args, data, item, index }));
|
|
140
|
+
if (!url) {
|
|
141
|
+
tracker.onFileComplete(false);
|
|
142
|
+
return {
|
|
143
|
+
...item,
|
|
144
|
+
_download: { status: 'failed', error: 'Empty URL' },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Render filename
|
|
148
|
+
let filename;
|
|
149
|
+
if (filenameTemplate) {
|
|
150
|
+
filename = String(render(filenameTemplate, { args, data, item, index }));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
filename = generateFilename(url, index);
|
|
154
|
+
}
|
|
155
|
+
filename = sanitizeFilename(filename);
|
|
156
|
+
const destPath = path.join(dir, filename);
|
|
157
|
+
// Check if file exists and skip_existing is true
|
|
158
|
+
if (skipExisting && fs.existsSync(destPath)) {
|
|
159
|
+
tracker.onFileComplete(true, true);
|
|
160
|
+
return {
|
|
161
|
+
...item,
|
|
162
|
+
_download: {
|
|
163
|
+
status: 'skipped',
|
|
164
|
+
path: destPath,
|
|
165
|
+
size: fs.statSync(destPath).size,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// Create progress bar for this file
|
|
170
|
+
const progressBar = tracker.onFileStart(filename, index);
|
|
171
|
+
// Determine download method
|
|
172
|
+
const detectedType = contentType === 'auto' ? detectContentType(url) : contentType;
|
|
173
|
+
const shouldUseYtdlp = useYtdlp || (detectedType === 'video' && requiresYtdlp(url));
|
|
174
|
+
let result;
|
|
175
|
+
try {
|
|
176
|
+
if (detectedType === 'document' && contentTemplate) {
|
|
177
|
+
// Save extracted content as document
|
|
178
|
+
const content = String(render(contentTemplate, { args, data, item, index }));
|
|
179
|
+
const metadata = metadataTemplate
|
|
180
|
+
? Object.fromEntries(Object.entries(metadataTemplate).map(([k, v]) => [k, render(v, { args, data, item, index })]))
|
|
181
|
+
: undefined;
|
|
182
|
+
const ext = path.extname(filename).toLowerCase();
|
|
183
|
+
const format = ext === '.json' ? 'json' : ext === '.html' ? 'html' : 'markdown';
|
|
184
|
+
result = await saveDocument(content, destPath, format, metadata);
|
|
185
|
+
if (progressBar) {
|
|
186
|
+
progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (shouldUseYtdlp) {
|
|
190
|
+
// Use yt-dlp for video downloads
|
|
191
|
+
result = await ytdlpDownload(url, destPath, {
|
|
192
|
+
cookiesFile,
|
|
193
|
+
extraArgs: ytdlpArgs,
|
|
194
|
+
onProgress: (percent) => {
|
|
195
|
+
if (progressBar) {
|
|
196
|
+
progressBar.update(percent, 100);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
if (progressBar) {
|
|
201
|
+
progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// Direct HTTP download
|
|
206
|
+
result = await httpDownload(url, destPath, {
|
|
207
|
+
cookies,
|
|
208
|
+
timeout,
|
|
209
|
+
onProgress: (received, total) => {
|
|
210
|
+
if (progressBar) {
|
|
211
|
+
progressBar.update(received, total);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
if (progressBar) {
|
|
216
|
+
progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
result = { success: false, size: 0, error: err.message };
|
|
222
|
+
if (progressBar) {
|
|
223
|
+
progressBar.fail(err.message);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
tracker.onFileComplete(result.success);
|
|
227
|
+
const duration = Date.now() - startTime;
|
|
228
|
+
return {
|
|
229
|
+
...item,
|
|
230
|
+
_download: {
|
|
231
|
+
status: result.success ? 'success' : 'failed',
|
|
232
|
+
path: result.success ? destPath : undefined,
|
|
233
|
+
size: result.size,
|
|
234
|
+
error: result.error,
|
|
235
|
+
duration,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
// Cleanup temp cookie file
|
|
240
|
+
if (cookiesFile && fs.existsSync(cookiesFile)) {
|
|
241
|
+
try {
|
|
242
|
+
fs.unlinkSync(cookiesFile);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// Ignore cleanup errors
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Show summary
|
|
249
|
+
tracker.finish();
|
|
250
|
+
return results;
|
|
251
|
+
}
|
|
@@ -114,6 +114,34 @@ function applyFilter(filterExpr, value) {
|
|
|
114
114
|
return Array.isArray(value) ? value[value.length - 1] : value;
|
|
115
115
|
case 'json':
|
|
116
116
|
return JSON.stringify(value ?? null);
|
|
117
|
+
case 'slugify':
|
|
118
|
+
// Convert to URL-safe slug
|
|
119
|
+
return typeof value === 'string'
|
|
120
|
+
? value
|
|
121
|
+
.toLowerCase()
|
|
122
|
+
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
|
123
|
+
.replace(/^-|-$/g, '')
|
|
124
|
+
: value;
|
|
125
|
+
case 'sanitize':
|
|
126
|
+
// Remove invalid filename characters
|
|
127
|
+
return typeof value === 'string'
|
|
128
|
+
? value.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_')
|
|
129
|
+
: value;
|
|
130
|
+
case 'ext': {
|
|
131
|
+
// Extract file extension from URL or path
|
|
132
|
+
if (typeof value !== 'string')
|
|
133
|
+
return value;
|
|
134
|
+
const lastDot = value.lastIndexOf('.');
|
|
135
|
+
const lastSlash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
|
|
136
|
+
return lastDot > lastSlash ? value.slice(lastDot) : '';
|
|
137
|
+
}
|
|
138
|
+
case 'basename': {
|
|
139
|
+
// Extract filename from URL or path
|
|
140
|
+
if (typeof value !== 'string')
|
|
141
|
+
return value;
|
|
142
|
+
const parts = value.split(/[/\\]/);
|
|
143
|
+
return parts[parts.length - 1] || value;
|
|
144
|
+
}
|
|
117
145
|
default:
|
|
118
146
|
return value;
|
|
119
147
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackwener/opencli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
7
|
"description": "Make any website your CLI. AI-powered.",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=
|
|
9
|
+
"node": ">=20.0.0"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "dist/main.js",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"lint": "tsc --noEmit",
|
|
26
26
|
"prepublishOnly": "npm run build",
|
|
27
27
|
"test": "vitest run",
|
|
28
|
+
"test:site": "node scripts/test-site.mjs",
|
|
28
29
|
"test:watch": "vitest"
|
|
29
30
|
},
|
|
30
31
|
"keywords": [
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"chalk": "^5.3.0",
|
|
45
46
|
"cli-table3": "^0.6.5",
|
|
46
|
-
"commander": "^
|
|
47
|
+
"commander": "^14.0.3",
|
|
47
48
|
"js-yaml": "^4.1.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const site = process.argv[2]?.trim();
|
|
8
|
+
|
|
9
|
+
if (!site) {
|
|
10
|
+
console.error('Usage: npm run test:site -- <site>');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const repoRoot = path.resolve(new URL('..', import.meta.url).pathname);
|
|
15
|
+
const srcDir = path.join(repoRoot, 'src');
|
|
16
|
+
|
|
17
|
+
function runStep(label, command, args) {
|
|
18
|
+
console.log(`\n==> ${label}`);
|
|
19
|
+
const result = spawnSync(command, args, {
|
|
20
|
+
cwd: repoRoot,
|
|
21
|
+
stdio: 'inherit',
|
|
22
|
+
env: process.env,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (result.status !== 0) {
|
|
26
|
+
process.exit(result.status ?? 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function walk(dir) {
|
|
31
|
+
const files = [];
|
|
32
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
33
|
+
const fullPath = path.join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
files.push(...walk(fullPath));
|
|
36
|
+
} else {
|
|
37
|
+
files.push(fullPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toPosix(filePath) {
|
|
44
|
+
return filePath.split(path.sep).join('/');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findSiteTests() {
|
|
48
|
+
return walk(srcDir)
|
|
49
|
+
.filter(filePath => filePath.endsWith('.test.ts'))
|
|
50
|
+
.filter(filePath => {
|
|
51
|
+
const normalized = toPosix(path.relative(repoRoot, filePath));
|
|
52
|
+
return normalized.includes(`/clis/${site}/`) || normalized.includes(`/${site}.test.ts`);
|
|
53
|
+
})
|
|
54
|
+
.sort();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
runStep('Typecheck', 'npm', ['run', 'typecheck']);
|
|
58
|
+
runStep('Targeted verify', 'npx', ['tsx', 'src/main.ts', 'verify', site]);
|
|
59
|
+
|
|
60
|
+
const testFiles = findSiteTests();
|
|
61
|
+
if (testFiles.length === 0) {
|
|
62
|
+
console.log(`\nNo site-specific vitest files found for "${site}". Skipping full vitest run.`);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
runStep(
|
|
67
|
+
`Site tests (${site})`,
|
|
68
|
+
'npx',
|
|
69
|
+
['vitest', 'run', ...testFiles.map(filePath => path.relative(repoRoot, filePath))],
|
|
70
|
+
);
|
package/src/browser/discover.ts
CHANGED
|
@@ -12,6 +12,19 @@ let _cachedMcpServerPath: string | null | undefined;
|
|
|
12
12
|
let _existsSync = fs.existsSync;
|
|
13
13
|
let _execSync = execSync;
|
|
14
14
|
|
|
15
|
+
function isSupportedMcpEntrypoint(candidate: string): boolean {
|
|
16
|
+
const normalized = candidate.replace(/\\/g, '/').toLowerCase();
|
|
17
|
+
return normalized.endsWith('/@playwright/mcp/cli.js') ||
|
|
18
|
+
normalized.endsWith('/mcp-server-playwright') ||
|
|
19
|
+
normalized.endsWith('/mcp-server-playwright.js');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveSupportedMcpPath(candidate: string | null | undefined): string | null {
|
|
23
|
+
const trimmed = candidate?.trim();
|
|
24
|
+
if (!trimmed || !_existsSync(trimmed)) return null;
|
|
25
|
+
return isSupportedMcpEntrypoint(trimmed) ? trimmed : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
export function resetMcpServerPathCache(): void {
|
|
16
29
|
_cachedMcpServerPath = undefined;
|
|
17
30
|
}
|
|
@@ -80,8 +93,9 @@ export function findMcpServerPath(): string | null {
|
|
|
80
93
|
// Try npx resolution (legacy package name)
|
|
81
94
|
try {
|
|
82
95
|
const result = _execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
const resolved = resolveSupportedMcpPath(result);
|
|
97
|
+
if (resolved) {
|
|
98
|
+
_cachedMcpServerPath = resolved;
|
|
85
99
|
return _cachedMcpServerPath;
|
|
86
100
|
}
|
|
87
101
|
} catch {}
|
|
@@ -89,8 +103,9 @@ export function findMcpServerPath(): string | null {
|
|
|
89
103
|
// Try which
|
|
90
104
|
try {
|
|
91
105
|
const result = _execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
const resolved = resolveSupportedMcpPath(result);
|
|
107
|
+
if (resolved) {
|
|
108
|
+
_cachedMcpServerPath = resolved;
|
|
94
109
|
return _cachedMcpServerPath;
|
|
95
110
|
}
|
|
96
111
|
} catch {}
|
|
@@ -99,9 +114,10 @@ export function findMcpServerPath(): string | null {
|
|
|
99
114
|
for (const base of candidates) {
|
|
100
115
|
if (!_existsSync(base)) continue;
|
|
101
116
|
try {
|
|
102
|
-
const found = _execSync(`find "${base}" -
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
const found = _execSync(`find "${base}" -type f -path "*/@playwright/mcp/cli.js" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
118
|
+
const resolved = resolveSupportedMcpPath(found);
|
|
119
|
+
if (resolved) {
|
|
120
|
+
_cachedMcpServerPath = resolved;
|
|
105
121
|
return _cachedMcpServerPath;
|
|
106
122
|
}
|
|
107
123
|
} catch {}
|
package/src/browser.test.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
2
4
|
import { PlaywrightMCP, __test__ } from './browser/index.js';
|
|
3
5
|
|
|
4
6
|
afterEach(() => {
|
|
@@ -248,6 +250,27 @@ describe('browser helpers', () => {
|
|
|
248
250
|
});
|
|
249
251
|
}
|
|
250
252
|
});
|
|
253
|
+
|
|
254
|
+
it('ignores non-server playwright cli paths discovered from fallback scans', () => {
|
|
255
|
+
const wrongCli = '/root/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/lib/mcp/terminal/cli.js';
|
|
256
|
+
const npxCacheBase = path.join(os.homedir(), '.npm', '_npx');
|
|
257
|
+
|
|
258
|
+
const existsSync = vi.fn((candidate: any) => {
|
|
259
|
+
const value = String(candidate);
|
|
260
|
+
return value === npxCacheBase || value === wrongCli;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const execSync = vi.fn((command: string) => {
|
|
264
|
+
if (String(command).includes('npm root -g')) return '/missing/global/node_modules\n' as any;
|
|
265
|
+
if (String(command).includes('--package=@playwright/mcp which mcp-server-playwright')) return `${wrongCli}\n` as any;
|
|
266
|
+
if (String(command).includes('which mcp-server-playwright')) return '' as any;
|
|
267
|
+
if (String(command).includes(`find "${npxCacheBase}"`)) return `${wrongCli}\n` as any;
|
|
268
|
+
throw new Error(`unexpected command: ${String(command)}`);
|
|
269
|
+
});
|
|
270
|
+
__test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync as any });
|
|
271
|
+
|
|
272
|
+
expect(__test__.findMcpServerPath()).toBeNull();
|
|
273
|
+
});
|
|
251
274
|
});
|
|
252
275
|
|
|
253
276
|
describe('PlaywrightMCP state', () => {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseTsArgsBlock } from './build-manifest.js';
|
|
3
|
+
|
|
4
|
+
describe('parseTsArgsBlock', () => {
|
|
5
|
+
it('keeps args with nested choices arrays', () => {
|
|
6
|
+
const args = parseTsArgsBlock(`
|
|
7
|
+
{
|
|
8
|
+
name: 'period',
|
|
9
|
+
type: 'string',
|
|
10
|
+
default: 'seven',
|
|
11
|
+
help: 'Stats period: seven or thirty',
|
|
12
|
+
choices: ['seven', 'thirty'],
|
|
13
|
+
},
|
|
14
|
+
`);
|
|
15
|
+
|
|
16
|
+
expect(args).toEqual([
|
|
17
|
+
{
|
|
18
|
+
name: 'period',
|
|
19
|
+
type: 'string',
|
|
20
|
+
default: 'seven',
|
|
21
|
+
required: false,
|
|
22
|
+
positional: undefined,
|
|
23
|
+
help: 'Stats period: seven or thirty',
|
|
24
|
+
choices: ['seven', 'thirty'],
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
});
|