@jackwener/opencli 1.3.1 → 1.3.2
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/CHANGELOG.md +128 -0
- package/README.md +44 -5
- package/README.zh-CN.md +44 -5
- package/SKILL.md +317 -5
- package/TESTING.md +4 -4
- package/dist/browser/errors.d.ts +2 -1
- package/dist/browser/errors.js +9 -10
- package/dist/build-manifest.js +1 -3
- package/dist/cli-manifest.json +2573 -989
- package/dist/cli.js +42 -2
- package/dist/clis/bilibili/download.js +20 -65
- package/dist/clis/bilibili/utils.js +2 -1
- package/dist/clis/chaoxing/assignments.js +2 -1
- package/dist/clis/doubao/ask.d.ts +1 -0
- package/dist/clis/doubao/ask.js +35 -0
- package/dist/clis/doubao/common.d.ts +23 -0
- package/dist/clis/doubao/common.js +564 -0
- package/dist/clis/doubao/new.d.ts +1 -0
- package/dist/clis/doubao/new.js +20 -0
- package/dist/clis/doubao/read.d.ts +1 -0
- package/dist/clis/doubao/read.js +19 -0
- package/dist/clis/doubao/send.d.ts +1 -0
- package/dist/clis/doubao/send.js +22 -0
- package/dist/clis/doubao/status.d.ts +1 -0
- package/dist/clis/doubao/status.js +24 -0
- package/dist/clis/doubao-app/ask.d.ts +1 -0
- package/dist/clis/doubao-app/ask.js +53 -0
- package/dist/clis/doubao-app/common.d.ts +37 -0
- package/dist/clis/doubao-app/common.js +110 -0
- package/dist/clis/doubao-app/dump.d.ts +1 -0
- package/dist/clis/doubao-app/dump.js +24 -0
- package/dist/clis/doubao-app/new.d.ts +1 -0
- package/dist/clis/doubao-app/new.js +20 -0
- package/dist/clis/doubao-app/read.d.ts +1 -0
- package/dist/clis/doubao-app/read.js +18 -0
- package/dist/clis/doubao-app/screenshot.d.ts +1 -0
- package/dist/clis/doubao-app/screenshot.js +18 -0
- package/dist/clis/doubao-app/send.d.ts +1 -0
- package/dist/clis/doubao-app/send.js +27 -0
- package/dist/clis/doubao-app/status.d.ts +1 -0
- package/dist/clis/doubao-app/status.js +16 -0
- package/dist/clis/hackernews/ask.yaml +38 -0
- package/dist/clis/hackernews/best.yaml +38 -0
- package/dist/clis/hackernews/jobs.yaml +36 -0
- package/dist/clis/hackernews/new.yaml +38 -0
- package/dist/clis/hackernews/search.yaml +44 -0
- package/dist/clis/hackernews/show.yaml +38 -0
- package/dist/clis/hackernews/top.yaml +3 -1
- package/dist/clis/hackernews/user.yaml +25 -0
- package/dist/clis/twitter/download.js +13 -97
- package/dist/clis/twitter/thread.js +2 -1
- package/dist/clis/v2ex/member.yaml +29 -0
- package/dist/clis/v2ex/node.yaml +34 -0
- package/dist/clis/v2ex/nodes.yaml +31 -0
- package/dist/clis/v2ex/replies.yaml +32 -0
- package/dist/clis/v2ex/user.yaml +34 -0
- package/dist/clis/weibo/search.d.ts +1 -0
- package/dist/clis/weibo/search.js +73 -0
- package/dist/clis/weixin/download.d.ts +12 -0
- package/dist/clis/weixin/download.js +183 -0
- package/dist/clis/xiaohongshu/download.js +12 -60
- package/dist/clis/xiaohongshu/publish.d.ts +18 -0
- package/dist/clis/xiaohongshu/publish.js +352 -0
- package/dist/clis/xiaohongshu/search.js +47 -15
- package/dist/clis/xiaohongshu/search.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/search.test.js +114 -0
- package/dist/clis/yollomi/background.d.ts +4 -0
- package/dist/clis/yollomi/background.js +45 -0
- package/dist/clis/yollomi/edit.d.ts +5 -0
- package/dist/clis/yollomi/edit.js +56 -0
- package/dist/clis/yollomi/face-swap.d.ts +5 -0
- package/dist/clis/yollomi/face-swap.js +43 -0
- package/dist/clis/yollomi/generate.d.ts +9 -0
- package/dist/clis/yollomi/generate.js +100 -0
- package/dist/clis/yollomi/models.d.ts +1 -0
- package/dist/clis/yollomi/models.js +33 -0
- package/dist/clis/yollomi/object-remover.d.ts +4 -0
- package/dist/clis/yollomi/object-remover.js +42 -0
- package/dist/clis/yollomi/remove-bg.d.ts +4 -0
- package/dist/clis/yollomi/remove-bg.js +38 -0
- package/dist/clis/yollomi/restore.d.ts +4 -0
- package/dist/clis/yollomi/restore.js +38 -0
- package/dist/clis/yollomi/try-on.d.ts +4 -0
- package/dist/clis/yollomi/try-on.js +46 -0
- package/dist/clis/yollomi/upload.d.ts +7 -0
- package/dist/clis/yollomi/upload.js +71 -0
- package/dist/clis/yollomi/upscale.d.ts +4 -0
- package/dist/clis/yollomi/upscale.js +53 -0
- package/dist/clis/yollomi/utils.d.ts +45 -0
- package/dist/clis/yollomi/utils.js +180 -0
- package/dist/clis/yollomi/video.d.ts +5 -0
- package/dist/clis/yollomi/video.js +56 -0
- package/dist/clis/zhihu/download.d.ts +1 -5
- package/dist/clis/zhihu/download.js +20 -126
- package/dist/clis/zhihu/download.test.js +7 -5
- package/dist/clis/zhihu/question.js +2 -1
- package/dist/commanderAdapter.js +4 -6
- package/dist/daemon.js +5 -2
- package/dist/discovery.js +10 -10
- package/dist/download/article-download.d.ts +59 -0
- package/dist/download/article-download.js +178 -0
- package/dist/download/media-download.d.ts +49 -0
- package/dist/download/media-download.js +112 -0
- package/dist/errors.d.ts +23 -2
- package/dist/errors.js +58 -2
- package/dist/errors.test.d.ts +1 -0
- package/dist/errors.test.js +59 -0
- package/dist/execution.js +9 -10
- package/dist/explore.js +4 -2
- package/dist/external.d.ts +15 -0
- package/dist/external.js +48 -2
- package/dist/external.test.d.ts +1 -0
- package/dist/external.test.js +64 -0
- package/dist/main.js +10 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.js +45 -23
- package/dist/plugin.test.js +6 -1
- package/dist/record.d.ts +47 -0
- package/dist/record.js +545 -0
- package/dist/registry.d.ts +7 -2
- package/dist/registry.js +2 -6
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +10 -3
- package/dist/validate.js +1 -3
- package/docs/.vitepress/config.mts +1 -0
- package/docs/adapters/browser/doubao.md +35 -0
- package/docs/adapters/browser/hackernews.md +20 -4
- package/docs/adapters/browser/tiktok.md +1 -1
- package/docs/adapters/browser/v2ex.md +31 -10
- package/docs/adapters/browser/weibo.md +4 -0
- package/docs/adapters/browser/weixin.md +33 -0
- package/docs/adapters/browser/xiaohongshu.md +8 -6
- package/docs/adapters/browser/yollomi.md +69 -0
- package/docs/adapters/desktop/doubao-app.md +35 -0
- package/docs/adapters/index.md +16 -5
- package/docs/advanced/download.md +4 -0
- package/package.json +3 -1
- package/src/browser/errors.ts +17 -11
- package/src/build-manifest.ts +2 -3
- package/src/cli.ts +45 -2
- package/src/clis/bilibili/download.ts +25 -83
- package/src/clis/bilibili/utils.ts +2 -1
- package/src/clis/chaoxing/assignments.ts +2 -1
- package/src/clis/doubao/ask.ts +40 -0
- package/src/clis/doubao/common.ts +619 -0
- package/src/clis/doubao/new.ts +22 -0
- package/src/clis/doubao/read.ts +20 -0
- package/src/clis/doubao/send.ts +25 -0
- package/src/clis/doubao/status.ts +27 -0
- package/src/clis/doubao-app/ask.ts +60 -0
- package/src/clis/doubao-app/common.ts +116 -0
- package/src/clis/doubao-app/dump.ts +28 -0
- package/src/clis/doubao-app/new.ts +21 -0
- package/src/clis/doubao-app/read.ts +21 -0
- package/src/clis/doubao-app/screenshot.ts +19 -0
- package/src/clis/doubao-app/send.ts +30 -0
- package/src/clis/doubao-app/status.ts +17 -0
- package/src/clis/hackernews/ask.yaml +38 -0
- package/src/clis/hackernews/best.yaml +38 -0
- package/src/clis/hackernews/jobs.yaml +36 -0
- package/src/clis/hackernews/new.yaml +38 -0
- package/src/clis/hackernews/search.yaml +44 -0
- package/src/clis/hackernews/show.yaml +38 -0
- package/src/clis/hackernews/top.yaml +3 -1
- package/src/clis/hackernews/user.yaml +25 -0
- package/src/clis/twitter/download.ts +13 -111
- package/src/clis/twitter/thread.ts +2 -1
- package/src/clis/v2ex/member.yaml +29 -0
- package/src/clis/v2ex/node.yaml +34 -0
- package/src/clis/v2ex/nodes.yaml +31 -0
- package/src/clis/v2ex/replies.yaml +32 -0
- package/src/clis/v2ex/user.yaml +34 -0
- package/src/clis/weibo/search.ts +78 -0
- package/src/clis/weixin/download.ts +199 -0
- package/src/clis/xiaohongshu/download.ts +12 -71
- package/src/clis/xiaohongshu/publish.ts +392 -0
- package/src/clis/xiaohongshu/search.test.ts +134 -0
- package/src/clis/xiaohongshu/search.ts +49 -15
- package/src/clis/yollomi/background.ts +48 -0
- package/src/clis/yollomi/edit.ts +58 -0
- package/src/clis/yollomi/face-swap.ts +45 -0
- package/src/clis/yollomi/generate.ts +95 -0
- package/src/clis/yollomi/models.ts +38 -0
- package/src/clis/yollomi/object-remover.ts +44 -0
- package/src/clis/yollomi/remove-bg.ts +40 -0
- package/src/clis/yollomi/restore.ts +40 -0
- package/src/clis/yollomi/try-on.ts +48 -0
- package/src/clis/yollomi/upload.ts +78 -0
- package/src/clis/yollomi/upscale.ts +49 -0
- package/src/clis/yollomi/utils.ts +202 -0
- package/src/clis/yollomi/video.ts +61 -0
- package/src/clis/zhihu/download.test.ts +7 -5
- package/src/clis/zhihu/download.ts +23 -158
- package/src/clis/zhihu/question.ts +2 -1
- package/src/commanderAdapter.ts +4 -7
- package/src/daemon.ts +5 -2
- package/src/discovery.ts +26 -26
- package/src/download/article-download.ts +272 -0
- package/src/download/media-download.ts +178 -0
- package/src/errors.test.ts +79 -0
- package/src/errors.ts +92 -2
- package/src/execution.ts +14 -10
- package/src/explore.ts +4 -2
- package/src/external.test.ts +88 -0
- package/src/external.ts +56 -2
- package/src/generate.ts +2 -1
- package/src/main.ts +10 -0
- package/src/plugin.test.ts +7 -1
- package/src/plugin.ts +49 -25
- package/src/record.ts +617 -0
- package/src/registry.ts +9 -5
- package/src/runtime.ts +16 -4
- package/src/validate.ts +2 -3
- package/tests/e2e/browser-auth.test.ts +10 -1
- package/tests/e2e/browser-public.test.ts +13 -8
- package/tests/e2e/public-commands.test.ts +209 -21
- package/tests/smoke/api-health.test.ts +65 -6
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi image upscaling — POST /api/ai/image-upscaler
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
import { CliError } from '../../errors.js';
|
|
9
|
+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
|
|
10
|
+
|
|
11
|
+
cli({
|
|
12
|
+
site: 'yollomi',
|
|
13
|
+
name: 'upscale',
|
|
14
|
+
description: 'Upscale image resolution with AI (1 credit)',
|
|
15
|
+
domain: YOLLOMI_DOMAIN,
|
|
16
|
+
strategy: Strategy.COOKIE,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'image', positional: true, required: true, help: 'Image URL to upscale' },
|
|
19
|
+
{ name: 'scale', default: '2', choices: ['2', '4'], help: 'Upscale factor (2 or 4)' },
|
|
20
|
+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
|
|
21
|
+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' },
|
|
22
|
+
],
|
|
23
|
+
columns: ['status', 'file', 'size', 'scale', 'url'],
|
|
24
|
+
func: async (page, kwargs) => {
|
|
25
|
+
const scale = parseInt(kwargs.scale as string, 10);
|
|
26
|
+
process.stderr.write(chalk.dim(`Upscaling ${scale}x...\n`));
|
|
27
|
+
const data = await yollomiPost(page, '/api/ai/image-upscaler', {
|
|
28
|
+
imageUrl: kwargs.image as string,
|
|
29
|
+
scale,
|
|
30
|
+
face_enhance: false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const url = data.image || (data.images?.[0]);
|
|
34
|
+
if (!url) throw new CliError('EMPTY_RESPONSE', 'No result', 'Check the input image');
|
|
35
|
+
|
|
36
|
+
if (kwargs['no-download']) return [{ status: 'upscaled', file: '-', size: '-', scale: `${scale}x`, url }];
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const urlPath = (() => { try { return new URL(url).pathname; } catch { return url; } })();
|
|
40
|
+
const ext = urlPath.endsWith('.png') || urlPath.endsWith('.webp') ? urlPath.slice(urlPath.lastIndexOf('.')) : '.jpg';
|
|
41
|
+
const filename = `yollomi_upscale_${scale}x_${Date.now()}${ext}`;
|
|
42
|
+
const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename);
|
|
43
|
+
if (data.remainingCredits !== undefined) process.stderr.write(chalk.dim(`Credits remaining: ${data.remainingCredits}\n`));
|
|
44
|
+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), scale: `${scale}x`, url }];
|
|
45
|
+
} catch {
|
|
46
|
+
return [{ status: 'download-failed', file: '-', size: '-', scale: `${scale}x`, url }];
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi API utilities — browser cookie strategy.
|
|
3
|
+
*
|
|
4
|
+
* Uses the same per-model API routes as the Yollomi frontend:
|
|
5
|
+
* POST /api/ai/<model> — image generation (session cookie auth)
|
|
6
|
+
* POST /api/ai/video — video generation (session cookie auth)
|
|
7
|
+
*
|
|
8
|
+
* Auth: browser session cookies from NextAuth — just log in to yollomi.com in Chrome.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import type { IPage } from '../../types.js';
|
|
14
|
+
import { CliError } from '../../errors.js';
|
|
15
|
+
|
|
16
|
+
export const YOLLOMI_DOMAIN = 'yollomi.com';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ensure the browser tab is on yollomi.com.
|
|
20
|
+
* The framework pre-nav sometimes silently fails, leaving the page on about:blank.
|
|
21
|
+
*/
|
|
22
|
+
export async function ensureOnYollomi(page: IPage): Promise<void> {
|
|
23
|
+
const currentUrl = await page.evaluate(`(() => location.href)()`) as string;
|
|
24
|
+
if (!currentUrl || !currentUrl.includes('yollomi.com')) {
|
|
25
|
+
await page.goto('https://yollomi.com');
|
|
26
|
+
await page.wait(3);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* POST to a Yollomi /api/ai/* route via the browser session.
|
|
32
|
+
* Uses relative paths (e.g. `/api/ai/flux`) — same as the frontend.
|
|
33
|
+
*/
|
|
34
|
+
export async function yollomiPost(page: IPage, apiPath: string, body: Record<string, unknown>): Promise<any> {
|
|
35
|
+
const bodyJson = JSON.stringify(body);
|
|
36
|
+
await ensureOnYollomi(page);
|
|
37
|
+
|
|
38
|
+
const result = await page.evaluate(`
|
|
39
|
+
(async () => {
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(${JSON.stringify(apiPath)}, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
credentials: 'include',
|
|
45
|
+
body: ${JSON.stringify(bodyJson)},
|
|
46
|
+
});
|
|
47
|
+
const text = await res.text();
|
|
48
|
+
return { ok: res.ok, status: res.status, body: text };
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return { ok: false, status: 0, body: err.message || 'fetch failed (on ' + location.href + ')' };
|
|
51
|
+
}
|
|
52
|
+
})()
|
|
53
|
+
`);
|
|
54
|
+
|
|
55
|
+
if (!result || result.status === 0) {
|
|
56
|
+
throw new CliError(
|
|
57
|
+
'FETCH_ERROR',
|
|
58
|
+
`Network error: ${result?.body || 'Failed to fetch'}`,
|
|
59
|
+
'Make sure Chrome is logged in to https://yollomi.com and the Browser Bridge is running',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
let detail = result.body;
|
|
65
|
+
try { detail = JSON.parse(result.body)?.error || JSON.parse(result.body)?.message || result.body; } catch {}
|
|
66
|
+
throw new CliError(
|
|
67
|
+
'API_ERROR',
|
|
68
|
+
`Yollomi API ${result.status}: ${detail}`,
|
|
69
|
+
result.status === 401
|
|
70
|
+
? 'Not logged in — open Chrome, go to https://yollomi.com and log in'
|
|
71
|
+
: result.status === 402
|
|
72
|
+
? 'Insufficient credits — top up at https://yollomi.com/pricing'
|
|
73
|
+
: result.status === 429
|
|
74
|
+
? 'Rate limited — wait a moment and retry'
|
|
75
|
+
: 'Check the model and parameters',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(result.body);
|
|
81
|
+
} catch {
|
|
82
|
+
throw new CliError('API_ERROR', 'Invalid JSON response', 'Try again');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolve an image input: local file → base64 data URL, URL → as-is.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveImageInput(input: string): string {
|
|
90
|
+
if (input.startsWith('http://') || input.startsWith('https://') || input.startsWith('data:')) {
|
|
91
|
+
return input;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const resolved = path.resolve(input);
|
|
95
|
+
if (!fs.existsSync(resolved)) {
|
|
96
|
+
throw new CliError('FILE_NOT_FOUND', `File not found: ${resolved}`, 'Provide a valid file path or URL');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
100
|
+
const mimeMap: Record<string, string> = {
|
|
101
|
+
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
102
|
+
'.png': 'image/png', '.gif': 'image/gif',
|
|
103
|
+
'.webp': 'image/webp', '.bmp': 'image/bmp',
|
|
104
|
+
};
|
|
105
|
+
const mime = mimeMap[ext] || 'image/png';
|
|
106
|
+
const data = fs.readFileSync(resolved);
|
|
107
|
+
return `data:${mime};base64,${data.toString('base64')}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function downloadOutput(
|
|
111
|
+
url: string, outputDir: string, filename: string,
|
|
112
|
+
): Promise<{ path: string; size: number }> {
|
|
113
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
114
|
+
const destPath = path.join(outputDir, filename);
|
|
115
|
+
const resp = await fetch(url);
|
|
116
|
+
if (!resp.ok) throw new CliError('DOWNLOAD_ERROR', `Download failed: HTTP ${resp.status}`, 'URL may have expired');
|
|
117
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
118
|
+
fs.writeFileSync(destPath, buffer);
|
|
119
|
+
return { path: destPath, size: buffer.length };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function fmtBytes(bytes: number): string {
|
|
123
|
+
if (bytes === 0) return '0 B';
|
|
124
|
+
const k = 1024;
|
|
125
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
126
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
127
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Per-model API route mapping (matches frontend model.apiEndpoint). */
|
|
131
|
+
export const MODEL_ROUTES: Record<string, string> = {
|
|
132
|
+
'flux': '/api/ai/flux',
|
|
133
|
+
'flux-schnell': '/api/ai/flux-schnell',
|
|
134
|
+
'flux-2-pro': '/api/ai/flux-2-pro',
|
|
135
|
+
'flux-kontext-pro': '/api/ai/flux-kontext-pro',
|
|
136
|
+
'nano-banana': '/api/ai/nano-banana',
|
|
137
|
+
'nano-banana-pro': '/api/ai/nano-banana-pro',
|
|
138
|
+
'nano-banana-2': '/api/ai/nano-banana-2',
|
|
139
|
+
'z-image-turbo': '/api/ai/z-image-turbo',
|
|
140
|
+
'imagen-4-ultra': '/api/ai/imagen-4-ultra',
|
|
141
|
+
'imagen-4-fast': '/api/ai/imagen-4-fast',
|
|
142
|
+
'ideogram-v3-turbo': '/api/ai/ideogram-v3-turbo',
|
|
143
|
+
'stable-diffusion-3-5-large':'/api/ai/stable-diffusion-3-5-large',
|
|
144
|
+
'seedream-4-5': '/api/ai/seedream-4-5',
|
|
145
|
+
'seedream-5-lite': '/api/ai/seedream-5-lite',
|
|
146
|
+
'qwen-image-edit': '/api/ai/qwen-image-edit',
|
|
147
|
+
'qwen-image-edit-plus': '/api/ai/qwen-image-edit-plus',
|
|
148
|
+
'remove-bg': '/api/ai/remove-bg',
|
|
149
|
+
'image-upscaler': '/api/ai/image-upscaler',
|
|
150
|
+
'face-swap': '/api/ai/face-swap',
|
|
151
|
+
'virtual-try-on': '/api/ai/virtual-try-on',
|
|
152
|
+
'photo-restoration': '/api/ai/photo-restoration',
|
|
153
|
+
'ai-background-generator': '/api/ai/ai-background-generator',
|
|
154
|
+
'object-remover': '/api/ai/object-remover',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Well-known image model IDs and their credit costs. */
|
|
158
|
+
export const IMAGE_MODELS: Record<string, { credits: number; description: string }> = {
|
|
159
|
+
'z-image-turbo': { credits: 1, description: 'Alibaba Qwen turbo (cheapest, 1 credit)' },
|
|
160
|
+
'flux-schnell': { credits: 2, description: 'High-speed Flux generation' },
|
|
161
|
+
'ideogram-v3-turbo': { credits: 3, description: 'Ideogram V3 Turbo' },
|
|
162
|
+
'imagen-4-fast': { credits: 3, description: 'Google Imagen 4 Fast' },
|
|
163
|
+
'seedream-4-5': { credits: 4, description: 'Seedream 4.5 (ByteDance)' },
|
|
164
|
+
'seedream-5-lite': { credits: 4, description: 'Seedream 5 Lite — 2K/3K' },
|
|
165
|
+
'flux': { credits: 4, description: 'Flux 1.1 Pro' },
|
|
166
|
+
'nano-banana': { credits: 4, description: 'Google Nano Banana' },
|
|
167
|
+
'flux-kontext-pro': { credits: 4, description: 'Flux Kontext Pro (img2img)' },
|
|
168
|
+
'imagen-4-ultra': { credits: 6, description: 'Google Imagen 4 Ultra' },
|
|
169
|
+
'nano-banana-2': { credits: 7, description: 'Google Nano Banana 2' },
|
|
170
|
+
'stable-diffusion-3-5-large':{ credits: 7, description: 'Stable Diffusion 3.5 Large' },
|
|
171
|
+
'nano-banana-pro': { credits: 15, description: 'Nano Banana Pro' },
|
|
172
|
+
'flux-2-pro': { credits: 15, description: 'Flux 2 Pro (premium)' },
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const VIDEO_MODELS: Record<string, { credits: number; description: string }> = {
|
|
176
|
+
'kling-v2-6-motion-control': { credits: 7, description: 'Kling v2.6 Motion Control' },
|
|
177
|
+
'bytedance-seedance-1-pro-fast': { credits: 8, description: 'Seedance 1.0 Pro Fast' },
|
|
178
|
+
'kling-2-1': { credits: 9, description: 'Kling 2.1' },
|
|
179
|
+
'minimax-hailuo-2-3': { credits: 9, description: 'Hailuo 2.3' },
|
|
180
|
+
'pixverse-5': { credits: 9, description: 'PixVerse 5' },
|
|
181
|
+
'wan-2-5-t2v': { credits: 9, description: 'Wan 2.5 Text-to-Video' },
|
|
182
|
+
'wan-2-5-i2v': { credits: 9, description: 'Wan 2.5 Image-to-Video' },
|
|
183
|
+
'google-veo-3-fast': { credits: 9, description: 'Google Veo 3 Fast' },
|
|
184
|
+
'google-veo-3-1-fast': { credits: 9, description: 'Google Veo 3.1 Fast' },
|
|
185
|
+
'openai-sora-2': { credits: 10, description: 'Sora 2' },
|
|
186
|
+
'google-veo-3': { credits: 10, description: 'Google Veo 3' },
|
|
187
|
+
'google-veo-3-1': { credits: 10, description: 'Google Veo 3.1' },
|
|
188
|
+
'wan-2-6-t2v': { credits: 29, description: 'Wan 2.6 T2V (premium)' },
|
|
189
|
+
'wan-2-6-i2v': { credits: 29, description: 'Wan 2.6 I2V (premium)' },
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const TOOL_MODELS: Record<string, { credits: number; description: string }> = {
|
|
193
|
+
'remove-bg': { credits: 0, description: 'Remove background (free)' },
|
|
194
|
+
'image-upscaler': { credits: 1, description: 'Enhance image resolution' },
|
|
195
|
+
'object-remover': { credits: 3, description: 'Remove unwanted objects' },
|
|
196
|
+
'face-swap': { credits: 3, description: 'Swap faces in photos' },
|
|
197
|
+
'virtual-try-on': { credits: 3, description: 'Try clothes on photos' },
|
|
198
|
+
'qwen-image-edit': { credits: 3, description: 'Edit image with text prompt' },
|
|
199
|
+
'qwen-image-edit-plus': { credits: 3, description: 'Advanced image editing' },
|
|
200
|
+
'photo-restoration': { credits: 4, description: 'Revive old/damaged photos' },
|
|
201
|
+
'ai-background-generator':{ credits: 5, description: 'Generate custom backgrounds' },
|
|
202
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi video generation — POST /api/ai/video
|
|
3
|
+
* Matches the frontend video-generator.tsx request format exactly.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { cli, Strategy } from '../../registry.js';
|
|
9
|
+
import { CliError } from '../../errors.js';
|
|
10
|
+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
|
|
11
|
+
|
|
12
|
+
cli({
|
|
13
|
+
site: 'yollomi',
|
|
14
|
+
name: 'video',
|
|
15
|
+
description: 'Generate videos with AI (text-to-video or image-to-video)',
|
|
16
|
+
domain: YOLLOMI_DOMAIN,
|
|
17
|
+
strategy: Strategy.COOKIE,
|
|
18
|
+
args: [
|
|
19
|
+
{ name: 'prompt', positional: true, required: true, help: 'Text prompt describing the video' },
|
|
20
|
+
{ name: 'model', default: 'kling-2-1', help: 'Model (kling-2-1, openai-sora-2, google-veo-3-1, wan-2-5-t2v, ...)' },
|
|
21
|
+
{ name: 'image', help: 'Input image URL for image-to-video' },
|
|
22
|
+
{ name: 'ratio', default: '16:9', choices: ['1:1', '16:9', '9:16', '4:3', '3:4'], help: 'Aspect ratio' },
|
|
23
|
+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
|
|
24
|
+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL, skip download' },
|
|
25
|
+
],
|
|
26
|
+
columns: ['status', 'file', 'size', 'credits', 'url'],
|
|
27
|
+
func: async (page, kwargs) => {
|
|
28
|
+
const prompt = kwargs.prompt as string;
|
|
29
|
+
const modelId = kwargs.model as string;
|
|
30
|
+
|
|
31
|
+
const inputs: Record<string, unknown> = {
|
|
32
|
+
aspect_ratio: kwargs.ratio as string,
|
|
33
|
+
};
|
|
34
|
+
if (kwargs.image) inputs.image = kwargs.image as string;
|
|
35
|
+
|
|
36
|
+
const body = { modelId, prompt, inputs };
|
|
37
|
+
|
|
38
|
+
process.stderr.write(chalk.dim(`Generating video with ${modelId} (may take a while)...\n`));
|
|
39
|
+
const data = await yollomiPost(page, '/api/ai/video', body);
|
|
40
|
+
|
|
41
|
+
const videoUrl: string = data.video || '';
|
|
42
|
+
if (!videoUrl) throw new CliError('EMPTY_RESPONSE', 'No video returned', 'Try a different prompt or model');
|
|
43
|
+
|
|
44
|
+
const credits = data.remainingCredits;
|
|
45
|
+
const noDownload = kwargs['no-download'] as boolean;
|
|
46
|
+
const outputDir = kwargs.output as string;
|
|
47
|
+
|
|
48
|
+
if (noDownload) {
|
|
49
|
+
return [{ status: 'generated', file: '-', size: '-', credits: credits ?? '-', url: videoUrl }];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const filename = `yollomi_${modelId}_${Date.now()}.mp4`;
|
|
54
|
+
const { path: fp, size } = await downloadOutput(videoUrl, outputDir, filename);
|
|
55
|
+
if (credits !== undefined) process.stderr.write(chalk.dim(`Credits remaining: ${credits}\n`));
|
|
56
|
+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), credits: credits ?? '-', url: videoUrl }];
|
|
57
|
+
} catch {
|
|
58
|
+
return [{ status: 'download-failed', file: '-', size: '-', credits: credits ?? '-', url: videoUrl }];
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import
|
|
2
|
+
import TurndownService from 'turndown';
|
|
3
3
|
|
|
4
|
-
describe('
|
|
4
|
+
describe('article markdown conversion', () => {
|
|
5
5
|
it('renders ordered lists with the original list item content', () => {
|
|
6
6
|
const html = '<ol><li>First item</li><li>Second item</li></ol>';
|
|
7
|
+
const td = new TurndownService({ headingStyle: 'atx', bulletListMarker: '-' });
|
|
8
|
+
const md = td.turndown(html);
|
|
7
9
|
|
|
8
|
-
expect(
|
|
9
|
-
expect(
|
|
10
|
-
expect(
|
|
10
|
+
expect(md).toMatch(/1\.\s+First item/);
|
|
11
|
+
expect(md).toMatch(/2\.\s+Second item/);
|
|
12
|
+
expect(md).not.toContain('$1');
|
|
11
13
|
});
|
|
12
14
|
});
|
|
@@ -5,85 +5,8 @@
|
|
|
5
5
|
* opencli zhihu download --url "https://zhuanlan.zhihu.com/p/xxx" --output ./zhihu
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import * as fs from 'node:fs';
|
|
9
|
-
import * as path from 'node:path';
|
|
10
8
|
import { cli, Strategy } from '../../registry.js';
|
|
11
|
-
import {
|
|
12
|
-
import { formatBytes } from '../../download/progress.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Convert HTML content to Markdown.
|
|
16
|
-
* This is a simplified converter for Zhihu article content.
|
|
17
|
-
*/
|
|
18
|
-
export function htmlToMarkdown(html: string): string {
|
|
19
|
-
let md = html;
|
|
20
|
-
|
|
21
|
-
// Remove script and style tags
|
|
22
|
-
md = md.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
23
|
-
md = md.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
24
|
-
|
|
25
|
-
// Convert headers
|
|
26
|
-
md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n');
|
|
27
|
-
md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n');
|
|
28
|
-
md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n');
|
|
29
|
-
md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '#### $1\n\n');
|
|
30
|
-
|
|
31
|
-
// Convert paragraphs
|
|
32
|
-
md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, '$1\n\n');
|
|
33
|
-
|
|
34
|
-
// Convert links
|
|
35
|
-
md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)');
|
|
36
|
-
|
|
37
|
-
// Convert images
|
|
38
|
-
md = md.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, '');
|
|
39
|
-
md = md.replace(/<img[^>]*src="([^"]*)"[^>]*\/?>/gi, '');
|
|
40
|
-
|
|
41
|
-
// Convert lists
|
|
42
|
-
md = md.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (match, content) => {
|
|
43
|
-
return content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, '- $1\n') + '\n';
|
|
44
|
-
});
|
|
45
|
-
md = md.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, (match, content) => {
|
|
46
|
-
let index = 0;
|
|
47
|
-
return content.replace(
|
|
48
|
-
/<li[^>]*>([\s\S]*?)<\/li>/gi,
|
|
49
|
-
(_itemMatch: string, itemContent: string) => `${++index}. ${itemContent}\n`,
|
|
50
|
-
) + '\n';
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Convert bold and italic
|
|
54
|
-
md = md.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**');
|
|
55
|
-
md = md.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**');
|
|
56
|
-
md = md.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*');
|
|
57
|
-
md = md.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*');
|
|
58
|
-
|
|
59
|
-
// Convert code blocks
|
|
60
|
-
md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, '```\n$1\n```\n\n');
|
|
61
|
-
md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`');
|
|
62
|
-
|
|
63
|
-
// Convert blockquotes
|
|
64
|
-
md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (match, content) => {
|
|
65
|
-
return content.split('\n').map((line: string) => `> ${line}`).join('\n') + '\n\n';
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Convert line breaks
|
|
69
|
-
md = md.replace(/<br\s*\/?>/gi, '\n');
|
|
70
|
-
|
|
71
|
-
// Remove remaining HTML tags
|
|
72
|
-
md = md.replace(/<[^>]+>/g, '');
|
|
73
|
-
|
|
74
|
-
// Decode HTML entities
|
|
75
|
-
md = md.replace(/ /g, ' ');
|
|
76
|
-
md = md.replace(/</g, '<');
|
|
77
|
-
md = md.replace(/>/g, '>');
|
|
78
|
-
md = md.replace(/&/g, '&');
|
|
79
|
-
md = md.replace(/"/g, '"');
|
|
80
|
-
|
|
81
|
-
// Clean up extra whitespace
|
|
82
|
-
md = md.replace(/\n{3,}/g, '\n\n');
|
|
83
|
-
md = md.trim();
|
|
84
|
-
|
|
85
|
-
return md;
|
|
86
|
-
}
|
|
9
|
+
import { downloadArticle } from '../../download/article-download.js';
|
|
87
10
|
|
|
88
11
|
cli({
|
|
89
12
|
site: 'zhihu',
|
|
@@ -92,18 +15,17 @@ cli({
|
|
|
92
15
|
domain: 'zhuanlan.zhihu.com',
|
|
93
16
|
strategy: Strategy.COOKIE,
|
|
94
17
|
args: [
|
|
95
|
-
{ name: 'url', required: true,
|
|
18
|
+
{ name: 'url', required: true, help: 'Article URL (zhuanlan.zhihu.com/p/xxx)' },
|
|
96
19
|
{ name: 'output', default: './zhihu-articles', help: 'Output directory' },
|
|
97
20
|
{ name: 'download-images', type: 'boolean', default: false, help: 'Download images locally' },
|
|
98
21
|
],
|
|
99
|
-
columns: ['title', 'author', 'status', 'size'],
|
|
22
|
+
columns: ['title', 'author', 'publish_time', 'status', 'size'],
|
|
100
23
|
func: async (page, kwargs) => {
|
|
101
24
|
const url = kwargs.url;
|
|
102
|
-
const output = kwargs.output;
|
|
103
|
-
const downloadImages = kwargs['download-images'];
|
|
104
25
|
|
|
105
26
|
// Navigate to article page
|
|
106
27
|
await page.goto(url);
|
|
28
|
+
await page.wait(3);
|
|
107
29
|
|
|
108
30
|
// Extract article content
|
|
109
31
|
const data = await page.evaluate(`
|
|
@@ -111,9 +33,9 @@ cli({
|
|
|
111
33
|
const result = {
|
|
112
34
|
title: '',
|
|
113
35
|
author: '',
|
|
114
|
-
content: '',
|
|
115
36
|
publishTime: '',
|
|
116
|
-
|
|
37
|
+
contentHtml: '',
|
|
38
|
+
imageUrls: []
|
|
117
39
|
};
|
|
118
40
|
|
|
119
41
|
// Get title
|
|
@@ -131,13 +53,13 @@ cli({
|
|
|
131
53
|
// Get content HTML
|
|
132
54
|
const contentEl = document.querySelector('.Post-RichTextContainer, .RichText, .ArticleContent');
|
|
133
55
|
if (contentEl) {
|
|
134
|
-
result.
|
|
56
|
+
result.contentHtml = contentEl.innerHTML;
|
|
135
57
|
|
|
136
58
|
// Extract image URLs
|
|
137
59
|
contentEl.querySelectorAll('img').forEach(img => {
|
|
138
60
|
const src = img.getAttribute('data-original') || img.getAttribute('data-actualsrc') || img.src;
|
|
139
61
|
if (src && !src.includes('data:image')) {
|
|
140
|
-
result.
|
|
62
|
+
result.imageUrls.push(src);
|
|
141
63
|
}
|
|
142
64
|
});
|
|
143
65
|
}
|
|
@@ -146,77 +68,20 @@ cli({
|
|
|
146
68
|
})()
|
|
147
69
|
`);
|
|
148
70
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
title: '
|
|
152
|
-
author:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// Create frontmatter
|
|
165
|
-
const frontmatter = [
|
|
166
|
-
'---',
|
|
167
|
-
`title: "${data.title.replace(/"/g, '\\"')}"`,
|
|
168
|
-
`author: "${data.author.replace(/"/g, '\\"')}"`,
|
|
169
|
-
`source: "${url}"`,
|
|
170
|
-
data.publishTime ? `date: "${data.publishTime}"` : '',
|
|
171
|
-
'---',
|
|
172
|
-
'',
|
|
173
|
-
].filter(Boolean).join('\n');
|
|
174
|
-
|
|
175
|
-
// Download images if requested
|
|
176
|
-
if (downloadImages && data.images && data.images.length > 0) {
|
|
177
|
-
const imagesDir = path.join(output, 'images');
|
|
178
|
-
fs.mkdirSync(imagesDir, { recursive: true });
|
|
179
|
-
|
|
180
|
-
const cookies = formatCookieHeader(await page.getCookies({ domain: 'zhihu.com' }));
|
|
181
|
-
|
|
182
|
-
for (let i = 0; i < data.images.length; i++) {
|
|
183
|
-
const imgUrl = data.images[i];
|
|
184
|
-
const ext = imgUrl.match(/\.(jpg|jpeg|png|gif|webp)/i)?.[1] || 'jpg';
|
|
185
|
-
const imgFilename = `img_${i + 1}.${ext}`;
|
|
186
|
-
const imgPath = path.join(imagesDir, imgFilename);
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
await httpDownload(imgUrl, imgPath, {
|
|
190
|
-
cookies,
|
|
191
|
-
timeout: 30000,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// Replace image URL in markdown with local path
|
|
195
|
-
markdown = markdown.replace(
|
|
196
|
-
new RegExp(imgUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
|
|
197
|
-
`./images/${imgFilename}`,
|
|
198
|
-
);
|
|
199
|
-
} catch {
|
|
200
|
-
// Keep original URL if download fails
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Write markdown file
|
|
206
|
-
const safeTitle = sanitizeFilename(data.title, 100);
|
|
207
|
-
const filename = `${safeTitle}.md`;
|
|
208
|
-
const filePath = path.join(output, filename);
|
|
209
|
-
|
|
210
|
-
const fullContent = frontmatter + '\n' + markdown;
|
|
211
|
-
fs.writeFileSync(filePath, fullContent, 'utf-8');
|
|
212
|
-
|
|
213
|
-
const size = Buffer.byteLength(fullContent, 'utf-8');
|
|
214
|
-
|
|
215
|
-
return [{
|
|
216
|
-
title: data.title,
|
|
217
|
-
author: data.author,
|
|
218
|
-
status: 'success',
|
|
219
|
-
size: formatBytes(size),
|
|
220
|
-
}];
|
|
71
|
+
return downloadArticle(
|
|
72
|
+
{
|
|
73
|
+
title: data?.title || '',
|
|
74
|
+
author: data?.author,
|
|
75
|
+
publishTime: data?.publishTime,
|
|
76
|
+
sourceUrl: url,
|
|
77
|
+
contentHtml: data?.contentHtml || '',
|
|
78
|
+
imageUrls: data?.imageUrls,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
output: kwargs.output,
|
|
82
|
+
downloadImages: kwargs['download-images'],
|
|
83
|
+
imageHeaders: { Referer: 'https://zhuanlan.zhihu.com/' },
|
|
84
|
+
},
|
|
85
|
+
);
|
|
221
86
|
},
|
|
222
87
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { AuthRequiredError } from '../../errors.js';
|
|
2
3
|
|
|
3
4
|
cli({
|
|
4
5
|
site: 'zhihu',
|
|
@@ -31,7 +32,7 @@ cli({
|
|
|
31
32
|
}
|
|
32
33
|
`);
|
|
33
34
|
|
|
34
|
-
if (!result || result.error) throw new
|
|
35
|
+
if (!result || result.error) throw new AuthRequiredError('www.zhihu.com', 'Failed to fetch question data from Zhihu');
|
|
35
36
|
|
|
36
37
|
const answers = (result.answers ?? []).slice(0, Number(limit)).map((a: any, i: number) => ({
|
|
37
38
|
rank: i + 1,
|
package/src/commanderAdapter.ts
CHANGED
|
@@ -16,11 +16,7 @@ import { type CliCommand, fullName, getRegistry } from './registry.js';
|
|
|
16
16
|
import { formatRegistryHelpText } from './serialization.js';
|
|
17
17
|
import { render as renderOutput } from './output.js';
|
|
18
18
|
import { executeCommand } from './execution.js';
|
|
19
|
-
import { CliError } from './errors.js';
|
|
20
|
-
|
|
21
|
-
function getErrorMessage(error: unknown): string {
|
|
22
|
-
return error instanceof Error ? error.message : String(error);
|
|
23
|
-
}
|
|
19
|
+
import { CliError, ERROR_ICONS, getErrorMessage } from './errors.js';
|
|
24
20
|
|
|
25
21
|
/**
|
|
26
22
|
* Register a single CliCommand as a Commander subcommand.
|
|
@@ -90,8 +86,9 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
90
86
|
});
|
|
91
87
|
} catch (err) {
|
|
92
88
|
if (err instanceof CliError) {
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
const icon = ERROR_ICONS[err.code] ?? '⚠️';
|
|
90
|
+
console.error(chalk.red(`${icon} ${err.message}`));
|
|
91
|
+
if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
|
|
95
92
|
} else if (optionsRecord.verbose === true && err instanceof Error && err.stack) {
|
|
96
93
|
console.error(chalk.red(err.stack));
|
|
97
94
|
} else {
|
package/src/daemon.ts
CHANGED
|
@@ -150,11 +150,14 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
|
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
const timeoutMs = typeof body.timeout === 'number' && body.timeout > 0
|
|
154
|
+
? body.timeout * 1000
|
|
155
|
+
: 120000;
|
|
153
156
|
const result = await new Promise<unknown>((resolve, reject) => {
|
|
154
157
|
const timer = setTimeout(() => {
|
|
155
158
|
pending.delete(body.id);
|
|
156
|
-
reject(new Error(
|
|
157
|
-
},
|
|
159
|
+
reject(new Error(`Command timeout (${timeoutMs / 1000}s)`));
|
|
160
|
+
}, timeoutMs);
|
|
158
161
|
pending.set(body.id, { resolve, reject, timer });
|
|
159
162
|
extensionWs!.send(JSON.stringify(body));
|
|
160
163
|
});
|