@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.
- package/SKILL.md +259 -97
- package/agents/codex.yaml +99 -0
- package/agents/cursor.yaml +47 -0
- package/agents/openai.yaml +176 -0
- package/dist/cli.js +285 -2
- package/dist/index.d.ts +149 -1
- package/dist/index.js +473 -14
- package/dist/install-skill.d.ts +2 -0
- package/dist/install-skill.js +69 -20
- package/package.json +7 -6
|
@@ -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 {
|
|
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
|
|
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,默认
|
|
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>;
|