@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
package/dist/doctor.test.js
CHANGED
|
@@ -1,191 +1,51 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
describe('shell token helpers', () => {
|
|
4
|
-
it('reads token from shell export', () => {
|
|
5
|
-
expect(readTokenFromShellContent('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="abc123"\n')).toBe('abc123');
|
|
6
|
-
});
|
|
7
|
-
it('appends token export when missing', () => {
|
|
8
|
-
const next = upsertShellToken('export PATH="/usr/bin"\n', 'abc123');
|
|
9
|
-
expect(next).toContain('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="abc123"');
|
|
10
|
-
});
|
|
11
|
-
it('replaces token export when present', () => {
|
|
12
|
-
const next = upsertShellToken('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="old"\n', 'new');
|
|
13
|
-
expect(next).toContain('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="new"');
|
|
14
|
-
expect(next).not.toContain('"old"');
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
describe('toml token helpers', () => {
|
|
18
|
-
it('reads token from playwright env section', () => {
|
|
19
|
-
const content = `
|
|
20
|
-
[mcp_servers.playwright.env]
|
|
21
|
-
PLAYWRIGHT_MCP_EXTENSION_TOKEN = "abc123"
|
|
22
|
-
`;
|
|
23
|
-
expect(readTomlConfigToken(content)).toBe('abc123');
|
|
24
|
-
});
|
|
25
|
-
it('updates token inside existing env section', () => {
|
|
26
|
-
const content = `
|
|
27
|
-
[mcp_servers.playwright.env]
|
|
28
|
-
PLAYWRIGHT_MCP_EXTENSION_TOKEN = "old"
|
|
29
|
-
`;
|
|
30
|
-
const next = upsertTomlConfigToken(content, 'new');
|
|
31
|
-
expect(next).toContain('PLAYWRIGHT_MCP_EXTENSION_TOKEN = "new"');
|
|
32
|
-
expect(next).not.toContain('"old"');
|
|
33
|
-
});
|
|
34
|
-
it('creates env section when missing', () => {
|
|
35
|
-
const content = `
|
|
36
|
-
[mcp_servers.playwright]
|
|
37
|
-
type = "stdio"
|
|
38
|
-
`;
|
|
39
|
-
const next = upsertTomlConfigToken(content, 'abc123');
|
|
40
|
-
expect(next).toContain('[mcp_servers.playwright.env]');
|
|
41
|
-
expect(next).toContain('PLAYWRIGHT_MCP_EXTENSION_TOKEN = "abc123"');
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
describe('json token helpers', () => {
|
|
45
|
-
it('writes token into standard mcpServers config', () => {
|
|
46
|
-
const next = upsertJsonConfigToken(JSON.stringify({
|
|
47
|
-
mcpServers: {
|
|
48
|
-
playwright: {
|
|
49
|
-
command: 'npx',
|
|
50
|
-
args: ['-y', '@playwright/mcp@latest', '--extension'],
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
}), 'abc123');
|
|
54
|
-
const parsed = JSON.parse(next);
|
|
55
|
-
expect(parsed.mcpServers.playwright.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
|
|
56
|
-
});
|
|
57
|
-
it('writes token into opencode mcp config', () => {
|
|
58
|
-
const next = upsertJsonConfigToken(JSON.stringify({
|
|
59
|
-
$schema: 'https://opencode.ai/config.json',
|
|
60
|
-
mcp: {
|
|
61
|
-
playwright: {
|
|
62
|
-
command: ['npx', '-y', '@playwright/mcp@latest', '--extension'],
|
|
63
|
-
enabled: true,
|
|
64
|
-
type: 'local',
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
}), 'abc123');
|
|
68
|
-
const parsed = JSON.parse(next);
|
|
69
|
-
expect(parsed.mcp.playwright.environment.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
|
|
70
|
-
});
|
|
71
|
-
it('creates standard mcpServers format for empty file (not OpenCode)', () => {
|
|
72
|
-
const next = upsertJsonConfigToken('', 'abc123');
|
|
73
|
-
const parsed = JSON.parse(next);
|
|
74
|
-
expect(parsed.mcpServers.playwright.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
|
|
75
|
-
expect(parsed.mcp).toBeUndefined();
|
|
76
|
-
});
|
|
77
|
-
it('creates OpenCode format when filePath contains opencode', () => {
|
|
78
|
-
const next = upsertJsonConfigToken('', 'abc123', '/home/user/.config/opencode/opencode.json');
|
|
79
|
-
const parsed = JSON.parse(next);
|
|
80
|
-
expect(parsed.mcp.playwright.environment.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
|
|
81
|
-
expect(parsed.mcpServers).toBeUndefined();
|
|
82
|
-
});
|
|
83
|
-
it('creates standard format when filePath is claude.json', () => {
|
|
84
|
-
const next = upsertJsonConfigToken('', 'abc123', '/home/user/.claude.json');
|
|
85
|
-
const parsed = JSON.parse(next);
|
|
86
|
-
expect(parsed.mcpServers.playwright.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
describe('fish shell support', () => {
|
|
90
|
-
it('generates fish set -gx syntax for fish config path', () => {
|
|
91
|
-
const next = upsertShellToken('', 'abc123', '/home/user/.config/fish/config.fish');
|
|
92
|
-
expect(next).toContain('set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "abc123"');
|
|
93
|
-
expect(next).not.toContain('export');
|
|
94
|
-
});
|
|
95
|
-
it('replaces existing fish set line', () => {
|
|
96
|
-
const content = 'set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "old"\n';
|
|
97
|
-
const next = upsertShellToken(content, 'new', '/home/user/.config/fish/config.fish');
|
|
98
|
-
expect(next).toContain('set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "new"');
|
|
99
|
-
expect(next).not.toContain('"old"');
|
|
100
|
-
});
|
|
101
|
-
it('appends fish syntax to existing fish config', () => {
|
|
102
|
-
const content = 'set -gx PATH /usr/bin\n';
|
|
103
|
-
const next = upsertShellToken(content, 'abc123', '/home/user/.config/fish/config.fish');
|
|
104
|
-
expect(next).toContain('set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "abc123"');
|
|
105
|
-
expect(next).toContain('set -gx PATH /usr/bin');
|
|
106
|
-
});
|
|
107
|
-
it('uses export syntax for zshrc even with filePath', () => {
|
|
108
|
-
const next = upsertShellToken('', 'abc123', '/home/user/.zshrc');
|
|
109
|
-
expect(next).toContain('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="abc123"');
|
|
110
|
-
expect(next).not.toContain('set -gx');
|
|
111
|
-
});
|
|
112
|
-
});
|
|
2
|
+
import { renderBrowserDoctorReport } from './doctor.js';
|
|
113
3
|
describe('doctor report rendering', () => {
|
|
114
4
|
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
115
|
-
it('renders OK-style report when
|
|
5
|
+
it('renders OK-style report when daemon and extension connected', () => {
|
|
116
6
|
const text = strip(renderBrowserDoctorReport({
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
extensionToken: 'abc123',
|
|
120
|
-
extensionFingerprint: 'fp1',
|
|
121
|
-
extensionInstalled: true,
|
|
122
|
-
extensionBrowsers: ['Chrome'],
|
|
123
|
-
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'abc123', fingerprint: 'fp1' }],
|
|
124
|
-
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
125
|
-
recommendedToken: 'abc123',
|
|
126
|
-
recommendedFingerprint: 'fp1',
|
|
127
|
-
warnings: [],
|
|
7
|
+
daemonRunning: true,
|
|
8
|
+
extensionConnected: true,
|
|
128
9
|
issues: [],
|
|
129
10
|
}));
|
|
130
|
-
expect(text).toContain('[OK]
|
|
131
|
-
expect(text).toContain('[OK]
|
|
132
|
-
expect(text).toContain('
|
|
133
|
-
|
|
11
|
+
expect(text).toContain('[OK] Daemon: running on port 19825');
|
|
12
|
+
expect(text).toContain('[OK] Extension: connected');
|
|
13
|
+
expect(text).toContain('Everything looks good!');
|
|
14
|
+
});
|
|
15
|
+
it('renders MISSING when daemon not running', () => {
|
|
16
|
+
const text = strip(renderBrowserDoctorReport({
|
|
17
|
+
daemonRunning: false,
|
|
18
|
+
extensionConnected: false,
|
|
19
|
+
issues: ['Daemon is not running.'],
|
|
20
|
+
}));
|
|
21
|
+
expect(text).toContain('[MISSING] Daemon: not running');
|
|
22
|
+
expect(text).toContain('[MISSING] Extension: not connected');
|
|
23
|
+
expect(text).toContain('Daemon is not running.');
|
|
134
24
|
});
|
|
135
|
-
it('renders
|
|
25
|
+
it('renders extension not connected when daemon is running', () => {
|
|
136
26
|
const text = strip(renderBrowserDoctorReport({
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
extensionFingerprint: null,
|
|
141
|
-
extensionInstalled: false,
|
|
142
|
-
extensionBrowsers: [],
|
|
143
|
-
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'def456', fingerprint: 'fp2' }],
|
|
144
|
-
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
145
|
-
recommendedToken: 'abc123',
|
|
146
|
-
recommendedFingerprint: 'fp1',
|
|
147
|
-
warnings: [],
|
|
148
|
-
issues: ['Detected inconsistent Playwright MCP tokens across env/config files.'],
|
|
27
|
+
daemonRunning: true,
|
|
28
|
+
extensionConnected: false,
|
|
29
|
+
issues: ['Daemon is running but the Chrome extension is not connected.'],
|
|
149
30
|
}));
|
|
150
|
-
expect(text).toContain('[
|
|
151
|
-
expect(text).toContain('[
|
|
152
|
-
expect(text).toContain('[MISMATCH] /tmp/.zshrc');
|
|
153
|
-
expect(text).toContain('configured (fp2)');
|
|
154
|
-
expect(text).toContain('[MISMATCH] Recommended token fingerprint: fp1');
|
|
31
|
+
expect(text).toContain('[OK] Daemon: running on port 19825');
|
|
32
|
+
expect(text).toContain('[MISSING] Extension: not connected');
|
|
155
33
|
});
|
|
156
34
|
it('renders connectivity OK when live test succeeds', () => {
|
|
157
35
|
const text = strip(renderBrowserDoctorReport({
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
extensionToken: 'abc123',
|
|
161
|
-
extensionFingerprint: 'fp1',
|
|
162
|
-
extensionInstalled: true,
|
|
163
|
-
extensionBrowsers: ['Chrome'],
|
|
164
|
-
shellFiles: [],
|
|
165
|
-
configs: [],
|
|
166
|
-
recommendedToken: 'abc123',
|
|
167
|
-
recommendedFingerprint: 'fp1',
|
|
36
|
+
daemonRunning: true,
|
|
37
|
+
extensionConnected: true,
|
|
168
38
|
connectivity: { ok: true, durationMs: 1234 },
|
|
169
|
-
warnings: [],
|
|
170
39
|
issues: [],
|
|
171
40
|
}));
|
|
172
|
-
expect(text).toContain('[OK]
|
|
41
|
+
expect(text).toContain('[OK] Connectivity: connected in 1.2s');
|
|
173
42
|
});
|
|
174
|
-
it('renders connectivity
|
|
43
|
+
it('renders connectivity SKIP when not tested', () => {
|
|
175
44
|
const text = strip(renderBrowserDoctorReport({
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
extensionToken: 'abc123',
|
|
179
|
-
extensionFingerprint: 'fp1',
|
|
180
|
-
extensionInstalled: true,
|
|
181
|
-
extensionBrowsers: ['Chrome'],
|
|
182
|
-
shellFiles: [],
|
|
183
|
-
configs: [],
|
|
184
|
-
recommendedToken: 'abc123',
|
|
185
|
-
recommendedFingerprint: 'fp1',
|
|
186
|
-
warnings: [],
|
|
45
|
+
daemonRunning: true,
|
|
46
|
+
extensionConnected: true,
|
|
187
47
|
issues: [],
|
|
188
48
|
}));
|
|
189
|
-
expect(text).toContain('[
|
|
49
|
+
expect(text).toContain('[SKIP] Connectivity: not tested (use --live)');
|
|
190
50
|
});
|
|
191
51
|
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download utilities: HTTP downloads, yt-dlp wrapper, format conversion.
|
|
3
|
+
*/
|
|
4
|
+
export interface DownloadOptions {
|
|
5
|
+
cookies?: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
timeout?: number;
|
|
8
|
+
onProgress?: (received: number, total: number) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface YtdlpOptions {
|
|
11
|
+
cookies?: string;
|
|
12
|
+
cookiesFile?: string;
|
|
13
|
+
format?: string;
|
|
14
|
+
extraArgs?: string[];
|
|
15
|
+
onProgress?: (percent: number) => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if yt-dlp is available in PATH.
|
|
19
|
+
*/
|
|
20
|
+
export declare function checkYtdlp(): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Check if ffmpeg is available in PATH.
|
|
23
|
+
*/
|
|
24
|
+
export declare function checkFfmpeg(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Detect content type from URL and optional headers.
|
|
27
|
+
*/
|
|
28
|
+
export declare function detectContentType(url: string, contentType?: string): 'image' | 'video' | 'document' | 'binary';
|
|
29
|
+
/**
|
|
30
|
+
* Check if URL requires yt-dlp for download.
|
|
31
|
+
*/
|
|
32
|
+
export declare function requiresYtdlp(url: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* HTTP download with progress callback.
|
|
35
|
+
*/
|
|
36
|
+
export declare function httpDownload(url: string, destPath: string, options?: DownloadOptions): Promise<{
|
|
37
|
+
success: boolean;
|
|
38
|
+
size: number;
|
|
39
|
+
error?: string;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Export cookies to Netscape format for yt-dlp.
|
|
43
|
+
*/
|
|
44
|
+
export declare function exportCookiesToNetscape(cookies: Array<{
|
|
45
|
+
name: string;
|
|
46
|
+
value: string;
|
|
47
|
+
domain: string;
|
|
48
|
+
path?: string;
|
|
49
|
+
secure?: boolean;
|
|
50
|
+
httpOnly?: boolean;
|
|
51
|
+
}>, filePath: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* Download video using yt-dlp.
|
|
54
|
+
*/
|
|
55
|
+
export declare function ytdlpDownload(url: string, destPath: string, options?: YtdlpOptions): Promise<{
|
|
56
|
+
success: boolean;
|
|
57
|
+
size: number;
|
|
58
|
+
error?: string;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Save document content to file.
|
|
62
|
+
*/
|
|
63
|
+
export declare function saveDocument(content: string, destPath: string, format?: 'json' | 'markdown' | 'html' | 'text', metadata?: Record<string, any>): Promise<{
|
|
64
|
+
success: boolean;
|
|
65
|
+
size: number;
|
|
66
|
+
error?: string;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Sanitize filename by removing invalid characters.
|
|
70
|
+
*/
|
|
71
|
+
export declare function sanitizeFilename(name: string, maxLength?: number): string;
|
|
72
|
+
/**
|
|
73
|
+
* Generate filename from URL if not provided.
|
|
74
|
+
*/
|
|
75
|
+
export declare function generateFilename(url: string, index: number, extension?: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Get temp directory for cookie files.
|
|
78
|
+
*/
|
|
79
|
+
export declare function getTempDir(): string;
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download utilities: HTTP downloads, yt-dlp wrapper, format conversion.
|
|
3
|
+
*/
|
|
4
|
+
import { spawn, execSync } from 'node:child_process';
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as https from 'node:https';
|
|
8
|
+
import * as http from 'node:http';
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import { URL } from 'node:url';
|
|
11
|
+
/**
|
|
12
|
+
* Check if yt-dlp is available in PATH.
|
|
13
|
+
*/
|
|
14
|
+
export function checkYtdlp() {
|
|
15
|
+
try {
|
|
16
|
+
execSync('yt-dlp --version', { encoding: 'utf-8', stdio: 'pipe' });
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if ffmpeg is available in PATH.
|
|
25
|
+
*/
|
|
26
|
+
export function checkFfmpeg() {
|
|
27
|
+
try {
|
|
28
|
+
execSync('ffmpeg -version', { encoding: 'utf-8', stdio: 'pipe' });
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Detect content type from URL and optional headers.
|
|
37
|
+
*/
|
|
38
|
+
export function detectContentType(url, contentType) {
|
|
39
|
+
// Check content-type header first
|
|
40
|
+
if (contentType) {
|
|
41
|
+
if (contentType.startsWith('image/'))
|
|
42
|
+
return 'image';
|
|
43
|
+
if (contentType.startsWith('video/'))
|
|
44
|
+
return 'video';
|
|
45
|
+
if (contentType.startsWith('text/') || contentType.includes('json') || contentType.includes('xml'))
|
|
46
|
+
return 'document';
|
|
47
|
+
}
|
|
48
|
+
// Detect from URL
|
|
49
|
+
const urlLower = url.toLowerCase();
|
|
50
|
+
const ext = path.extname(new URL(url).pathname).toLowerCase();
|
|
51
|
+
// Image extensions
|
|
52
|
+
if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.avif'].includes(ext)) {
|
|
53
|
+
return 'image';
|
|
54
|
+
}
|
|
55
|
+
// Video extensions
|
|
56
|
+
if (['.mp4', '.webm', '.avi', '.mov', '.mkv', '.flv', '.m3u8', '.ts'].includes(ext)) {
|
|
57
|
+
return 'video';
|
|
58
|
+
}
|
|
59
|
+
// Video platforms (need yt-dlp)
|
|
60
|
+
if (urlLower.includes('youtube.com') || urlLower.includes('youtu.be') ||
|
|
61
|
+
urlLower.includes('bilibili.com') || urlLower.includes('twitter.com') ||
|
|
62
|
+
urlLower.includes('x.com') || urlLower.includes('tiktok.com') ||
|
|
63
|
+
urlLower.includes('vimeo.com') || urlLower.includes('twitch.tv')) {
|
|
64
|
+
return 'video';
|
|
65
|
+
}
|
|
66
|
+
// Document extensions
|
|
67
|
+
if (['.html', '.htm', '.json', '.xml', '.txt', '.md', '.markdown'].includes(ext)) {
|
|
68
|
+
return 'document';
|
|
69
|
+
}
|
|
70
|
+
return 'binary';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check if URL requires yt-dlp for download.
|
|
74
|
+
*/
|
|
75
|
+
export function requiresYtdlp(url) {
|
|
76
|
+
const urlLower = url.toLowerCase();
|
|
77
|
+
return (urlLower.includes('youtube.com') ||
|
|
78
|
+
urlLower.includes('youtu.be') ||
|
|
79
|
+
urlLower.includes('bilibili.com/video') ||
|
|
80
|
+
urlLower.includes('twitter.com') ||
|
|
81
|
+
urlLower.includes('x.com') ||
|
|
82
|
+
urlLower.includes('tiktok.com') ||
|
|
83
|
+
urlLower.includes('vimeo.com') ||
|
|
84
|
+
urlLower.includes('twitch.tv'));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* HTTP download with progress callback.
|
|
88
|
+
*/
|
|
89
|
+
export async function httpDownload(url, destPath, options = {}) {
|
|
90
|
+
const { cookies, headers = {}, timeout = 30000, onProgress } = options;
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
const parsedUrl = new URL(url);
|
|
93
|
+
const protocol = parsedUrl.protocol === 'https:' ? https : http;
|
|
94
|
+
const requestHeaders = {
|
|
95
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
96
|
+
...headers,
|
|
97
|
+
};
|
|
98
|
+
if (cookies) {
|
|
99
|
+
requestHeaders['Cookie'] = cookies;
|
|
100
|
+
}
|
|
101
|
+
// Ensure directory exists
|
|
102
|
+
const dir = path.dirname(destPath);
|
|
103
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
104
|
+
const tempPath = `${destPath}.tmp`;
|
|
105
|
+
const file = fs.createWriteStream(tempPath);
|
|
106
|
+
const request = protocol.get(url, { headers: requestHeaders, timeout }, (response) => {
|
|
107
|
+
// Handle redirects
|
|
108
|
+
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
109
|
+
file.close();
|
|
110
|
+
fs.unlinkSync(tempPath);
|
|
111
|
+
httpDownload(response.headers.location, destPath, options).then(resolve);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (response.statusCode !== 200) {
|
|
115
|
+
file.close();
|
|
116
|
+
fs.unlinkSync(tempPath);
|
|
117
|
+
resolve({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
|
|
121
|
+
let received = 0;
|
|
122
|
+
response.on('data', (chunk) => {
|
|
123
|
+
received += chunk.length;
|
|
124
|
+
if (onProgress)
|
|
125
|
+
onProgress(received, totalSize);
|
|
126
|
+
});
|
|
127
|
+
response.pipe(file);
|
|
128
|
+
file.on('finish', () => {
|
|
129
|
+
file.close();
|
|
130
|
+
// Rename temp file to final destination
|
|
131
|
+
fs.renameSync(tempPath, destPath);
|
|
132
|
+
resolve({ success: true, size: received });
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
request.on('error', (err) => {
|
|
136
|
+
file.close();
|
|
137
|
+
if (fs.existsSync(tempPath))
|
|
138
|
+
fs.unlinkSync(tempPath);
|
|
139
|
+
resolve({ success: false, size: 0, error: err.message });
|
|
140
|
+
});
|
|
141
|
+
request.on('timeout', () => {
|
|
142
|
+
request.destroy();
|
|
143
|
+
file.close();
|
|
144
|
+
if (fs.existsSync(tempPath))
|
|
145
|
+
fs.unlinkSync(tempPath);
|
|
146
|
+
resolve({ success: false, size: 0, error: 'Timeout' });
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Export cookies to Netscape format for yt-dlp.
|
|
152
|
+
*/
|
|
153
|
+
export function exportCookiesToNetscape(cookies, filePath) {
|
|
154
|
+
const lines = [
|
|
155
|
+
'# Netscape HTTP Cookie File',
|
|
156
|
+
'# https://curl.se/docs/http-cookies.html',
|
|
157
|
+
'# This is a generated file! Do not edit.',
|
|
158
|
+
'',
|
|
159
|
+
];
|
|
160
|
+
for (const cookie of cookies) {
|
|
161
|
+
const domain = cookie.domain.startsWith('.') ? cookie.domain : `.${cookie.domain}`;
|
|
162
|
+
const includeSubdomains = 'TRUE';
|
|
163
|
+
const cookiePath = cookie.path || '/';
|
|
164
|
+
const secure = cookie.secure ? 'TRUE' : 'FALSE';
|
|
165
|
+
const expiry = Math.floor(Date.now() / 1000) + 86400 * 365; // 1 year from now
|
|
166
|
+
lines.push(`${domain}\t${includeSubdomains}\t${cookiePath}\t${secure}\t${expiry}\t${cookie.name}\t${cookie.value}`);
|
|
167
|
+
}
|
|
168
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
169
|
+
fs.writeFileSync(filePath, lines.join('\n'));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Download video using yt-dlp.
|
|
173
|
+
*/
|
|
174
|
+
export async function ytdlpDownload(url, destPath, options = {}) {
|
|
175
|
+
const { cookiesFile, format = 'best', extraArgs = [], onProgress } = options;
|
|
176
|
+
if (!checkYtdlp()) {
|
|
177
|
+
return { success: false, size: 0, error: 'yt-dlp not installed. Install with: pip install yt-dlp' };
|
|
178
|
+
}
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
const dir = path.dirname(destPath);
|
|
181
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
182
|
+
// Build yt-dlp arguments
|
|
183
|
+
const args = [
|
|
184
|
+
url,
|
|
185
|
+
'-o', destPath,
|
|
186
|
+
'-f', format,
|
|
187
|
+
'--no-playlist',
|
|
188
|
+
'--progress',
|
|
189
|
+
];
|
|
190
|
+
if (cookiesFile && fs.existsSync(cookiesFile)) {
|
|
191
|
+
args.push('--cookies', cookiesFile);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
// Try to use browser cookies
|
|
195
|
+
args.push('--cookies-from-browser', 'chrome');
|
|
196
|
+
}
|
|
197
|
+
args.push(...extraArgs);
|
|
198
|
+
const proc = spawn('yt-dlp', args, {
|
|
199
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
200
|
+
});
|
|
201
|
+
let lastPercent = 0;
|
|
202
|
+
let errorOutput = '';
|
|
203
|
+
proc.stderr.on('data', (data) => {
|
|
204
|
+
const line = data.toString();
|
|
205
|
+
errorOutput += line;
|
|
206
|
+
// Parse progress from yt-dlp output
|
|
207
|
+
const match = line.match(/(\d+\.?\d*)%/);
|
|
208
|
+
if (match && onProgress) {
|
|
209
|
+
const percent = parseFloat(match[1]);
|
|
210
|
+
if (percent > lastPercent) {
|
|
211
|
+
lastPercent = percent;
|
|
212
|
+
onProgress(percent);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
proc.stdout.on('data', (data) => {
|
|
217
|
+
const line = data.toString();
|
|
218
|
+
const match = line.match(/(\d+\.?\d*)%/);
|
|
219
|
+
if (match && onProgress) {
|
|
220
|
+
const percent = parseFloat(match[1]);
|
|
221
|
+
if (percent > lastPercent) {
|
|
222
|
+
lastPercent = percent;
|
|
223
|
+
onProgress(percent);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
proc.on('close', (code) => {
|
|
228
|
+
if (code === 0 && fs.existsSync(destPath)) {
|
|
229
|
+
const stats = fs.statSync(destPath);
|
|
230
|
+
resolve({ success: true, size: stats.size });
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
// Check for common yt-dlp output patterns
|
|
234
|
+
const patterns = fs.readdirSync(dir).filter(f => f.startsWith(path.basename(destPath, path.extname(destPath))));
|
|
235
|
+
if (patterns.length > 0) {
|
|
236
|
+
const actualFile = path.join(dir, patterns[0]);
|
|
237
|
+
const stats = fs.statSync(actualFile);
|
|
238
|
+
resolve({ success: true, size: stats.size });
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
resolve({ success: false, size: 0, error: errorOutput.slice(0, 200) || `Exit code ${code}` });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
proc.on('error', (err) => {
|
|
246
|
+
resolve({ success: false, size: 0, error: err.message });
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Save document content to file.
|
|
252
|
+
*/
|
|
253
|
+
export async function saveDocument(content, destPath, format = 'markdown', metadata) {
|
|
254
|
+
try {
|
|
255
|
+
const dir = path.dirname(destPath);
|
|
256
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
257
|
+
let output;
|
|
258
|
+
if (format === 'json') {
|
|
259
|
+
output = JSON.stringify({ ...metadata, content }, null, 2);
|
|
260
|
+
}
|
|
261
|
+
else if (format === 'markdown') {
|
|
262
|
+
// Add frontmatter if metadata exists
|
|
263
|
+
const frontmatter = metadata ? `---\n${Object.entries(metadata).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join('\n')}\n---\n\n` : '';
|
|
264
|
+
output = frontmatter + content;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
output = content;
|
|
268
|
+
}
|
|
269
|
+
fs.writeFileSync(destPath, output, 'utf-8');
|
|
270
|
+
return { success: true, size: Buffer.byteLength(output, 'utf-8') };
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
return { success: false, size: 0, error: err.message };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Sanitize filename by removing invalid characters.
|
|
278
|
+
*/
|
|
279
|
+
export function sanitizeFilename(name, maxLength = 200) {
|
|
280
|
+
return name
|
|
281
|
+
.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') // Remove invalid chars
|
|
282
|
+
.replace(/\s+/g, '_') // Replace spaces with underscores
|
|
283
|
+
.replace(/_+/g, '_') // Collapse multiple underscores
|
|
284
|
+
.replace(/^_|_$/g, '') // Trim underscores
|
|
285
|
+
.slice(0, maxLength);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Generate filename from URL if not provided.
|
|
289
|
+
*/
|
|
290
|
+
export function generateFilename(url, index, extension) {
|
|
291
|
+
try {
|
|
292
|
+
const parsedUrl = new URL(url);
|
|
293
|
+
const pathname = parsedUrl.pathname;
|
|
294
|
+
const basename = path.basename(pathname);
|
|
295
|
+
if (basename && basename !== '/' && basename.includes('.')) {
|
|
296
|
+
return sanitizeFilename(basename);
|
|
297
|
+
}
|
|
298
|
+
// Generate from hostname and index
|
|
299
|
+
const ext = extension || detectExtension(url);
|
|
300
|
+
const hostname = parsedUrl.hostname.replace(/^www\./, '');
|
|
301
|
+
return sanitizeFilename(`${hostname}_${index + 1}${ext}`);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
const ext = extension || '.bin';
|
|
305
|
+
return `download_${index + 1}${ext}`;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Detect file extension from URL.
|
|
310
|
+
*/
|
|
311
|
+
function detectExtension(url) {
|
|
312
|
+
const type = detectContentType(url);
|
|
313
|
+
switch (type) {
|
|
314
|
+
case 'image': return '.jpg';
|
|
315
|
+
case 'video': return '.mp4';
|
|
316
|
+
case 'document': return '.md';
|
|
317
|
+
default: return '.bin';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get temp directory for cookie files.
|
|
322
|
+
*/
|
|
323
|
+
export function getTempDir() {
|
|
324
|
+
return path.join(os.tmpdir(), 'opencli-download');
|
|
325
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download progress display: terminal progress bars, status updates.
|
|
3
|
+
*/
|
|
4
|
+
export interface ProgressBar {
|
|
5
|
+
update(current: number, total: number, label?: string): void;
|
|
6
|
+
complete(success: boolean, message?: string): void;
|
|
7
|
+
fail(error: string): void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Format bytes as human-readable string (KB, MB, GB).
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatBytes(bytes: number): string;
|
|
13
|
+
/**
|
|
14
|
+
* Format milliseconds as human-readable duration.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatDuration(ms: number): string;
|
|
17
|
+
/**
|
|
18
|
+
* Create a simple progress bar for terminal display.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createProgressBar(filename: string, index: number, total: number): ProgressBar;
|
|
21
|
+
/**
|
|
22
|
+
* Multi-file download progress tracker.
|
|
23
|
+
*/
|
|
24
|
+
export declare class DownloadProgressTracker {
|
|
25
|
+
private completed;
|
|
26
|
+
private failed;
|
|
27
|
+
private skipped;
|
|
28
|
+
private total;
|
|
29
|
+
private startTime;
|
|
30
|
+
private verbose;
|
|
31
|
+
constructor(total: number, verbose?: boolean);
|
|
32
|
+
onFileStart(filename: string, index: number): ProgressBar | null;
|
|
33
|
+
onFileComplete(success: boolean, skipped?: boolean): void;
|
|
34
|
+
getSummary(): string;
|
|
35
|
+
finish(): void;
|
|
36
|
+
}
|