@lemonppt/cli 0.1.8 → 0.2.1

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.
@@ -0,0 +1,176 @@
1
+ # lemonPPT - AI-powered presentation generation
2
+ # Copyright (c) 2026 lemonforme
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ name: lemonppt
6
+ description: |
7
+ lemonPPT 是一个结构化 goal.json → 视觉 PPT 的渲染与导出引擎。
8
+ 支持自然语言生成 PPT、按角色自动选版式、导出 PPTX/PDF,以及程序化选页与字段校验。
9
+
10
+ functions:
11
+ - name: generate_goal
12
+ description: 根据一句话需求生成完整的 goal.json(端到端入口)
13
+ parameters:
14
+ type: object
15
+ properties:
16
+ input:
17
+ type: string
18
+ description: 用户的 PPT 需求描述
19
+ pageCount:
20
+ type: integer
21
+ description: 期望页数,默认 8
22
+ default: 8
23
+ theme:
24
+ type: string
25
+ description: 主题 ID,如 theme01、theme06
26
+ default: theme01
27
+ language:
28
+ type: string
29
+ enum: [zh, en]
30
+ description: 输出语言
31
+ default: zh
32
+ required: [input]
33
+
34
+ - name: render_goal
35
+ description: 将 goal.json 渲染为离线可打开的 HTML deck
36
+ parameters:
37
+ type: object
38
+ properties:
39
+ goal:
40
+ type: object
41
+ description: 符合 lemonPPT 规范的 DeckGoal 对象
42
+ editable:
43
+ type: boolean
44
+ description: 是否生成浏览器端可编辑版本
45
+ default: false
46
+ required: [goal]
47
+
48
+ - name: export_pptx
49
+ description: 将 goal.json 导出为可编辑 PPTX
50
+ parameters:
51
+ type: object
52
+ properties:
53
+ goal:
54
+ type: object
55
+ description: 符合 lemonPPT 规范的 DeckGoal 对象
56
+ required: [goal]
57
+
58
+ - name: export_pdf
59
+ description: 将 goal.json 导出为截图 PDF
60
+ parameters:
61
+ type: object
62
+ properties:
63
+ goal:
64
+ type: object
65
+ description: 符合 lemonPPT 规范的 DeckGoal 对象
66
+ required: [goal]
67
+
68
+ - name: list_themes
69
+ description: 列出所有可用主题
70
+ parameters:
71
+ type: object
72
+ properties: {}
73
+
74
+ - name: layout_query
75
+ description: 按主题与角色查询候选版式,供 Agent 程序化选页
76
+ parameters:
77
+ type: object
78
+ properties:
79
+ theme:
80
+ type: string
81
+ description: 主题 ID
82
+ role:
83
+ type: string
84
+ description: 页面角色,如 cover、metric、chart、process
85
+ keyword:
86
+ type: string
87
+ description: 关键词过滤
88
+ needsMedia:
89
+ type: boolean
90
+ description: 是否只返回需要媒体的版式
91
+ limit:
92
+ type: integer
93
+ description: 返回数量上限
94
+ default: 8
95
+ required: [theme, role]
96
+
97
+ - name: inspect_layout
98
+ description: 查看指定版式的字段契约、默认值与媒体槽位
99
+ parameters:
100
+ type: object
101
+ properties:
102
+ layoutId:
103
+ type: string
104
+ description: 完整版式 ID,如 theme06_metric_hero_v1
105
+ required: [layoutId]
106
+
107
+ - name: goal_scaffold
108
+ description: 生成只含 role 的 goal.json 骨架,由外层 Agent 填充内容
109
+ parameters:
110
+ type: object
111
+ properties:
112
+ title:
113
+ type: string
114
+ description: PPT 标题
115
+ goal:
116
+ type: string
117
+ description: PPT 目标/核心信息
118
+ audience:
119
+ type: string
120
+ description: 目标受众
121
+ owner:
122
+ type: string
123
+ description: 作者或团队
124
+ theme:
125
+ type: string
126
+ description: 主题 ID
127
+ default: theme01
128
+ pages:
129
+ type: integer
130
+ description: 页数
131
+ default: 8
132
+ language:
133
+ type: string
134
+ enum: [zh, en]
135
+ description: 语言
136
+ default: zh
137
+ required: [title, goal]
138
+
139
+ - name: write_safe_props
140
+ description: 规范化 goal.json 的 props,填充默认值并校验未知字段
141
+ parameters:
142
+ type: object
143
+ properties:
144
+ goal:
145
+ type: object
146
+ description: DeckGoal 对象
147
+ required: [goal]
148
+
149
+ - name: validate_goal_spec
150
+ description: 独立校验 goal.json 是否符合规范
151
+ parameters:
152
+ type: object
153
+ properties:
154
+ goal:
155
+ type: object
156
+ description: DeckGoal 对象
157
+ strict:
158
+ type: boolean
159
+ description: 是否将内容警告视为错误
160
+ default: false
161
+ required: [goal]
162
+
163
+ usage:
164
+ - |
165
+ 端到端生成:
166
+ 1. generate_goal(input="面向企业客户的 AI 助手产品发布会...", pageCount=8, theme="theme01")
167
+ 2. export_pptx(goal=result.goal)
168
+ - |
169
+ 精细编排:
170
+ 1. list_themes()
171
+ 2. layout_query(theme="theme06", role="metric", limit=3)
172
+ 3. inspect_layout(layoutId="theme06_metric_hero_v1")
173
+ 4. goal_scaffold(title="...", goal="...", theme="theme06", pages=10)
174
+ 5. write_safe_props(goal=...)
175
+ 6. validate_goal_spec(goal=...)
176
+ 7. export_pptx(goal=...)
package/dist/cli.js CHANGED
@@ -2,14 +2,32 @@
2
2
  // lemonPPT - AI-powered presentation generation
