@lemonppt/cli 0.1.8 → 0.2.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.
@@ -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,25 @@
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 } from 'node:http';
6
+ import { stat, readFile } from 'node:fs/promises';
7
+ import { extname, join, resolve } from 'node:path';
8
+ import { exportGoalToPdf, exportGoalToPptx, generateGoalToFile, inspectLayout, listThemes, queryLayouts, readGoalFromFile, renderGoalToDir, scaffoldGoalToFile, validateGoalSpec, writeSafePropsToFile, } from './index.js';
6
9
  import { installSkill } from './install-skill.js';
7
10
  function printUsage() {
8
11
  console.log(`Usage:
9
12
  lemonppt generate "<input>" [--pages N] [--theme <id>] [--language zh|en] [--out goal.json] [--api-key KEY]
10
13
  lemonppt render <goal.json> [--out ./output] [--editable]
11
14
  lemonppt export <goal.json> --pptx out.pptx [--pdf out.pdf]
15
+ lemonppt serve [<dir>] [--port N]
12
16
  lemonppt install-skill [--claude] [--codex] [--cursor] [--all]
17
+
18
+ lemonppt list-themes
19
+ lemonppt layout-query --theme <id> --role <role> [--keyword K] [--needs-media] [--limit N] [--seed S]
20
+ lemonppt inspect-layout <layoutId> [--compact]
21
+ lemonppt goal-scaffold --title T --goal G --theme <id> --pages N [--out goal.json]
22
+ lemonppt write-safe-props <goal.json> [--write]
23
+ lemonppt validate-goal-spec <goal.json> [--strict]
13
24
  `);
14
25
  }
15
26
  function parseArgs(argv) {
@@ -34,6 +45,71 @@ function parseArgs(argv) {
34
45
  }
35
46
  return { positional, options };
36
47
  }
