@lemonppt/cli 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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] # start API server if built, else static preview
19
- lemonppt server [<dir>] [--port N] # alias for serve
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 serveDir(dir, port) {
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 server = createHttpServer(async (req, res) => {
99
- const url = new URL(req.url || '/', `http://${req.headers.host}`);
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 http://localhost:${port}`);
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 { normalizeDeckGoal, preprocessAgentGoal, validateDeckGoal, validateDeckGoalContent, validateSlideCount } from '@lemonppt/core';
6
- import { exportDeckToPdf, exportDeckToPptx, renderDeck, renderEditorData, } from '@lemonppt/renderer';
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 normalizeDeckGoal(parsed);
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
- await exportDeckToPptx(goal, { outFile });
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
- await exportDeckToPdf(goal, { outFile });
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.6",
3
+ "version": "1.0.8",
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
- "@lemonppt/agent-prompts": "1.0.6",
35
- "@lemonppt/core": "1.0.6",
36
- "@lemonppt/renderer": "1.0.6",
37
- "@lemonppt/themes": "1.0.6"
34
+ "selfsigned": "^2.4.1",
35
+ "@lemonppt/agent-prompts": "1.0.8",
36
+ "@lemonppt/core": "1.0.8",
37
+ "@lemonppt/renderer": "1.0.8",
38
+ "@lemonppt/themes": "1.0.8"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/node": "^20.0.0",