3
3
  // Copyright (c) 2026 lemonforme
4
4
  // SPDX-License-Identifier: AGPL-3.0-or-later
5
- import { exportGoalToPdf, exportGoalToPptx, generateGoalToFile, readGoalFromFile, renderGoalToDir, } from './index.js';
5
+ import { createServer as createHttpServer } from 'node:http';
6
+ import { spawn } from 'node:child_process';
7
+ import { existsSync } from 'node:fs';
8
+ import { stat, readFile } from 'node:fs/promises';
9
+ import { extname, join, resolve, dirname } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { exportGoalToPdf, exportGoalToPptx, generateGoalToFile, inspectLayout, listThemes, queryLayouts, readGoalFromFile, renderGoalToDir, scaffoldGoalToFile, stageMediaToFile, validateDeck, validateGoalCopy, validateGoalSpec, writeSafePropsToFile, } from './index.js';
6
12
  import { installSkill } from './install-skill.js';
7
13
  function printUsage() {
8
14
  console.log(`Usage:
9
15
  lemonppt generate "<input>" [--pages N] [--theme <id>] [--language zh|en] [--out goal.json] [--api-key KEY]
10
16
  lemonppt render <goal.json> [--out ./output] [--editable]
11
17
  lemonppt export <goal.json> --pptx out.pptx [--pdf out.pdf]
12
- lemonppt install-skill [--claude] [--codex] [--cursor] [--all]
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 install-skill [--claude] [--codex] [--cursor] [--all] [--target <dir>]
21
+
22
+ lemonppt list-themes
23
+ lemonppt layout-query --theme <id> --role <role> [--keyword K] [--needs-media] [--limit N] [--seed S]
24
+ lemonppt inspect-layout <layoutId> [--compact]
25
+ lemonppt goal-scaffold --title T --goal G --theme <id> --pages N [--out goal.json]
26
+ lemonppt write-safe-props <goal.json> [--write]
27
+ lemonppt validate-goal-spec <goal.json> [--strict]
28
+ lemonppt validate-deck <deckDir> [--goal goal.json]
29
+ lemonppt validate-copy <goal.json> <deckDir>
30
+ lemonppt stage-media <file> [--out ./output]
13
31
  `);
14
32
  }
15
33
  function parseArgs(argv) {
@@ -34,6 +52,96 @@ function parseArgs(argv) {
34
52
  }
35
53
  return { positional, options };
36
54
  }