48
+ async function serveDir(dir, port) {
49
+ const root = resolve(dir);
50
+ const mimeTypes = {
51
+ '.html': 'text/html; charset=utf-8',
52
+ '.js': 'text/javascript; charset=utf-8',
53
+ '.css': 'text/css; charset=utf-8',
54
+ '.json': 'application/json; charset=utf-8',
55
+ '.png': 'image/png',
56
+ '.jpg': 'image/jpeg',
57
+ '.jpeg': 'image/jpeg',
58
+ '.gif': 'image/gif',
59
+ '.svg': 'image/svg+xml',
60
+ '.woff': 'font/woff',
61
+ '.woff2': 'font/woff2',
62
+ '.ttf': 'font/ttf',
63
+ '.otf': 'font/otf',
64
+ '.eot': 'application/vnd.ms-fontobject',
65
+ };
66
+ const server = createServer(async (req, res) => {
67
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
68
+ let pathname = decodeURIComponent(url.pathname);
69
+ if (pathname === '/') {
70
+ // 优先 editable 输出(editor.html),回退到非 editable 输出(index.html)
71
+ for (const fallback of ['editor.html', 'index.html']) {
72
+ try {
73
+ const s = await stat(join(root, fallback));
74
+ if (s.isFile()) {
75
+ pathname = '/' + fallback;
76
+ break;
77
+ }
78
+ }
79
+ catch { }
80
+ }
81
+ }
82
+ const filePath = join(root, pathname);
83
+ if (!filePath.startsWith(root + '/') && filePath !== root) {
84
+ res.writeHead(403);
85
+ res.end('Forbidden');
86
+ return;
87
+ }
88
+ try {
89
+ const s = await stat(filePath);
90
+ if (!s.isFile()) {
91
+ res.writeHead(404);
92
+ res.end('Not found');
93
+ return;
94
+ }
95
+ const ext = extname(filePath).toLowerCase();
96
+ const contentType = mimeTypes[ext] || 'application/octet-stream';
97
+ const data = await readFile(filePath);
98
+ res.writeHead(200, { 'Content-Type': contentType });
99
+ res.end(data);
100
+ }
101
+ catch {
102
+ res.writeHead(404);
103
+ res.end('Not found');
104
+ }
105
+ });
106
+ return new Promise((resolve) => {
107
+ server.listen(port, () => {
108
+ console.log(`Serving ${root} at http://localhost:${port}`);
109
+ resolve();
110
+ });
111
+ });
112
+ }
37
113
  async function main() {
38
114
  const argv = process.argv.slice(2);
39
115
  if (argv.length === 0) {
@@ -99,6 +175,128 @@ async function main() {
99
175
  }
100
176
  break;
101
177
  }
178
+ case 'serve': {
179
+ const dir = positional[0] || './output';
180
+ const port = args.options.port ? Number(args.options.port) : 3456;
181
+ await serveDir(dir, port);
182
+ // 保持进程运行
183
+ await new Promise(() => { });
184
+ break;
185
+ }
186
+ case 'list-themes': {
187
+ const themes = listThemes();
188
+ console.log(JSON.stringify(themes, null, 2));
189
+ break;
190
+ }
191
+ case 'layout-query': {
192
+ const theme = args.options.theme;
193
+ const role = args.options.role;
194
+ if (!theme || !role) {
195
+ console.error('Error: layout-query requires --theme and --role.');
196
+ process.exit(1);
197
+ }
198
+ const result = queryLayouts({
199
+ theme,
200
+ role,
201
+ keyword: args.options.keyword,
202
+ needsMedia: args.options['needs-media'] === true,
203
+ limit: args.options.limit ? Number(args.options.limit) : 8,
204
+ seed: args.options.seed,
205
+ });
206
+ console.log(JSON.stringify(result, null, 2));
207
+ break;
208
+ }
209
+ case 'inspect-layout': {
210
+ const layoutId = positional[0];
211
+ if (!layoutId) {
212
+ console.error('Error: inspect-layout requires a layoutId.');
213
+ process.exit(1);
214
+ }
215
+ const info = inspectLayout(layoutId);
216
+ if (!info) {
217
+ console.error(`Error: layout "${layoutId}" not found.`);
218
+ process.exit(1);
219
+ }
220
+ if (args.options.compact === true) {
221
+ console.log(JSON.stringify({
222
+ layout: info.layout,
223
+ displayName: info.displayName,
224
+ role: info.role,
225
+ description: info.description,
226
+ needsMedia: info.needsMedia,
227
+ mediaSlots: info.mediaSlots,
228
+ tags: info.tags,
229
+ contentShape: info.contentShape,
230
+ fields: info.fields,
231
+ }, null, 2));
232
+ }
233
+ else {
234
+ console.log(JSON.stringify(info, null, 2));
235
+ }
236
+ break;
237
+ }
238
+ case 'goal-scaffold': {
239
+ const title = args.options.title;
240
+ const goal = args.options.goal;
241
+ if (!title || !goal) {
242
+ console.error('Error: goal-scaffold requires --title and --goal.');
243
+ process.exit(1);
244
+ }
245
+ const result = await scaffoldGoalToFile({
246
+ title,
247
+ goal,
248
+ audience: args.options.audience,
249
+ owner: args.options.owner,
250
+ theme: args.options.theme,
251
+ pages: args.options.pages ? Number(args.options.pages) : 8,
252
+ language: args.options.language,
253
+ seed: args.options.seed,
254
+ outFile: args.options.out || './goal.json',
255
+ });
256
+ console.log(`Scaffolded goal: ${result.title} (${result.slides.length} slides)`);
257
+ break;
258
+ }
259
+ case 'write-safe-props': {
260
+ const goalPath = positional[0];
261
+ if (!goalPath) {
262
+ console.error('Error: write-safe-props requires a goal.json path.');
263
+ process.exit(1);
264
+ }
265
+ const result = await writeSafePropsToFile({
266
+ goalPath,
267
+ write: args.options.write === true,
268
+ });
269
+ if (args.options.write !== true) {
270
+ console.log(JSON.stringify({
271
+ valid: result.valid,
272
+ layoutChanges: result.layoutChanges,
273
+ unknownFields: result.unknownFields,
274
+ }, null, 2));
275
+ }
276
+ else {
277
+ console.log(`Safe props written to ${goalPath}`);
278
+ if (result.layoutChanges.length > 0) {
279
+ console.warn(`Layout changes: ${result.layoutChanges.length}`);
280
+ }
281
+ if (result.unknownFields.length > 0) {
282
+ console.warn(`Unknown fields: ${result.unknownFields.length}`);
283
+ }
284
+ }
285
+ break;
286
+ }
287
+ case 'validate-goal-spec': {
288
+ const goalPath = positional[0];
289
+ if (!goalPath) {
290
+ console.error('Error: validate-goal-spec requires a goal.json path.');
291
+ process.exit(1);
292
+ }
293
+ const result = await validateGoalSpec(goalPath, args.options.strict === true);
294
+ console.log(JSON.stringify(result, null, 2));
295
+ if (!result.valid) {
296
+ process.exit(1);
297
+ }
298
+ break;
299
+ }
102
300
  case 'install-skill': {
103
301
  const agents = [];
104
302
  if (args.options.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';
@@ -53,3 +53,100 @@ export declare function exportGoalToPptx(goal: DeckGoal, options: ExportCliOptio
53
53
  * 导出 goal 为 PDF。
54
54
  */
55
55
  export declare function exportGoalToPdf(goal: DeckGoal, options: ExportCliOptions): Promise<void>;
56
+ /**
57
+ * 列出所有可用主题。
58
+ */
59
+ export declare function listThemes(): {
60
+ id: string;
61
+ name: string;
62
+ }[];
63
+ /**
64
+ * 按主题与角色查询候选版式。
65
+ */
66
+ export declare function queryLayouts(options: {
67
+ theme: string;
68
+ role: string;
69
+ keyword?: string;
70
+ needsMedia?: boolean;
71
+ limit?: number;
72
+ seed?: string;
73
+ }): {
74
+ theme: string;
75
+ role: string;
76
+ count: number;
77
+ layouts: {
78
+ layout: string;
79
+ displayName: string;
80
+ description: string;
81
+ needsMedia: boolean;
82
+ }[];
83
+ };
84
+ /**
85
+ * 查看指定版式的字段契约。
86
+ */
87
+ export declare function inspectLayout(layoutId: string): {
88
+ layout: string;
89
+ displayName: string;
90
+ theme: string;
91
+ role: string;
92
+ description: string;
93
+ needsMedia: boolean;
94
+ mediaSlots: {
95
+ name: string;
96
+ fieldPath: string;
97
+ canPresetMedia: boolean;
98
+ }[];
99
+ tags: string[];
100
+ contentShape: string;
101
+ fields: {
102
+ key: string;
103
+ label: string;
104
+ type: string;
105
+ defaultValue?: unknown;
106
+ }[];
107
+ } | null;
108
+ /**
109
+ * 生成只含 role 的 goal.json 骨架。
110
+ */
111
+ export declare function scaffoldGoalToFile(options: {
112
+ title: string;
113
+ goal: string;
114
+ audience?: string;
115
+ owner?: string;
116
+ theme?: string;
117
+ pages?: number;
118
+ language?: 'zh' | 'en';
119
+ seed?: string;
120
+ outFile?: string;
121
+ }): Promise<DeckGoal>;
122
+ /**
123
+ * 规范化 goal.json 的 props。
124
+ */
125
+ export declare function writeSafePropsToFile(options: {
126
+ goalPath: string;
127
+ write?: boolean;
128
+ }): Promise<{
129
+ valid: boolean;
130
+ layoutChanges: {
131
+ index: number;
132
+ from: string;
133
+ to: string;
134
+ }[];
135
+ unknownFields: {
136
+ index: number;
137
+ layout: string;
138
+ keys: string[];
139
+ }[];
140
+ goal: DeckGoal;
141
+ }>;
142
+ /**
143
+ * 校验 goal.json 规范。
144
+ */
145
+ export declare function validateGoalSpec(goalPath: string, strict?: boolean): Promise<{
146
+ valid: boolean;
147
+ errors: {
148
+ type: string;
149
+ detail: unknown;
150
+ }[];
151
+ warnings: string[];
152
+ }>;