@jackwener/opencli 0.9.6 → 1.0.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/.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/CDP.md +1 -1
- package/CDP.zh-CN.md +1 -1
- package/CLI-ELECTRON.md +89 -36
- package/CLI-EXPLORER.md +4 -4
- package/CONTRIBUTING.md +167 -0
- package/README.md +113 -89
- package/README.zh-CN.md +114 -91
- package/SKILL.md +10 -8
- package/TESTING.md +7 -7
- package/dist/browser/daemon-client.d.ts +37 -0
- package/dist/browser/daemon-client.js +82 -0
- package/dist/browser/discover.d.ts +11 -34
- package/dist/browser/discover.js +15 -190
- package/dist/browser/errors.d.ts +6 -20
- package/dist/browser/errors.js +24 -63
- package/dist/browser/index.d.ts +2 -11
- package/dist/browser/index.js +5 -11
- package/dist/browser/mcp.d.ts +9 -18
- package/dist/browser/mcp.js +70 -284
- package/dist/browser/page.d.ts +28 -6
- package/dist/browser/page.js +210 -85
- package/dist/browser.test.js +4 -202
- 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 +1582 -29
- package/dist/clis/bilibili/download.d.ts +10 -0
- package/dist/clis/bilibili/download.js +135 -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/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/neteasemusic/like.d.ts +1 -0
- package/dist/clis/neteasemusic/like.js +25 -0
- package/dist/clis/neteasemusic/lyrics.d.ts +1 -0
- package/dist/clis/neteasemusic/lyrics.js +47 -0
- package/dist/clis/neteasemusic/next.d.ts +1 -0
- package/dist/clis/neteasemusic/next.js +26 -0
- package/dist/clis/neteasemusic/play.d.ts +1 -0
- package/dist/clis/neteasemusic/play.js +26 -0
- package/dist/clis/neteasemusic/playing.d.ts +1 -0
- package/dist/clis/neteasemusic/playing.js +59 -0
- package/dist/clis/neteasemusic/playlist.d.ts +1 -0
- package/dist/clis/neteasemusic/playlist.js +46 -0
- package/dist/clis/neteasemusic/prev.d.ts +1 -0
- package/dist/clis/neteasemusic/prev.js +25 -0
- package/dist/clis/neteasemusic/search.d.ts +1 -0
- package/dist/clis/neteasemusic/search.js +52 -0
- package/dist/clis/neteasemusic/status.d.ts +1 -0
- package/dist/clis/neteasemusic/status.js +16 -0
- package/dist/clis/neteasemusic/volume.d.ts +1 -0
- package/dist/clis/neteasemusic/volume.js +54 -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/daemon.d.ts +13 -0
- package/dist/daemon.js +187 -0
- package/dist/doctor.d.ts +27 -61
- package/dist/doctor.js +70 -601
- package/dist/doctor.test.js +30 -170
- 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 +22 -28
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/registry.js +2 -0
- package/dist/pipeline/steps/browser.js +2 -2
- package/dist/pipeline/steps/download.d.ts +34 -0
- package/dist/pipeline/steps/download.js +251 -0
- package/dist/pipeline/steps/intercept.js +1 -2
- package/dist/pipeline/template.js +28 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +46 -160
- package/dist/types.d.ts +6 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +31 -0
- package/extension/package.json +16 -0
- package/extension/src/background.ts +293 -0
- package/extension/src/cdp.ts +125 -0
- package/extension/src/protocol.ts +57 -0
- package/extension/store-assets/screenshot-1280x800.png +0 -0
- package/extension/tsconfig.json +15 -0
- package/extension/vite.config.ts +18 -0
- package/package.json +8 -7
- package/scripts/test-site.mjs +70 -0
- package/src/browser/daemon-client.ts +113 -0
- package/src/browser/discover.ts +18 -216
- package/src/browser/errors.ts +30 -100
- package/src/browser/index.ts +6 -12
- package/src/browser/mcp.ts +78 -278
- package/src/browser/page.ts +222 -88
- package/src/browser.test.ts +3 -210
- package/src/build-manifest.test.ts +28 -0
- package/src/build-manifest.ts +147 -57
- package/src/clis/bilibili/download.ts +161 -0
- package/src/clis/chatgpt/README.md +1 -1
- package/src/clis/chatgpt/README.zh-CN.md +1 -1
- 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/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/neteasemusic/README.md +31 -0
- package/src/clis/neteasemusic/README.zh-CN.md +31 -0
- package/src/clis/neteasemusic/like.ts +28 -0
- package/src/clis/neteasemusic/lyrics.ts +53 -0
- package/src/clis/neteasemusic/next.ts +30 -0
- package/src/clis/neteasemusic/play.ts +30 -0
- package/src/clis/neteasemusic/playing.ts +62 -0
- package/src/clis/neteasemusic/playlist.ts +51 -0
- package/src/clis/neteasemusic/prev.ts +29 -0
- package/src/clis/neteasemusic/search.ts +58 -0
- package/src/clis/neteasemusic/status.ts +18 -0
- package/src/clis/neteasemusic/volume.ts +61 -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/daemon.ts +217 -0
- package/src/doctor.test.ts +32 -193
- package/src/doctor.ts +74 -668
- 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 +18 -26
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/registry.ts +2 -0
- package/src/pipeline/steps/browser.ts +2 -2
- package/src/pipeline/steps/download.ts +310 -0
- package/src/pipeline/steps/intercept.ts +1 -2
- package/src/pipeline/template.ts +26 -0
- package/src/setup.ts +47 -183
- package/src/types.ts +1 -0
- package/tests/e2e/browser-auth.test.ts +25 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download progress display: terminal progress bars, status updates.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
/**
|
|
6
|
+
* Format bytes as human-readable string (KB, MB, GB).
|
|
7
|
+
*/
|
|
8
|
+
export function formatBytes(bytes) {
|
|
9
|
+
if (bytes === 0)
|
|
10
|
+
return '0 B';
|
|
11
|
+
const k = 1024;
|
|
12
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
13
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
14
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Format milliseconds as human-readable duration.
|
|
18
|
+
*/
|
|
19
|
+
export function formatDuration(ms) {
|
|
20
|
+
if (ms < 1000)
|
|
21
|
+
return `${ms}ms`;
|
|
22
|
+
const seconds = Math.floor(ms / 1000);
|
|
23
|
+
if (seconds < 60)
|
|
24
|
+
return `${seconds}s`;
|
|
25
|
+
const minutes = Math.floor(seconds / 60);
|
|
26
|
+
const remainingSeconds = seconds % 60;
|
|
27
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a simple progress bar for terminal display.
|
|
31
|
+
*/
|
|
32
|
+
export function createProgressBar(filename, index, total) {
|
|
33
|
+
const prefix = chalk.dim(`[${index + 1}/${total}]`);
|
|
34
|
+
const truncatedName = filename.length > 40 ? filename.slice(0, 37) + '...' : filename;
|
|
35
|
+
return {
|
|
36
|
+
update(current, totalBytes, label) {
|
|
37
|
+
const percent = totalBytes > 0 ? Math.round((current / totalBytes) * 100) : 0;
|
|
38
|
+
const bar = createBar(percent);
|
|
39
|
+
const size = totalBytes > 0 ? formatBytes(totalBytes) : '';
|
|
40
|
+
const extra = label ? ` ${label}` : '';
|
|
41
|
+
process.stderr.write(`\r${prefix} ${truncatedName} ${bar} ${percent}% ${size}${extra}`);
|
|
42
|
+
},
|
|
43
|
+
complete(success, message) {
|
|
44
|
+
const icon = success ? chalk.green('✓') : chalk.red('✗');
|
|
45
|
+
const msg = message ? ` ${chalk.dim(message)}` : '';
|
|
46
|
+
process.stderr.write(`\r${prefix} ${icon} ${truncatedName}${msg}\n`);
|
|
47
|
+
},
|
|
48
|
+
fail(error) {
|
|
49
|
+
process.stderr.write(`\r${prefix} ${chalk.red('✗')} ${truncatedName} ${chalk.red(error)}\n`);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a progress bar string.
|
|
55
|
+
*/
|
|
56
|
+
function createBar(percent, width = 20) {
|
|
57
|
+
const filled = Math.round((percent / 100) * width);
|
|
58
|
+
const empty = width - filled;
|
|
59
|
+
return chalk.cyan('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Multi-file download progress tracker.
|
|
63
|
+
*/
|
|
64
|
+
export class DownloadProgressTracker {
|
|
65
|
+
completed = 0;
|
|
66
|
+
failed = 0;
|
|
67
|
+
skipped = 0;
|
|
68
|
+
total;
|
|
69
|
+
startTime;
|
|
70
|
+
verbose;
|
|
71
|
+
constructor(total, verbose = true) {
|
|
72
|
+
this.total = total;
|
|
73
|
+
this.startTime = Date.now();
|
|
74
|
+
this.verbose = verbose;
|
|
75
|
+
}
|
|
76
|
+
onFileStart(filename, index) {
|
|
77
|
+
if (!this.verbose)
|
|
78
|
+
return null;
|
|
79
|
+
return createProgressBar(filename, index, this.total);
|
|
80
|
+
}
|
|
81
|
+
onFileComplete(success, skipped = false) {
|
|
82
|
+
if (skipped) {
|
|
83
|
+
this.skipped++;
|
|
84
|
+
}
|
|
85
|
+
else if (success) {
|
|
86
|
+
this.completed++;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.failed++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
getSummary() {
|
|
93
|
+
const elapsed = formatDuration(Date.now() - this.startTime);
|
|
94
|
+
const parts = [];
|
|
95
|
+
if (this.completed > 0) {
|
|
96
|
+
parts.push(chalk.green(`${this.completed} downloaded`));
|
|
97
|
+
}
|
|
98
|
+
if (this.skipped > 0) {
|
|
99
|
+
parts.push(chalk.yellow(`${this.skipped} skipped`));
|
|
100
|
+
}
|
|
101
|
+
if (this.failed > 0) {
|
|
102
|
+
parts.push(chalk.red(`${this.failed} failed`));
|
|
103
|
+
}
|
|
104
|
+
return `${parts.join(', ')} in ${elapsed}`;
|
|
105
|
+
}
|
|
106
|
+
finish() {
|
|
107
|
+
if (this.verbose) {
|
|
108
|
+
process.stderr.write(`\n${chalk.bold('Download complete:')} ${this.getSummary()}\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/engine.test.js
CHANGED
|
@@ -11,6 +11,21 @@ describe('discoverClis', () => {
|
|
|
11
11
|
});
|
|
12
12
|
});
|
|
13
13
|
describe('executeCommand', () => {
|
|
14
|
+
it('accepts kebab-case option names after Commander camelCases them', async () => {
|
|
15
|
+
const cmd = cli({
|
|
16
|
+
site: 'test-engine',
|
|
17
|
+
name: 'kebab-arg-test',
|
|
18
|
+
description: 'test command with kebab-case arg',
|
|
19
|
+
browser: false,
|
|
20
|
+
strategy: Strategy.PUBLIC,
|
|
21
|
+
args: [
|
|
22
|
+
{ name: 'note-id', required: true, help: 'Note ID' },
|
|
23
|
+
],
|
|
24
|
+
func: async (_page, kwargs) => [{ noteId: kwargs['note-id'] }],
|
|
25
|
+
});
|
|
26
|
+
const result = await executeCommand(cmd, null, { 'note-id': 'abc123' });
|
|
27
|
+
expect(result).toEqual([{ noteId: 'abc123' }]);
|
|
28
|
+
});
|
|
14
29
|
it('executes a command with func', async () => {
|
|
15
30
|
const cmd = cli({
|
|
16
31
|
site: 'test-engine',
|
package/dist/main.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { Command } from 'commander';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { discoverClis, executeCommand } from './engine.js';
|
|
11
|
-
import { fullName, getRegistry, strategyLabel } from './registry.js';
|
|
11
|
+
import { Strategy, fullName, getRegistry, strategyLabel } from './registry.js';
|
|
12
12
|
import { render as renderOutput } from './output.js';
|
|
13
13
|
import { PlaywrightMCP } from './browser/index.js';
|
|
14
14
|
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
|
@@ -122,37 +122,18 @@ program.command('cascade').description('Strategy cascade: find simplest working
|
|
|
122
122
|
console.log(renderCascadeResult(result));
|
|
123
123
|
});
|
|
124
124
|
program.command('doctor')
|
|
125
|
-
.description('Diagnose
|
|
126
|
-
.option('--fix', 'Apply suggested fixes to shell rc and detected MCP configs', false)
|
|
127
|
-
.option('-y, --yes', 'Skip confirmation prompts when applying fixes', false)
|
|
128
|
-
.option('--token <token>', 'Override token to write instead of auto-detecting')
|
|
125
|
+
.description('Diagnose opencli browser bridge connectivity')
|
|
129
126
|
.option('--live', 'Test browser connectivity (requires Chrome running)', false)
|
|
130
|
-
.option('--shell-rc <path>', 'Shell startup file to update')
|
|
131
|
-
.option('--mcp-config <paths>', 'Comma-separated MCP config paths to scan/update')
|
|
132
127
|
.action(async (opts) => {
|
|
133
|
-
const { runBrowserDoctor, renderBrowserDoctorReport
|
|
134
|
-
const
|
|
135
|
-
const report = await runBrowserDoctor({ token: opts.token, live: opts.live, shellRc: opts.shellRc, configPaths, cliVersion: PKG_VERSION });
|
|
128
|
+
const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
|
|
129
|
+
const report = await runBrowserDoctor({ live: opts.live, cliVersion: PKG_VERSION });
|
|
136
130
|
console.log(renderBrowserDoctorReport(report));
|
|
137
|
-
if (opts.fix) {
|
|
138
|
-
const written = await applyBrowserDoctorFix(report, { fix: true, yes: opts.yes, token: opts.token, shellRc: opts.shellRc, configPaths });
|
|
139
|
-
console.log();
|
|
140
|
-
if (written.length > 0) {
|
|
141
|
-
console.log(chalk.green('Updated files:'));
|
|
142
|
-
for (const filePath of written)
|
|
143
|
-
console.log(`- ${filePath}`);
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
console.log(chalk.yellow('No files were changed.'));
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
131
|
});
|
|
150
132
|
program.command('setup')
|
|
151
|
-
.description('Interactive setup:
|
|
152
|
-
.
|
|
153
|
-
.action(async (opts) => {
|
|
133
|
+
.description('Interactive setup: verify browser bridge connectivity')
|
|
134
|
+
.action(async () => {
|
|
154
135
|
const { runSetup } = await import('./setup.js');
|
|
155
|
-
await runSetup({ cliVersion: PKG_VERSION
|
|
136
|
+
await runSetup({ cliVersion: PKG_VERSION });
|
|
156
137
|
});
|
|
157
138
|
program.command('completion')
|
|
158
139
|
.description('Output shell completion script')
|
|
@@ -205,7 +186,8 @@ for (const [, cmd] of registry) {
|
|
|
205
186
|
for (const arg of cmd.args) {
|
|
206
187
|
if (arg.positional)
|
|
207
188
|
continue;
|
|
208
|
-
const
|
|
189
|
+
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
|
|
190
|
+
const v = actionOpts[arg.name] ?? actionOpts[camelName];
|
|
209
191
|
if (v !== undefined)
|
|
210
192
|
kwargs[arg.name] = v;
|
|
211
193
|
}
|
|
@@ -214,7 +196,19 @@ for (const [, cmd] of registry) {
|
|
|
214
196
|
process.env.OPENCLI_VERBOSE = '1';
|
|
215
197
|
let result;
|
|
216
198
|
if (cmd.browser) {
|
|
217
|
-
result = await browserSession(PlaywrightMCP, async (page) =>
|
|
199
|
+
result = await browserSession(PlaywrightMCP, async (page) => {
|
|
200
|
+
// Cookie/header strategies require same-origin context for credentialed fetch.
|
|
201
|
+
// In CDP mode the active tab may be on an unrelated domain, causing CORS failures.
|
|
202
|
+
// Navigate to the command's domain first (mirrors cascade command behavior).
|
|
203
|
+
if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
|
|
204
|
+
try {
|
|
205
|
+
await page.goto(`https://${cmd.domain}`);
|
|
206
|
+
await page.wait(2);
|
|
207
|
+
}
|
|
208
|
+
catch { }
|
|
209
|
+
}
|
|
210
|
+
return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
|
|
211
|
+
});
|
|
218
212
|
}
|
|
219
213
|
else {
|
|
220
214
|
result = await executeCommand(cmd, null, kwargs, actionOpts.verbose);
|
|
@@ -8,6 +8,7 @@ import { stepFetch } from './steps/fetch.js';
|
|
|
8
8
|
import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
|
|
9
9
|
import { stepIntercept } from './steps/intercept.js';
|
|
10
10
|
import { stepTap } from './steps/tap.js';
|
|
11
|
+
import { stepDownload } from './steps/download.js';
|
|
11
12
|
const _stepRegistry = new Map();
|
|
12
13
|
/**
|
|
13
14
|
* Get a registered step handler by name.
|
|
@@ -39,3 +40,4 @@ registerStep('sort', stepSort);
|
|
|
39
40
|
registerStep('limit', stepLimit);
|
|
40
41
|
registerStep('intercept', stepIntercept);
|
|
41
42
|
registerStep('tap', stepTap);
|
|
43
|
+
registerStep('download', stepDownload);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Pipeline step: navigate, click, type, wait, press, snapshot.
|
|
3
3
|
* Browser interaction primitives.
|
|
4
4
|
*/
|
|
5
|
-
import { render
|
|
5
|
+
import { render } from '../template.js';
|
|
6
6
|
export async function stepNavigate(page, params, data, args) {
|
|
7
7
|
const url = render(params, { args, data });
|
|
8
8
|
await page.goto(String(url));
|
|
@@ -49,7 +49,7 @@ export async function stepSnapshot(page, params, _data, _args) {
|
|
|
49
49
|
}
|
|
50
50
|
export async function stepEvaluate(page, params, data, args) {
|
|
51
51
|
const js = String(render(params, { args, data }));
|
|
52
|
-
let result = await page.evaluate(
|
|
52
|
+
let result = await page.evaluate(js);
|
|
53
53
|
// MCP may return JSON as a string — auto-parse it
|
|
54
54
|
if (typeof result === 'string') {
|
|
55
55
|
const trimmed = result.trim();
|
|
@@ -0,0 +1,34 @@
|
|
|
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 type { IPage } from '../../types.js';
|
|
11
|
+
export interface DownloadResult {
|
|
12
|
+
status: 'success' | 'skipped' | 'failed';
|
|
13
|
+
path?: string;
|
|
14
|
+
size?: number;
|
|
15
|
+
error?: string;
|
|
16
|
+
duration?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Download step handler for YAML pipelines.
|
|
20
|
+
*
|
|
21
|
+
* Usage in YAML:
|
|
22
|
+
* ```yaml
|
|
23
|
+
* pipeline:
|
|
24
|
+
* - download:
|
|
25
|
+
* url: ${{ item.imageUrl }}
|
|
26
|
+
* dir: ./downloads
|
|
27
|
+
* filename: ${{ item.title }}.jpg
|
|
28
|
+
* concurrency: 5
|
|
29
|
+
* skip_existing: true
|
|
30
|
+
* use_ytdlp: false
|
|
31
|
+
* type: auto
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function stepDownload(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pipeline step: intercept — declarative XHR interception.
|
|
3
3
|
*/
|
|
4
|
-
import { render } from '../template.js';
|
|
4
|
+
import { render, normalizeEvaluateSource } from '../template.js';
|
|
5
5
|
import { generateInterceptorJs, generateReadInterceptedJs } from '../../interceptor.js';
|
|
6
6
|
export async function stepIntercept(page, params, data, args) {
|
|
7
7
|
const cfg = typeof params === 'object' ? params : {};
|
|
@@ -20,7 +20,6 @@ export async function stepIntercept(page, params, data, args) {
|
|
|
20
20
|
}
|
|
21
21
|
else if (trigger.startsWith('evaluate:')) {
|
|
22
22
|
const js = trigger.slice('evaluate:'.length);
|
|
23
|
-
const { normalizeEvaluateSource } = await import('../template.js');
|
|
24
23
|
await page.evaluate(normalizeEvaluateSource(render(js, { args, data })));
|
|
25
24
|
}
|
|
26
25
|
else if (trigger.startsWith('click:')) {
|
|
@@ -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/dist/setup.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setup.ts — Interactive browser setup for opencli
|
|
3
|
+
*
|
|
4
|
+
* Simplified for daemon-based architecture. No more token management.
|
|
5
|
+
* Just verifies daemon + extension connectivity.
|
|
6
|
+
*/
|
|
1
7
|
export declare function runSetup(opts?: {
|
|
2
8
|
cliVersion?: string;
|
|
3
9
|
token?: string;
|