@lemonppt/cli 1.0.7 → 1.0.9
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/dist/cli.js +44 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +61 -5
- package/package.json +6 -5
package/dist/cli.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
// Copyright (c) 2026 lemonforme
|
|
4
4
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
import { createServer as createHttpServer } from 'node:http';
|
|
6
|
+
import { createServer as createHttpsServer } from 'node:https';
|
|
6
7
|
import { spawn } from 'node:child_process';
|
|
7
8
|
import { existsSync } from 'node:fs';
|
|
8
9
|
import { stat, readFile } from 'node:fs/promises';
|
|
9
10
|
import { extname, join, resolve, dirname } from 'node:path';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { generate as generateSelfSignedCert } from 'selfsigned';
|
|
11
13
|
import { exportGoalToPdf, exportGoalToPptx, generateGoalToFile, inspectLayout, listThemes, queryLayouts, readGoalFromFile, renderGoalToDir, scaffoldGoalToFile, stageMediaToFile, validateDeck, validateGoalCopy, validateGoalSpec, writeSafePropsToFile, } from './index.js';
|
|
12
14
|
import { installSkill } from './install-skill.js';
|
|
13
15
|
function printUsage() {
|
|
@@ -15,8 +17,10 @@ function printUsage() {
|
|
|
15
17
|
lemonppt generate "<input>" [--pages N] [--theme <id>] [--language zh|en] [--out goal.json] [--api-key KEY]
|
|
16
18
|
lemonppt render <goal.json> [--out ./output] [--editable]
|
|
17
19
|
lemonppt export <goal.json> --pptx out.pptx [--pdf out.pdf]
|
|
18
|
-
lemonppt serve [<dir>] [--port N]
|
|
19
|
-
|
|
20
|
+
lemonppt serve [<dir>] [--port N] [--https] [--cert <path>] [--key <path>]
|
|
21
|
+
# start API server if built, else static preview
|
|
22
|
+
lemonppt server [<dir>] [--port N] [--https] [--cert <path>] [--key <path>]
|
|
23
|
+
# alias for serve
|
|
20
24
|
lemonppt install-skill [--claude] [--codex] [--cursor] [--all] [--target <dir>]
|
|
21
25
|
|
|
22
26
|
lemonppt list-themes
|
|
@@ -77,7 +81,28 @@ async function startApiServer(dir, port) {
|
|
|
77
81
|
});
|
|
78
82
|
});
|
|
79
83
|
}
|
|
80
|
-
async function
|
|
84
|
+
async function readCertFile(filePath) {
|
|
85
|
+
if (!filePath)
|
|
86
|
+
return undefined;
|
|
87
|
+
const content = await readFile(resolve(filePath), 'utf-8');
|
|
88
|
+
return content;
|
|
89
|
+
}
|
|
90
|
+
async function resolveHttpsCredentials(options) {
|
|
91
|
+
if (!options.https && !options.cert && !options.key)
|
|
92
|
+
return undefined;
|
|
93
|
+
const certFromFile = await readCertFile(options.cert);
|
|
94
|
+
const keyFromFile = await readCertFile(options.key);
|
|
95
|
+
if (certFromFile && keyFromFile) {
|
|
96
|
+
return { cert: certFromFile, key: keyFromFile };
|
|
97
|
+
}
|
|
98
|
+
if (certFromFile || keyFromFile) {
|
|
99
|
+
throw new Error('使用 HTTPS 时必须同时提供 --cert 和 --key,或都不提供以自动生成自签名证书。');
|
|
100
|
+
}
|
|
101
|
+
const attrs = [{ name: 'commonName', value: 'localhost' }];
|
|
102
|
+
const pems = generateSelfSignedCert(attrs, { days: 365, keySize: 2048 });
|
|
103
|
+
return { cert: pems.cert, key: pems.private };
|
|
104
|
+
}
|
|
105
|
+
async function serveDir(dir, port, options = {}) {
|
|
81
106
|
const root = resolve(dir);
|
|
82
107
|
const mimeTypes = {
|
|
83
108
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -95,8 +120,9 @@ async function serveDir(dir, port) {
|
|
|
95
120
|
'.otf': 'font/otf',
|
|
96
121
|
'.eot': 'application/vnd.ms-fontobject',
|
|
97
122
|
};
|
|
98
|
-
const
|
|
99
|
-
const
|
|
123
|
+
const requestHandler = async (req, res) => {
|
|
124
|
+
const protocol = options.https ? 'https' : 'http';
|
|
125
|
+
const url = new URL(req.url || '/', `${protocol}://${req.headers.host}`);
|
|
100
126
|
let pathname = decodeURIComponent(url.pathname);
|
|
101
127
|
if (pathname === '/') {
|
|
102
128
|
// 优先 editable 输出(editor.html),回退到非 editable 输出(index.html)
|
|
@@ -134,10 +160,15 @@ async function serveDir(dir, port) {
|
|
|
134
160
|
res.writeHead(404);
|
|
135
161
|
res.end('Not found');
|
|
136
162
|
}
|
|
137
|
-
}
|
|
163
|
+
};
|
|
164
|
+
const credentials = await resolveHttpsCredentials(options);
|
|
165
|
+
const protocol = credentials ? 'https' : 'http';
|
|
166
|
+
const server = credentials
|
|
167
|
+
? createHttpsServer(credentials, requestHandler)
|
|
168
|
+
: createHttpServer(requestHandler);
|
|
138
169
|
return new Promise((resolve) => {
|
|
139
170
|
server.listen(port, () => {
|
|
140
|
-
console.log(`Serving ${root} at
|
|
171
|
+
console.log(`Serving ${root} at ${protocol}://localhost:${port}`);
|
|
141
172
|
resolve();
|
|
142
173
|
});
|
|
143
174
|
});
|
|
@@ -211,13 +242,18 @@ async function main() {
|
|
|
211
242
|
case 'server': {
|
|
212
243
|
const dir = positional[0] || './output';
|
|
213
244
|
const port = args.options.port ? Number(args.options.port) : 3456;
|
|
245
|
+
const serveOptions = {
|
|
246
|
+
https: args.options.https === true,
|
|
247
|
+
cert: args.options.cert,
|
|
248
|
+
key: args.options.key,
|
|
249
|
+
};
|
|
214
250
|
if (existsSync(apiServerPath)) {
|
|
215
251
|
console.log(`Starting API server on port ${port}...`);
|
|
216
252
|
await startApiServer(dir, port);
|
|
217
253
|
}
|
|
218
254
|
else {
|
|
219
255
|
console.log(`API server not built, falling back to static preview.`);
|
|
220
|
-
await serveDir(dir, port);
|
|
256
|
+
await serveDir(dir, port, serveOptions);
|
|
221
257
|
// 保持进程运行
|
|
222
258
|
await new Promise(() => { });
|
|
223
259
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -68,10 +68,12 @@ export declare function renderGoalToDir(goal: DeckGoal, options?: RenderCliOptio
|
|
|
68
68
|
}>;
|
|
69
69
|
/**
|
|
70
70
|
* 导出 goal 为 PPTX。
|
|
71
|
+
* 若服务端 Chromium 无法启动,会自动生成浏览器兜底驱动页。
|
|
71
72
|
*/
|
|
72
73
|
export declare function exportGoalToPptx(goal: DeckGoal, options: ExportCliOptions): Promise<void>;
|
|
73
74
|
/**
|
|
74
75
|
* 导出 goal 为 PDF。
|
|
76
|
+
* 若服务端 Chromium 无法启动,会自动生成浏览器兜底驱动页。
|
|
75
77
|
*/
|
|
76
78
|
export declare function exportGoalToPdf(goal: DeckGoal, options: ExportCliOptions): Promise<void>;
|
|
77
79
|
/**
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Copyright (c) 2026 lemonforme
|
|
3
3
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
4
|
import { generateGoal } from '@lemonppt/agent-prompts';
|
|
5
|
-
import {
|
|
6
|
-
import { exportDeckToPdf,
|
|
5
|
+
import { preprocessAgentGoal, validateDeckGoal, validateDeckGoalContent, validateSlideCount } from '@lemonppt/core';
|
|
6
|
+
import { exportDeckToPdf, exportDeckToPptxScreenshot, normalizeGoal, renderBrowserExportDriver, renderDeck, renderEditorData, } from '@lemonppt/renderer';
|
|
7
7
|
import { getTheme, themes } from '@lemonppt/themes';
|
|
8
8
|
import { getLayout, getLayoutSchema, listLayoutsByRoleAndTheme } from '@lemonppt/templates';
|
|
9
9
|
import { copyFile, cp, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
@@ -14,6 +14,19 @@ function resolveTheme(themeId) {
|
|
|
14
14
|
const id = themeId || 'theme01';
|
|
15
15
|
return getTheme(id) ? id : 'theme01';
|
|
16
16
|
}
|
|
17
|
+
function isBrowserLaunchError(err) {
|
|
18
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19
|
+
return /launch|browser|chromium|executable|playwright/i.test(message);
|
|
20
|
+
}
|
|
21
|
+
async function copyBrowserExportAssets(assetsDir) {
|
|
22
|
+
await mkdir(assetsDir, { recursive: true });
|
|
23
|
+
const vendorSource = resolvePackagePath('@lemonppt/renderer', 'assets', 'vendor');
|
|
24
|
+
const vendorDest = path.join(assetsDir, 'vendor');
|
|
25
|
+
await cp(vendorSource, vendorDest, { recursive: true, force: true });
|
|
26
|
+
const bundleSource = resolvePackagePath('@lemonppt/renderer', 'dist', 'client', 'browser-export.js');
|
|
27
|
+
const bundleDest = path.join(assetsDir, 'browser-export.js');
|
|
28
|
+
await copyFile(bundleSource, bundleDest);
|
|
29
|
+
}
|
|
17
30
|
export async function copyThemeAssets(themeId, assetsDir) {
|
|
18
31
|
const theme = resolveTheme(themeId);
|
|
19
32
|
await mkdir(assetsDir, { recursive: true });
|
|
@@ -40,6 +53,13 @@ export async function copyThemeAssets(themeId, assetsDir) {
|
|
|
40
53
|
const editorScriptSource = resolvePackagePath('@lemonppt/renderer', 'dist', 'client', 'editor-script.js');
|
|
41
54
|
const editorScriptDest = path.join(assetsDir, 'editor-script.js');
|
|
42
55
|
await copyFile(editorScriptSource, editorScriptDest);
|
|
56
|
+
// 复制浏览器端导出兜底脚本及 vendor bundle(沙箱环境无法启动 Chromium 时使用)
|
|
57
|
+
const browserExportSource = resolvePackagePath('@lemonppt/renderer', 'dist', 'client', 'browser-export.js');
|
|
58
|
+
const browserExportDest = path.join(assetsDir, 'browser-export.js');
|
|
59
|
+
await copyFile(browserExportSource, browserExportDest);
|
|
60
|
+
const vendorDirSource = resolvePackagePath('@lemonppt/renderer', 'assets', 'vendor');
|
|
61
|
+
const vendorDirDest = path.join(assetsDir, 'vendor');
|
|
62
|
+
await cp(vendorDirSource, vendorDirDest, { recursive: true, force: true });
|
|
43
63
|
}
|
|
44
64
|
/**
|
|
45
65
|
* 把本地媒体文件复制到输出目录的 media/ 下,供 goal.json 引用。
|
|
@@ -101,7 +121,7 @@ export async function readGoalFromFile(filePath) {
|
|
|
101
121
|
const raw = await readFile(path.resolve(filePath), 'utf-8');
|
|
102
122
|
const parsed = preprocessAgentGoal(JSON.parse(raw));
|
|
103
123
|
parsed.theme = resolveTheme(parsed.theme);
|
|
104
|
-
return
|
|
124
|
+
return normalizeGoal(parsed);
|
|
105
125
|
}
|
|
106
126
|
/**
|
|
107
127
|
* 渲染 deck 到输出目录。
|
|
@@ -156,21 +176,57 @@ window.__lemonPPT_layoutSchemas = ${JSON.stringify(layoutSchemas)};
|
|
|
156
176
|
await writeFile(indexPath, result.html, 'utf-8');
|
|
157
177
|
return { html: result.html, indexPath, assetsDir, assets: result.assets };
|
|
158
178
|
}
|
|
179
|
+
async function generateBrowserFallbackDriver(goal, mode, outFile) {
|
|
180
|
+
const driverDir = path.join(path.dirname(outFile), `.browser-fallback-${mode}`);
|
|
181
|
+
const assetsDir = path.join(driverDir, 'assets');
|
|
182
|
+
await mkdir(assetsDir, { recursive: true });
|
|
183
|
+
await copyThemeAssets(goal.theme, assetsDir);
|
|
184
|
+
await copyBrowserExportAssets(assetsDir);
|
|
185
|
+
const driverHtml = renderBrowserExportDriver({
|
|
186
|
+
mode,
|
|
187
|
+
goal,
|
|
188
|
+
callbackUrl: '',
|
|
189
|
+
assetBaseUrl: './assets',
|
|
190
|
+
});
|
|
191
|
+
const driverPath = path.join(driverDir, 'index.html');
|
|
192
|
+
await writeFile(driverPath, driverHtml, 'utf-8');
|
|
193
|
+
return driverPath;
|
|
194
|
+
}
|
|
159
195
|
/**
|
|
160
196
|
* 导出 goal 为 PPTX。
|
|
197
|
+
* 若服务端 Chromium 无法启动,会自动生成浏览器兜底驱动页。
|
|
161
198
|
*/
|
|
162
199
|
export async function exportGoalToPptx(goal, options) {
|
|
163
200
|
const outFile = path.resolve(options.outFile);
|
|
164
201
|
await mkdir(path.dirname(outFile), { recursive: true });
|
|
165
|
-
|
|
202
|
+
try {
|
|
203
|
+
await exportDeckToPptxScreenshot(goal, { outFile });
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
if (!isBrowserLaunchError(err)) {
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
const driverPath = await generateBrowserFallbackDriver(goal, 'pptx', outFile);
|
|
210
|
+
throw new Error(`无法启动 Chromium 进行 PPTX 导出。已生成浏览器兜底驱动页:${driverPath},请用浏览器打开该页面完成导出。`);
|
|
211
|
+
}
|
|
166
212
|
}
|
|
167
213
|
/**
|
|
168
214
|
* 导出 goal 为 PDF。
|
|
215
|
+
* 若服务端 Chromium 无法启动,会自动生成浏览器兜底驱动页。
|
|
169
216
|
*/
|
|
170
217
|
export async function exportGoalToPdf(goal, options) {
|
|
171
218
|
const outFile = path.resolve(options.outFile);
|
|
172
219
|
await mkdir(path.dirname(outFile), { recursive: true });
|
|
173
|
-
|
|
220
|
+
try {
|
|
221
|
+
await exportDeckToPdf(goal, { outFile });
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
if (!isBrowserLaunchError(err)) {
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
const driverPath = await generateBrowserFallbackDriver(goal, 'pdf', outFile);
|
|
228
|
+
throw new Error(`无法启动 Chromium 进行 PDF 导出。已生成浏览器兜底驱动页:${driverPath},请用浏览器打开该页面完成导出。`);
|
|
229
|
+
}
|
|
174
230
|
}
|
|
175
231
|
/**
|
|
176
232
|
* 列出所有可用主题。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lemonppt/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -31,10 +31,11 @@
|
|
|
31
31
|
},
|
|
32
32
|
"homepage": "https://github.com/lemonforme/lemonPPT#readme",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"
|
|
35
|
-
"@lemonppt/
|
|
36
|
-
"@lemonppt/
|
|
37
|
-
"@lemonppt/
|
|
34
|
+
"selfsigned": "^2.4.1",
|
|
35
|
+
"@lemonppt/agent-prompts": "1.0.9",
|
|
36
|
+
"@lemonppt/core": "1.0.9",
|
|
37
|
+
"@lemonppt/renderer": "1.0.9",
|
|
38
|
+
"@lemonppt/themes": "1.0.9"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@types/node": "^20.0.0",
|