55
+ const __dirname = dirname(fileURLToPath(import.meta.url));
56
+ const projectRoot = resolve(__dirname, '../../..');
57
+ const apiServerPath = join(projectRoot, 'apps/server/dist/index.js');
58
+ async function startApiServer(dir, port) {
59
+ if (!existsSync(apiServerPath)) {
60
+ throw new Error(`API server not found at ${apiServerPath}. Run 'pnpm -r build' first.`);
61
+ }
62
+ const child = spawn('node', [apiServerPath], {
63
+ stdio: 'inherit',
64
+ env: {
65
+ ...process.env,
66
+ LEMONPPT_PORT: String(port),
67
+ LEMONPPT_OUTPUT_DIR: resolve(dir),
68
+ },
69
+ });
70
+ return new Promise((resolve, reject) => {
71
+ child.on('error', reject);
72
+ child.on('exit', (code) => {
73
+ if (code === 0)
74
+ resolve();
75
+ else
76
+ reject(new Error(`API server exited with code ${code}`));
77
+ });
78
+ });
79
+ }
80
+ async function serveDir(dir, port) {
81
+ const root = resolve(dir);
82
+ const mimeTypes = {
83
+ '.html': 'text/html; charset=utf-8',
84
+ '.js': 'text/javascript; charset=utf-8',
85
+ '.css': 'text/css; charset=utf-8',
86
+ '.json': 'application/json; charset=utf-8',
87
+ '.png': 'image/png',
88
+ '.jpg': 'image/jpeg',
89
+ '.jpeg': 'image/jpeg',
90
+ '.gif': 'image/gif',
91
+ '.svg': 'image/svg+xml',
92
+ '.woff': 'font/woff',
93
+ '.woff2': 'font/woff2',
94
+ '.ttf': 'font/ttf',
95
+ '.otf': 'font/otf',
96
+ '.eot': 'application/vnd.ms-fontobject',
97
+ };
98
+ const server = createHttpServer(async (req, res) => {
99
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
100
+ let pathname = decodeURIComponent(url.pathname);
101
+ if (pathname === '/') {
102
+ // 优先 editable 输出(editor.html),回退到非 editable 输出(index.html)
103
+ for (const fallback of ['editor.html', 'index.html']) {
104
+ try {
105
+ const s = await stat(join(root, fallback));
106
+ if (s.isFile()) {
107
+ pathname = '/' + fallback;
108
+ break;
109
+ }
110
+ }
111
+ catch { }
112
+ }
113
+ }
114
+ const filePath = join(root, pathname);
115
+ if (!filePath.startsWith(root + '/') && filePath !== root) {
116
+ res.writeHead(403);
117
+ res.end('Forbidden');
118
+ return;
119
+ }
120
+ try {
121
+ const s = await stat(filePath);
122
+ if (!s.isFile()) {
123
+ res.writeHead(404);
124
+ res.end('Not found');
125
+ return;
126
+ }
127
+ const ext = extname(filePath).toLowerCase();
128
+ const contentType = mimeTypes[ext] || 'application/octet-stream';
129
+ const data = await readFile(filePath);
130
+ res.writeHead(200, { 'Content-Type': contentType });
131
+ res.end(data);
132
+ }
133
+ catch {
134
+ res.writeHead(404);
135
+ res.end('Not found');
136
+ }
137
+ });
138
+ return new Promise((resolve) => {
139
+ server.listen(port, () => {
140
+ console.log(`Serving ${root} at http://localhost:${port}`);
141
+ resolve();
142
+ });
143
+ });
144
+ }
37
145
  async function main() {
38
146
  const argv = process.argv.slice(2);
39
147
  if (argv.length === 0) {
@@ -99,7 +207,182 @@ async function main() {
99
207
  }
100
208
  break;
101
209
  }
210
+ case 'serve':
211
+ case 'server': {
212
+ const dir = positional[0] || './output';
213
+ const port = args.options.port ? Number(args.options.port) : 3456;
214
+ if (existsSync(apiServerPath)) {
215
+ console.log(`Starting API server on port ${port}...`);
216
+ await startApiServer(dir, port);
217
+ }
218
+ else {
219
+ console.log(`API server not built, falling back to static preview.`);
220
+ await serveDir(dir, port);
221
+ // 保持进程运行
222
+ await new Promise(() => { });
223
+ }
224
+ break;
225
+ }
226
+ case 'list-themes': {
227
+ const themes = listThemes();
228
+ console.log(JSON.stringify(themes, null, 2));
229
+ break;
230
+ }
231
+ case 'layout-query': {
232
+ const theme = args.options.theme;
233
+ const role = args.options.role;
234
+ if (!theme || !role) {
235
+ console.error('Error: layout-query requires --theme and --role.');
236
+ process.exit(1);
237
+ }
238
+ const result = queryLayouts({
239
+ theme,
240
+ role,
241
+ keyword: args.options.keyword,
242
+ needsMedia: args.options['needs-media'] === true,
243
+ limit: args.options.limit ? Number(args.options.limit) : 8,
244
+ seed: args.options.seed,
245
+ });
246
+ console.log(JSON.stringify(result, null, 2));
247
+ break;
248
+ }
249
+ case 'inspect-layout': {
250
+ const layoutId = positional[0];
251
+ if (!layoutId) {
252
+ console.error('Error: inspect-layout requires a layoutId.');
253
+ process.exit(1);
254
+ }
255
+ const info = inspectLayout(layoutId);
256
+ if (!info) {
257
+ console.error(`Error: layout "${layoutId}" not found.`);
258
+ process.exit(1);
259
+ }
260
+ if (args.options.compact === true) {
261
+ console.log(JSON.stringify({
262
+ layout: info.layout,
263
+ displayName: info.displayName,
264
+ role: info.role,
265
+ description: info.description,
266
+ needsMedia: info.needsMedia,
267
+ mediaSlots: info.mediaSlots,
268
+ tags: info.tags,
269
+ contentShape: info.contentShape,
270
+ fields: info.fields,
271
+ }, null, 2));
272
+ }
273
+ else {
274
+ console.log(JSON.stringify(info, null, 2));
275
+ }
276
+ break;
277
+ }
278
+ case 'goal-scaffold': {
279
+ const title = args.options.title;
280
+ const goal = args.options.goal;
281
+ if (!title || !goal) {
282
+ console.error('Error: goal-scaffold requires --title and --goal.');
283
+ process.exit(1);
284
+ }
285
+ const result = await scaffoldGoalToFile({
286
+ title,
287
+ goal,
288
+ audience: args.options.audience,
289
+ owner: args.options.owner,
290
+ theme: args.options.theme,
291
+ pages: args.options.pages ? Number(args.options.pages) : 8,
292
+ language: args.options.language,
293
+ seed: args.options.seed,
294
+ outFile: args.options.out || './goal.json',
295
+ });
296
+ console.log(`Scaffolded goal: ${result.title} (${result.slides.length} slides)`);
297
+ break;
298
+ }
299
+ case 'write-safe-props': {
300
+ const goalPath = positional[0];
301
+ if (!goalPath) {
302
+ console.error('Error: write-safe-props requires a goal.json path.');
303
+ process.exit(1);
304
+ }
305
+ const result = await writeSafePropsToFile({
306
+ goalPath,
307
+ write: args.options.write === true,
308
+ });
309
+ if (args.options.write !== true) {
310
+ console.log(JSON.stringify({
311
+ valid: result.valid,
312
+ layoutChanges: result.layoutChanges,
313
+ unknownFields: result.unknownFields,
314
+ }, null, 2));
315
+ }
316
+ else {
317
+ console.log(`Safe props written to ${goalPath}`);
318
+ if (result.layoutChanges.length > 0) {
319
+ console.warn(`Layout changes: ${result.layoutChanges.length}`);
320
+ }
321
+ if (result.unknownFields.length > 0) {
322
+ console.warn(`Unknown fields: ${result.unknownFields.length}`);
323
+ }
324
+ }
325
+ break;
326
+ }
327
+ case 'validate-goal-spec': {
328
+ const goalPath = positional[0];
329
+ if (!goalPath) {
330
+ console.error('Error: validate-goal-spec requires a goal.json path.');
331
+ process.exit(1);
332
+ }
333
+ const result = await validateGoalSpec(goalPath, args.options.strict === true);
334
+ console.log(JSON.stringify(result, null, 2));
335
+ if (!result.valid) {
336
+ process.exit(1);
337
+ }
338
+ break;
339
+ }
340
+ case 'stage-media': {
341
+ const filePath = positional[0];
342
+ if (!filePath) {
343
+ console.error('Error: stage-media requires a file path.');
344
+ process.exit(1);
345
+ }
346
+ const result = await stageMediaToFile({
347
+ filePath,
348
+ outDir: args.options.out || './output',
349
+ });
350
+ console.log(JSON.stringify({ success: true, ...result }, null, 2));
351
+ break;
352
+ }
353
+ case 'validate-deck': {
354
+ const deckDir = positional[0];
355
+ if (!deckDir) {
356
+ console.error('Error: validate-deck requires a deck directory path.');
357
+ process.exit(1);
358
+ }
359
+ const result = await validateDeck({
360
+ deckDir,
361
+ goalPath: args.options.goal,
362
+ });
363
+ console.log(JSON.stringify(result, null, 2));
364
+ if (!result.valid)
365
+ process.exit(1);
366
+ break;
367
+ }
368
+ case 'validate-copy': {
369
+ const goalPath = positional[0];
370
+ const deckDir = positional[1];
371
+ if (!goalPath || !deckDir) {
372
+ console.error('Error: validate-copy requires <goal.json> <deckDir>.');
373
+ process.exit(1);
374
+ }
375
+ const result = await validateGoalCopy({ goalPath, deckDir });
376
+ console.log(JSON.stringify(result, null, 2));
377
+ if (!result.valid)
378
+ process.exit(1);
379
+ break;
380
+ }
102
381
  case 'install-skill': {
382
+ if (args.options.target) {
383
+ await installSkill({ target: args.options.target });
384
+ break;
385
+ }
103
386
  const agents = [];
104
387
  if (args.options.claude)
105
388
  agents.push('claude');
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface GenerateCliOptions {
5
5
  input: string;
6
6
  /** 页数,默认 8 */
7
7
  pageCount?: number;
8
- /** 主题 ID,默认 base */
8
+ /** 主题 ID,默认 theme01 */
9
9
  theme?: string;
10
10
  /** 语言,默认 zh */
11
11
  language?: 'zh' | 'en';
@@ -28,6 +28,25 @@ export interface ExportCliOptions {
28
28
  /** 输出文件路径 */
29
29
  outFile: string;
30
30
  }
31
+ export declare function copyThemeAssets(themeId: string, assetsDir: string): Promise<void>;
32
+ export interface StageMediaOptions {
33
+ /** 源文件路径 */
34
+ filePath: string;
35
+ /** 输出目录,默认 ./output */
36
+ outDir?: string;
37
+ }
38
+ export interface StageMediaResult {
39
+ /** 安全化后的文件名 */
40
+ filename: string;
41
+ /** 本地绝对路径 */
42
+ localPath: string;
43
+ /** 相对于输出目录的引用路径 */
44
+ relativePath: string;
45
+ }
46
+ /**
47
+ * 把本地媒体文件复制到输出目录的 media/ 下,供 goal.json 引用。
48
+ */
49
+ export declare function stageMediaToFile(options: StageMediaOptions): Promise<StageMediaResult>;
31
50
  /**
32
51
  * 生成 goal.json 并可选写入文件。
33
52
  */
@@ -38,6 +57,8 @@ export declare function generateGoalToFile(options: GenerateCliOptions): Promise
38
57
  export declare function readGoalFromFile(filePath: string): Promise<DeckGoal>;
39
58
  /**
40
59
  * 渲染 deck 到输出目录。
60
+ * editable 模式下使用单页编辑器架构:复制 server 的 editor.html/editor.js,
61
+ * 内嵌 renderEditorData 生成的 EditorData,资源路径使用相对路径。
41
62
  */
42
63
  export declare function renderGoalToDir(goal: DeckGoal, options?: RenderCliOptions): Promise<{
43
64
  html: string;
@@ -53,3 +74,130 @@ export declare function exportGoalToPptx(goal: DeckGoal, options: ExportCliOptio
53
74
  * 导出 goal 为 PDF。
54
75
  */
55
76
  export declare function exportGoalToPdf(goal: DeckGoal, options: ExportCliOptions): Promise<void>;
77
+ /**
78
+ * 列出所有可用主题。
79
+ */
80
+ export declare function listThemes(): {
81
+ id: string;
82
+ name: string;
83
+ }[];
84
+ /**
85
+ * 按主题与角色查询候选版式。
86
+ */
87
+ export declare function queryLayouts(options: {
88
+ theme: string;
89
+ role: string;
90
+ keyword?: string;
91
+ needsMedia?: boolean;
92
+ limit?: number;
93
+ seed?: string;
94
+ }): {
95
+ theme: string;
96
+ role: string;
97
+ count: number;
98
+ layouts: {
99
+ layout: string;
100
+ displayName: string;
101
+ description: string;
102
+ needsMedia: boolean;
103
+ }[];
104
+ };
105
+ /**
106
+ * 查看指定版式的字段契约。
107
+ */
108
+ export declare function inspectLayout(layoutId: string): {
109
+ layout: string;
110
+ displayName: string;
111
+ theme: string;
112
+ role: string;
113
+ description: string;
114
+ needsMedia: boolean;
115
+ mediaSlots: {
116
+ name: string;
117
+ fieldPath: string;
118
+ canPresetMedia: boolean;
119
+ }[];
120
+ tags: string[];
121
+ contentShape: string;
122
+ fields: {
123
+ key: string;
124
+ label: string;
125
+ type: string;
126
+ defaultValue?: unknown;
127
+ }[];
128
+ } | null;
129
+ /**
130
+ * 生成只含 role 的 goal.json 骨架。
131
+ */
132
+ export declare function scaffoldGoalToFile(options: {
133
+ title: string;
134
+ goal: string;
135
+ audience?: string;
136
+ owner?: string;
137
+ theme?: string;
138
+ pages?: number;
139
+ language?: 'zh' | 'en';
140
+ seed?: string;
141
+ outFile?: string;
142
+ }): Promise<DeckGoal>;
143
+ /**
144
+ * 规范化 goal.json 的 props。
145
+ */
146
+ export declare function writeSafePropsToFile(options: {
147
+ goalPath: string;
148
+ write?: boolean;
149
+ }): Promise<{
150
+ valid: boolean;
151
+ layoutChanges: {
152
+ index: number;
153
+ from: string;
154
+ to: string;
155
+ }[];
156
+ unknownFields: {
157
+ index: number;
158
+ layout: string;
159
+ keys: string[];
160
+ }[];
161
+ goal: DeckGoal;
162
+ }>;
163
+ /**
164
+ * 校验 goal.json 规范。
165
+ */
166
+ export declare function validateGoalSpec(goalPath: string, strict?: boolean): Promise<{
167
+ valid: boolean;
168
+ errors: {
169
+ type: string;
170
+ detail: unknown;
171
+ }[];
172
+ warnings: string[];
173
+ }>;
174
+ export interface ValidateDeckOptions {
175
+ /** 渲染输出目录(应包含 index.html) */
176
+ deckDir: string;
177
+ /** 可选的 goal.json 路径,用于比对页数 */
178
+ goalPath?: string;
179
+ }
180
+ export interface ValidateDeckResult {
181
+ valid: boolean;
182
+ slides: number;
183
+ errors: string[];
184
+ warnings: string[];
185
+ }
186
+ /**
187
+ * 校验渲染后的 HTML deck 结构是否完整。
188
+ */
189
+ export declare function validateDeck(options: ValidateDeckOptions): Promise<ValidateDeckResult>;
190
+ export interface ValidateCopyOptions {
191
+ goalPath: string;
192
+ deckDir: string;
193
+ }
194
+ export interface ValidateCopyResult {
195
+ valid: boolean;
196
+ missing: string[];
197
+ checked: number;
198
+ errors: string[];
199
+ }
200
+ /**
201
+ * 校验 goal.json 中的文案是否都出现在渲染后的 HTML 中。
202
+ */
203
+ export declare function validateGoalCopy(options: ValidateCopyOptions): Promise<ValidateCopyResult>;