@lemonppt/cli 0.2.0 → 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 +74 -4
- package/dist/cli.js +95 -10
- package/dist/index.d.ts +51 -0
- package/dist/index.js +170 -23
- package/dist/install-skill.d.ts +2 -0
- package/dist/install-skill.js +69 -20
- package/package.json +5 -5
package/SKILL.md
CHANGED
|
@@ -72,14 +72,23 @@ lemonppt export ./goal.json --pptx ./deck.pptx
|
|
|
72
72
|
|
|
73
73
|
### 方式三:HTTP API 服务
|
|
74
74
|
|
|
75
|
+
先确保已构建:`corepack pnpm -r build`(若系统未全局安装 pnpm,corepack 会自动提供)。
|
|
76
|
+
|
|
75
77
|
```bash
|
|
78
|
+
# 启动真正的 lemonPPT API 服务(默认 3456 端口)
|
|
76
79
|
lemonppt serve --port 3456
|
|
80
|
+
|
|
81
|
+
# 等价的 server 别名
|
|
82
|
+
lemonppt server --port 3456
|
|
77
83
|
```
|
|
78
84
|
|
|
85
|
+
底层启动 `apps/server/dist/index.js`,输出目录默认 `./output`。
|
|
86
|
+
|
|
79
87
|
接口:
|
|
80
88
|
|
|
81
89
|
- `POST /api/generate-goal`:自然语言 → `goal.json`
|
|
82
90
|
- `POST /api/render`:`goal.json` → HTML
|
|
91
|
+
- `POST /api/render-editor`:`goal.json` → 单页编辑器渲染数据(`EditorData` JSON),不再生成静态文件
|
|
83
92
|
- `POST /api/export/pptx`:`goal.json` → PPTX
|
|
84
93
|
- `POST /api/export/pdf`:`goal.json` → PDF
|
|
85
94
|
- `POST /api/layout-query`:候选版式查询
|
|
@@ -87,6 +96,31 @@ lemonppt serve --port 3456
|
|
|
87
96
|
- `POST /api/goal-scaffold`:生成骨架
|
|
88
97
|
- `POST /api/write-safe-props`:规范化 props
|
|
89
98
|
- `POST /api/validate-goal-spec`:校验 goal.json
|
|
99
|
+
- `POST /api/stage-media`:上传 base64 图片到服务目录,返回可在 `goal.json` 中引用的 URL
|
|
100
|
+
- `GET /editor`:打开单页编辑器(所有主题共享同一页面,通过 `?theme=theme01` 切换主题)
|
|
101
|
+
- `GET /api/render-editor?theme=theme01`:基于示例 goal 返回指定主题的 `EditorData`
|
|
102
|
+
|
|
103
|
+
调用示例:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
curl -X POST http://localhost:3456/api/render \
|
|
107
|
+
-H "Content-Type: application/json" \
|
|
108
|
+
-d @goal.json
|
|
109
|
+
|
|
110
|
+
curl -X POST http://localhost:3456/api/export/pptx \
|
|
111
|
+
-H "Content-Type: application/json" \
|
|
112
|
+
-d @goal.json \
|
|
113
|
+
--output deck.pptx
|
|
114
|
+
|
|
115
|
+
curl -X POST "http://localhost:3456/api/render-editor?theme=theme02" \
|
|
116
|
+
-H "Content-Type: application/json" \
|
|
117
|
+
-d @goal.json
|
|
118
|
+
|
|
119
|
+
# 上传本地图片(base64)
|
|
120
|
+
curl -X POST http://localhost:3456/api/stage-media \
|
|
121
|
+
-H "Content-Type: application/json" \
|
|
122
|
+
-d '{"filename":"logo.png","data":"iVBORw0KGgoAAAANSUhEUg..."}'
|
|
123
|
+
```
|
|
90
124
|
|
|
91
125
|
---
|
|
92
126
|
|
|
@@ -100,6 +134,10 @@ lemonppt serve --port 3456
|
|
|
100
134
|
| `theme04` | 玻璃糖果风 | green / yellow / blue / pink + light / dark |
|
|
101
135
|
| `theme05` | 光谱报告风 | coral / amber / teal / indigo / violet + light / dark |
|
|
102
136
|
| `theme06` | 深色图谱风 | volt / magma / nebula / nova + light / dark |
|
|
137
|
+
| `theme07` | 冷白金融投资风 | cold-white / warm-gray / ink / navy + light / dark |
|
|
138
|
+
| `theme08` | 曜金黑金机构风 | obsidian-gold / midnight-silver / graphite-rose / forest-gold |
|
|
139
|
+
| `theme09` | 墨韵杂志印刷风 | paper / ink 双基底 + primary / muted |
|
|
140
|
+
| `theme10` | 金指数据指数风 | gold-index / blue-index / green-index |
|
|
103
141
|
|
|
104
142
|
默认主题:`theme01`。
|
|
105
143
|
|
|
@@ -140,11 +178,11 @@ lemonppt serve --port 3456
|
|
|
140
178
|
| `goal` | 是 | 演示目标/背景 |
|
|
141
179
|
| `audience` | 是 | 受众描述 |
|
|
142
180
|
| `owner` | 否 | 汇报人 |
|
|
143
|
-
| `theme` | 是 | 主题 ID |
|
|
181
|
+
| `theme` | 是 | 主题 ID;也兼容 `themePack` 作为别名 |
|
|
144
182
|
| `colorScheme` | 否 | 主题专用配色方案,见「可用主题」 |
|
|
145
183
|
| `appearance` | 否 | `light` / `dark`,部分主题支持 |
|
|
146
184
|
| `language` | 否 | `zh` 或 `en`,默认 `zh` |
|
|
147
|
-
| `pageCount` |
|
|
185
|
+
| `pageCount` | 否 | 总页数;留空时自动等于 `slides.length` |
|
|
148
186
|
| `randomSeed` | 否 | 随机种子,保证选页可复现 |
|
|
149
187
|
| `slides` | 是 | 幻灯片数组 |
|
|
150
188
|
|
|
@@ -152,10 +190,12 @@ lemonppt serve --port 3456
|
|
|
152
190
|
|
|
153
191
|
| 字段 | 必填 | 说明 |
|
|
154
192
|
|---|---|---|
|
|
155
|
-
| `role` |
|
|
193
|
+
| `role` | 否 | 页面角色,见下方「页面角色」;留空时尝试从 `layout` 推断 |
|
|
156
194
|
| `layout` | 否 | 具体版式 ID;留空时系统按 role 自动选择 |
|
|
157
195
|
| `props` | 是 | 该版式所需数据 |
|
|
158
196
|
|
|
197
|
+
> **外部 Agent 友好**:HTTP API 与 CLI 均支持 `themePack` 替代 `theme`、`pageCount` 省略、`role` 省略(系统从 `layout` ID 推断),方便被其他 Agent 调用。未提供的必填字段将自动补全。
|
|
198
|
+
|
|
159
199
|
---
|
|
160
200
|
|
|
161
201
|
## 页面角色
|
|
@@ -220,8 +260,9 @@ lemonppt render <goal.json> [--out ./output] [--editable]
|
|
|
220
260
|
# 导出
|
|
221
261
|
lemonppt export <goal.json> --pptx out.pptx [--pdf out.pdf]
|
|
222
262
|
|
|
223
|
-
#
|
|
263
|
+
# 本地服务(优先启动 API 服务;未构建时回退到静态预览)
|
|
224
264
|
lemonppt serve [<dir>] [--port N]
|
|
265
|
+
lemonppt server [<dir>] [--port N]
|
|
225
266
|
|
|
226
267
|
# 主题/版式查询
|
|
227
268
|
lemonppt list-themes
|
|
@@ -235,10 +276,39 @@ lemonppt validate-goal-spec <goal.json>
|
|
|
235
276
|
|
|
236
277
|
# 安装到 Agent 技能目录
|
|
237
278
|
lemonppt install-skill [--claude] [--codex] [--cursor] [--all]
|
|
279
|
+
lemonppt install-skill --target ./my-agent/skills/lemonppt
|
|
238
280
|
```
|
|
239
281
|
|
|
240
282
|
---
|
|
241
283
|
|
|
284
|
+
## Skill 包内 npm scripts(Dashi 风格)
|
|
285
|
+
|
|
286
|
+
将 `skills/lemonppt/` 复制到 Agent 技能目录后,也可以直接进入 skill 目录调用 npm scripts:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
cd ~/.claude/skills/lemonppt
|
|
290
|
+
|
|
291
|
+
npm run layout:query -- --theme theme01 --role cover --limit 5
|
|
292
|
+
npm run inspect:layout -- theme01_cover_v1
|
|
293
|
+
npm run goal:scaffold -- --title "AI 报告" --goal "..." --theme theme01 --pages 8 --out ./goal.json
|
|
294
|
+
npm run props:safe -- ./goal.json --write
|
|
295
|
+
npm run validate:goal-spec -- ./goal.json
|
|
296
|
+
npm run render:goal -- ./goal.json --out ./output
|
|
297
|
+
npm run validate:deck -- ./output --goal ./goal.json
|
|
298
|
+
npm run validate:goal-copy -- ./goal.json ./output
|
|
299
|
+
npm run export:pptx -- ./goal.json ./deck.pptx
|
|
300
|
+
npm run export:pdf -- ./goal.json ./deck.pdf
|
|
301
|
+
npm run preview:start -- ./output --port 3456
|
|
302
|
+
npm run media:stage -- ./image.png --out ./output/assets
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
这些脚本本质上是调用 `lemonppt` CLI 的薄包装,因此首次使用前需要:
|
|
306
|
+
|
|
307
|
+
- 源码模式:确保项目已构建(`corepack pnpm -r build`),skill 包会被写入本地 CLI 路径;
|
|
308
|
+
- 发布模式:执行过 `npm install -g @lemonppt/cli`,或脚本自动通过 `npx @lemonppt/cli` 调用。
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
242
312
|
## 常见错误处理
|
|
243
313
|
|
|
244
314
|
1. **没有 API Key**:`lemonppt generate` 会 fallback 到内置示例内容,仍可生成完整文件。如需更贴合主题的文案,提供 `--api-key`。
|
package/dist/cli.js
CHANGED
|
@@ -2,18 +2,22 @@
|
|
|
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 { createServer } from 'node:http';
|
|
5
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
6
8
|
import { stat, readFile } from 'node:fs/promises';
|
|
7
|
-
import { extname, join, resolve } from 'node:path';
|
|
8
|
-
import {
|
|
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';
|
|
9
12
|
import { installSkill } from './install-skill.js';
|
|
10
13
|
function printUsage() {
|
|
11
14
|
console.log(`Usage:
|
|
12
15
|
lemonppt generate "<input>" [--pages N] [--theme <id>] [--language zh|en] [--out goal.json] [--api-key KEY]
|
|
13
16
|
lemonppt render <goal.json> [--out ./output] [--editable]
|
|
14
17
|
lemonppt export <goal.json> --pptx out.pptx [--pdf out.pdf]
|
|
15
|
-
lemonppt serve [<dir>] [--port N]
|
|
16
|
-
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>]
|
|
17
21
|
|
|
18
22
|
lemonppt list-themes
|
|
19
23
|
lemonppt layout-query --theme <id> --role <role> [--keyword K] [--needs-media] [--limit N] [--seed S]
|
|
@@ -21,6 +25,9 @@ function printUsage() {
|
|
|
21
25
|
lemonppt goal-scaffold --title T --goal G --theme <id> --pages N [--out goal.json]
|
|
22
26
|
lemonppt write-safe-props <goal.json> [--write]
|
|
23
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]
|
|
24
31
|
`);
|
|
25
32
|
}
|
|
26
33
|
function parseArgs(argv) {
|
|
@@ -45,6 +52,31 @@ function parseArgs(argv) {
|
|
|
45
52
|
}
|
|
46
53
|
return { positional, options };
|
|
47
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
|
+
}
|
|
48
80
|
async function serveDir(dir, port) {
|
|
49
81
|
const root = resolve(dir);
|
|
50
82
|
const mimeTypes = {
|
|
@@ -63,7 +95,7 @@ async function serveDir(dir, port) {
|
|
|
63
95
|
'.otf': 'font/otf',
|
|
64
96
|
'.eot': 'application/vnd.ms-fontobject',
|
|
65
97
|
};
|
|
66
|
-
const server =
|
|
98
|
+
const server = createHttpServer(async (req, res) => {
|
|
67
99
|
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
68
100
|
let pathname = decodeURIComponent(url.pathname);
|
|
69
101
|
if (pathname === '/') {
|
|
@@ -175,12 +207,20 @@ async function main() {
|
|
|
175
207
|
}
|
|
176
208
|
break;
|
|
177
209
|
}
|
|
178
|
-
case 'serve':
|
|
210
|
+
case 'serve':
|
|
211
|
+
case 'server': {
|
|
179
212
|
const dir = positional[0] || './output';
|
|
180
213
|
const port = args.options.port ? Number(args.options.port) : 3456;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
}
|
|
184
224
|
break;
|
|
185
225
|
}
|
|
186
226
|
case 'list-themes': {
|
|
@@ -297,7 +337,52 @@ async function main() {
|
|
|
297
337
|
}
|
|
298
338
|
break;
|
|
299
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
|
+
}
|
|
300
381
|
case 'install-skill': {
|
|
382
|
+
if (args.options.target) {
|
|
383
|
+
await installSkill({ target: args.options.target });
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
301
386
|
const agents = [];
|
|
302
387
|
if (args.options.claude)
|
|
303
388
|
agents.push('claude');
|
package/dist/index.d.ts
CHANGED
|
@@ -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;
|
|
@@ -150,3 +171,33 @@ export declare function validateGoalSpec(goalPath: string, strict?: boolean): Pr
|
|
|
150
171
|
}[];
|
|
151
172
|
warnings: string[];
|
|
152
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>;
|
package/dist/index.js
CHANGED
|
@@ -2,18 +2,19 @@
|
|
|
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, validateDeckGoal, validateDeckGoalContent, validateSlideCount } from '@lemonppt/core';
|
|
6
|
-
import { exportDeckToPdf, exportDeckToPptx, renderDeck, } from '@lemonppt/renderer';
|
|
5
|
+
import { normalizeDeckGoal, preprocessAgentGoal, validateDeckGoal, validateDeckGoalContent, validateSlideCount } from '@lemonppt/core';
|
|
6
|
+
import { exportDeckToPdf, exportDeckToPptx, 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';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
13
|
function resolveTheme(themeId) {
|
|
13
14
|
const id = themeId || 'theme01';
|
|
14
15
|
return getTheme(id) ? id : 'theme01';
|
|
15
16
|
}
|
|
16
|
-
async function copyThemeAssets(themeId, assetsDir) {
|
|
17
|
+
export async function copyThemeAssets(themeId, assetsDir) {
|
|
17
18
|
const theme = resolveTheme(themeId);
|
|
18
19
|
await mkdir(assetsDir, { recursive: true });
|
|
19
20
|
const cssSource = resolvePackagePath('@lemonppt/themes', 'src', theme, 'styles.css');
|
|
@@ -23,14 +24,39 @@ async function copyThemeAssets(themeId, assetsDir) {
|
|
|
23
24
|
const fontsSource = resolvePackagePath('@lemonppt/renderer', 'assets', 'fonts');
|
|
24
25
|
const fontsDest = path.join(assetsDir, 'fonts');
|
|
25
26
|
await cp(fontsSource, fontsDest, { recursive: true, force: true });
|
|
26
|
-
//
|
|
27
|
-
const clientRenderSource = resolvePackagePath('@lemonppt/renderer', 'dist', 'client-render.js');
|
|
27
|
+
// 复制浏览器可执行 bundle(IIFE 格式),支持静态文件模式下结构编辑
|
|
28
|
+
const clientRenderSource = resolvePackagePath('@lemonppt/renderer', 'dist', 'client', 'client-render.js');
|
|
28
29
|
const clientRenderDest = path.join(assetsDir, 'client-render.js');
|
|
29
30
|
await copyFile(clientRenderSource, clientRenderDest);
|
|
31
|
+
// 复制 ECharts 主题脚本
|
|
32
|
+
const themeEChartsSource = resolvePackagePath('@lemonppt/renderer', 'dist', 'client', 'theme-echarts.js');
|
|
33
|
+
const themeEChartsDest = path.join(assetsDir, 'theme-echarts.js');
|
|
34
|
+
await copyFile(themeEChartsSource, themeEChartsDest);
|
|
30
35
|
// 复制 jQuery,供编辑器初始化自定义滚动条样式类
|
|
31
36
|
const jquerySource = resolvePackagePath('jquery', 'dist', 'jquery.min.js');
|
|
32
37
|
const jqueryDest = path.join(assetsDir, 'jquery.min.js');
|
|
33
38
|
await copyFile(jquerySource, jqueryDest);
|
|
39
|
+
// 复制编辑器交互脚本,供单页编辑器动态初始化
|
|
40
|
+
const editorScriptSource = resolvePackagePath('@lemonppt/renderer', 'dist', 'client', 'editor-script.js');
|
|
41
|
+
const editorScriptDest = path.join(assetsDir, 'editor-script.js');
|
|
42
|
+
await copyFile(editorScriptSource, editorScriptDest);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 把本地媒体文件复制到输出目录的 media/ 下,供 goal.json 引用。
|
|
46
|
+
*/
|
|
47
|
+
export async function stageMediaToFile(options) {
|
|
48
|
+
const outDir = options.outDir || './output';
|
|
49
|
+
const originalName = path.basename(options.filePath);
|
|
50
|
+
const safeName = originalName.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
51
|
+
const mediaDir = path.join(outDir, 'media');
|
|
52
|
+
await mkdir(mediaDir, { recursive: true });
|
|
53
|
+
const localPath = path.resolve(path.join(mediaDir, safeName));
|
|
54
|
+
await copyFile(options.filePath, localPath);
|
|
55
|
+
return {
|
|
56
|
+
filename: safeName,
|
|
57
|
+
localPath,
|
|
58
|
+
relativePath: path.join('media', safeName),
|
|
59
|
+
};
|
|
34
60
|
}
|
|
35
61
|
function resolvePackagePath(pkg, ...segments) {
|
|
36
62
|
// 优先通过 package.json 定位包根(第三方 npm 包)
|
|
@@ -73,22 +99,50 @@ export async function generateGoalToFile(options) {
|
|
|
73
99
|
*/
|
|
74
100
|
export async function readGoalFromFile(filePath) {
|
|
75
101
|
const raw = await readFile(path.resolve(filePath), 'utf-8');
|
|
76
|
-
const parsed = JSON.parse(raw);
|
|
102
|
+
const parsed = preprocessAgentGoal(JSON.parse(raw));
|
|
77
103
|
parsed.theme = resolveTheme(parsed.theme);
|
|
78
104
|
return normalizeDeckGoal(parsed);
|
|
79
105
|
}
|
|
80
106
|
/**
|
|
81
107
|
* 渲染 deck 到输出目录。
|
|
108
|
+
* editable 模式下使用单页编辑器架构:复制 server 的 editor.html/editor.js,
|
|
109
|
+
* 内嵌 renderEditorData 生成的 EditorData,资源路径使用相对路径。
|
|
82
110
|
*/
|
|
83
111
|
export async function renderGoalToDir(goal, options = {}) {
|
|
84
112
|
const { outDir = './output', editable = false, width, height } = options;
|
|
85
113
|
const outputDir = path.resolve(outDir);
|
|
86
114
|
const assetsDir = path.join(outputDir, 'assets');
|
|
87
|
-
const result = renderDeck(goal, { width, height, editable });
|
|
88
115
|
await copyThemeAssets(goal.theme, assetsDir);
|
|
89
|
-
const indexName = editable ? 'editor.html' : 'index.html';
|
|
90
|
-
const indexPath = path.join(outputDir, indexName);
|
|
91
116
|
await mkdir(outputDir, { recursive: true });
|
|
117
|
+
if (editable) {
|
|
118
|
+
const data = renderEditorData(goal, { width: width ?? 1280, height: height ?? 720 });
|
|
119
|
+
const rendererTemplatesDir = resolvePackagePath('@lemonppt/renderer', 'templates');
|
|
120
|
+
let editorHtml = await readFile(path.join(rendererTemplatesDir, 'editor.html'), 'utf-8');
|
|
121
|
+
let editorJs = await readFile(path.join(rendererTemplatesDir, 'editor.js'), 'utf-8');
|
|
122
|
+
// 静态文件模式下使用相对资源路径
|
|
123
|
+
editorHtml = editorHtml.replace(/\/deck\/assets\//g, './assets/');
|
|
124
|
+
editorHtml = editorHtml.replace(/src="\/editor\.js"/g, 'src="./editor.js"');
|
|
125
|
+
// 内嵌 EditorData,让 editor.js 在静态模式下无需请求 API
|
|
126
|
+
const embeddedData = `<script>
|
|
127
|
+
window.__lemonPPT_assetsBase = './assets/';
|
|
128
|
+
window.__lemonPPT_editorData = ${JSON.stringify(data)};
|
|
129
|
+
</script>`;
|
|
130
|
+
editorHtml = editorHtml.replace('</head>', `${embeddedData}\n</head>`);
|
|
131
|
+
const indexPath = path.join(outputDir, 'editor.html');
|
|
132
|
+
await writeFile(indexPath, editorHtml, 'utf-8');
|
|
133
|
+
await writeFile(path.join(outputDir, 'editor.js'), editorJs, 'utf-8');
|
|
134
|
+
const assets = [
|
|
135
|
+
'./assets/fonts/fonts.css',
|
|
136
|
+
`./assets/${data.theme}.css`,
|
|
137
|
+
'./assets/jquery.min.js',
|
|
138
|
+
'./assets/editor-script.js',
|
|
139
|
+
'./assets/client-render.js',
|
|
140
|
+
'./assets/theme-echarts.js',
|
|
141
|
+
];
|
|
142
|
+
return { html: editorHtml, indexPath, assetsDir, assets };
|
|
143
|
+
}
|
|
144
|
+
const result = renderDeck(goal, { width, height });
|
|
145
|
+
const indexPath = path.join(outputDir, 'index.html');
|
|
92
146
|
await writeFile(indexPath, result.html, 'utf-8');
|
|
93
147
|
return { html: result.html, indexPath, assetsDir, assets: result.assets };
|
|
94
148
|
}
|
|
@@ -319,25 +373,25 @@ function normalizePropsWithSchema(props, fields) {
|
|
|
319
373
|
*/
|
|
320
374
|
export async function writeSafePropsToFile(options) {
|
|
321
375
|
const { goalPath, write } = options;
|
|
322
|
-
const
|
|
376
|
+
const preprocessed = preprocessAgentGoal(JSON.parse(await readFile(path.resolve(goalPath), 'utf-8')));
|
|
323
377
|
const { composeDeckFromRaw: compose } = await import('@lemonppt/composer');
|
|
324
378
|
const composed = compose({
|
|
325
|
-
title:
|
|
326
|
-
goal:
|
|
327
|
-
audience:
|
|
328
|
-
owner:
|
|
329
|
-
theme:
|
|
330
|
-
language:
|
|
331
|
-
colorScheme:
|
|
332
|
-
appearance:
|
|
333
|
-
pageCount:
|
|
334
|
-
randomSeed:
|
|
335
|
-
slides:
|
|
379
|
+
title: preprocessed.title,
|
|
380
|
+
goal: preprocessed.goal,
|
|
381
|
+
audience: preprocessed.audience,
|
|
382
|
+
owner: preprocessed.owner,
|
|
383
|
+
theme: preprocessed.theme,
|
|
384
|
+
language: preprocessed.language,
|
|
385
|
+
colorScheme: preprocessed.colorScheme,
|
|
386
|
+
appearance: preprocessed.appearance,
|
|
387
|
+
pageCount: preprocessed.pageCount,
|
|
388
|
+
randomSeed: preprocessed.randomSeed,
|
|
389
|
+
slides: preprocessed.slides.map((s) => ({ role: s.role, layout: s.layout, props: s.props })),
|
|
336
390
|
});
|
|
337
391
|
const layoutChanges = [];
|
|
338
392
|
const unknownFields = [];
|
|
339
393
|
const safeSlides = composed.slides.map((slide, index) => {
|
|
340
|
-
const originalLayout =
|
|
394
|
+
const originalLayout = preprocessed.slides[index]?.layout;
|
|
341
395
|
const registered = getLayout(slide.layout);
|
|
342
396
|
const schema = registered ? getLayoutSchema(slide.layout) : undefined;
|
|
343
397
|
if (originalLayout && originalLayout !== slide.layout) {
|
|
@@ -366,7 +420,7 @@ export async function writeSafePropsToFile(options) {
|
|
|
366
420
|
* 校验 goal.json 规范。
|
|
367
421
|
*/
|
|
368
422
|
export async function validateGoalSpec(goalPath, strict) {
|
|
369
|
-
const raw = JSON.parse(await readFile(path.resolve(goalPath), 'utf-8'));
|
|
423
|
+
const raw = preprocessAgentGoal(JSON.parse(await readFile(path.resolve(goalPath), 'utf-8')));
|
|
370
424
|
const result = {
|
|
371
425
|
valid: false,
|
|
372
426
|
errors: [],
|
|
@@ -402,3 +456,96 @@ export async function validateGoalSpec(goalPath, strict) {
|
|
|
402
456
|
}
|
|
403
457
|
return result;
|
|
404
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* 校验渲染后的 HTML deck 结构是否完整。
|
|
461
|
+
*/
|
|
462
|
+
export async function validateDeck(options) {
|
|
463
|
+
const result = { valid: false, slides: 0, errors: [], warnings: [] };
|
|
464
|
+
const indexPath = path.resolve(options.deckDir, 'index.html');
|
|
465
|
+
if (!existsSync(indexPath)) {
|
|
466
|
+
result.errors.push(`deck index.html not found: ${indexPath}`);
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
const html = await readFile(indexPath, 'utf-8');
|
|
470
|
+
if (html.length < 100) {
|
|
471
|
+
result.errors.push('index.html is too short, render may have failed');
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
// 估算 slide 数量:通过 lp-slide-wrapper 容器
|
|
475
|
+
const wrapperMatches = html.match(/class="[^"]*\blp-slide-wrapper\b[^"]*"/g) ?? [];
|
|
476
|
+
const indexMatches = html.match(/data-slide-index="\d+"/g) ?? [];
|
|
477
|
+
result.slides = Math.max(wrapperMatches.length, indexMatches.length);
|
|
478
|
+
if (result.slides === 0) {
|
|
479
|
+
result.errors.push('no slides found in index.html');
|
|
480
|
+
}
|
|
481
|
+
if (options.goalPath) {
|
|
482
|
+
const goal = JSON.parse(await readFile(path.resolve(options.goalPath), 'utf-8'));
|
|
483
|
+
const expected = goal.slides.length;
|
|
484
|
+
if (result.slides !== expected) {
|
|
485
|
+
result.errors.push(`slide count mismatch: deck=${result.slides}, goal=${expected}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!html.includes('</html>')) {
|
|
489
|
+
result.warnings.push('index.html may be truncated (missing closing </html>)');
|
|
490
|
+
}
|
|
491
|
+
if (result.errors.length === 0) {
|
|
492
|
+
result.valid = true;
|
|
493
|
+
}
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
function collectTextStrings(value) {
|
|
497
|
+
const results = [];
|
|
498
|
+
function walk(v) {
|
|
499
|
+
if (typeof v === 'string') {
|
|
500
|
+
const trimmed = v.trim();
|
|
501
|
+
if (trimmed.length >= 4)
|
|
502
|
+
results.push(trimmed);
|
|
503
|
+
}
|
|
504
|
+
else if (Array.isArray(v)) {
|
|
505
|
+
v.forEach(walk);
|
|
506
|
+
}
|
|
507
|
+
else if (v && typeof v === 'object') {
|
|
508
|
+
Object.values(v).forEach(walk);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
walk(value);
|
|
512
|
+
return [...new Set(results)];
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* 校验 goal.json 中的文案是否都出现在渲染后的 HTML 中。
|
|
516
|
+
*/
|
|
517
|
+
export async function validateGoalCopy(options) {
|
|
518
|
+
const result = { valid: false, missing: [], checked: 0, errors: [] };
|
|
519
|
+
const indexPath = path.resolve(options.deckDir, 'index.html');
|
|
520
|
+
if (!existsSync(indexPath)) {
|
|
521
|
+
result.errors.push(`deck index.html not found: ${indexPath}`);
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
const goal = JSON.parse(await readFile(path.resolve(options.goalPath), 'utf-8'));
|
|
525
|
+
const html = await readFile(indexPath, 'utf-8');
|
|
526
|
+
const texts = collectTextStrings(goal.slides.map((s) => s.props));
|
|
527
|
+
result.checked = texts.length;
|
|
528
|
+
const normalizedHtml = html
|
|
529
|
+
.replace(/&/g, '&')
|
|
530
|
+
.replace(/"/g, '"')
|
|
531
|
+
.replace(/'/g, "'")
|
|
532
|
+
.replace(/</g, '<')
|
|
533
|
+
.replace(/>/g, '>')
|
|
534
|
+
.replace(/\s+/g, ' ');
|
|
535
|
+
for (const text of texts) {
|
|
536
|
+
const normalizedText = text.replace(/\s+/g, ' ');
|
|
537
|
+
if (!normalizedHtml.includes(normalizedText)) {
|
|
538
|
+
result.missing.push(text.slice(0, 80));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (result.missing.length > 0) {
|
|
542
|
+
result.errors.push(`${result.missing.length} / ${result.checked} text snippets missing in rendered HTML`);
|
|
543
|
+
}
|
|
544
|
+
else if (result.checked === 0) {
|
|
545
|
+
result.errors.push('no text found in goal.json to validate');
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
result.valid = true;
|
|
549
|
+
}
|
|
550
|
+
return result;
|
|
551
|
+
}
|
package/dist/install-skill.d.ts
CHANGED
package/dist/install-skill.js
CHANGED
|
@@ -1,51 +1,100 @@
|
|
|
1
1
|
// lemonPPT - AI-powered presentation generation
|
|
2
2
|
// Copyright (c) 2026 lemonforme
|
|
3
3
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
-
import { copyFile, mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { copyFile, cp, mkdir, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
const DEFAULT_AGENTS = ['claude', 'codex', 'cursor'];
|
|
10
10
|
function resolveSkillMdPath() {
|
|
11
|
-
// Published package: SKILL.md lives next to the package root (one level above dist/)
|
|
12
11
|
const mainUrl = import.meta.resolve('@lemonppt/cli');
|
|
13
12
|
const pkgRoot = path.resolve(path.dirname(fileURLToPath(mainUrl)), '..');
|
|
13
|
+
// Local monorepo dev: repo root is two levels above the package root
|
|
14
|
+
const local = path.resolve(pkgRoot, '..', '..', 'SKILL.md');
|
|
15
|
+
if (existsSync(local) && existsSync(path.resolve(pkgRoot, '..', '..', 'apps', 'server'))) {
|
|
16
|
+
return local;
|
|
17
|
+
}
|
|
18
|
+
// Published package: SKILL.md lives next to the package root (one level above dist/)
|
|
14
19
|
const published = path.join(pkgRoot, 'SKILL.md');
|
|
15
20
|
if (existsSync(published)) {
|
|
16
21
|
return published;
|
|
17
22
|
}
|
|
18
|
-
// Local monorepo dev: fallback to repo root SKILL.md
|
|
19
|
-
const local = path.resolve(pkgRoot, '..', '..', 'SKILL.md');
|
|
20
|
-
if (existsSync(local)) {
|
|
21
|
-
return local;
|
|
22
|
-
}
|
|
23
23
|
throw new Error('SKILL.md not found. It should be bundled with @lemonppt/cli or exist at the repo root.');
|
|
24
24
|
}
|
|
25
|
+
function resolveRepoRoot(skillMdPath) {
|
|
26
|
+
return path.dirname(skillMdPath);
|
|
27
|
+
}
|
|
28
|
+
function resolveSkillBundlePath(skillMdPath) {
|
|
29
|
+
const repoRoot = resolveRepoRoot(skillMdPath);
|
|
30
|
+
const bundlePath = path.join(repoRoot, 'skills', 'lemonppt');
|
|
31
|
+
if (existsSync(bundlePath)) {
|
|
32
|
+
return bundlePath;
|
|
33
|
+
}
|
|
34
|
+
throw new Error('Skill bundle not found at skills/lemonppt. Did you run the build script?');
|
|
35
|
+
}
|
|
36
|
+
function isSourceRepo(repoRoot) {
|
|
37
|
+
return existsSync(path.join(repoRoot, 'apps', 'server'));
|
|
38
|
+
}
|
|
39
|
+
function resolveCliWrapper(repoRoot) {
|
|
40
|
+
if (isSourceRepo(repoRoot)) {
|
|
41
|
+
const cliPath = path.join(repoRoot, 'packages', 'cli', 'dist', 'cli.js');
|
|
42
|
+
return {
|
|
43
|
+
type: 'source',
|
|
44
|
+
cli: cliPath,
|
|
45
|
+
sh: `#!/bin/bash\nset -e\nexec "${cliPath}" "$@"\n`,
|
|
46
|
+
ps1: `#Requires -Version 5.1\n& node "${cliPath}" @args\n`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
type: 'published',
|
|
51
|
+
sh: `#!/bin/bash\nset -e\nexec npx @lemonppt/cli "$@"\n`,
|
|
52
|
+
ps1: `#Requires -Version 5.1\n& npx @lemonppt/cli @args\n`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function resolveCliMarker(wrapper) {
|
|
56
|
+
return JSON.stringify(wrapper.type === 'source' && wrapper.cli
|
|
57
|
+
? { type: wrapper.type, cli: wrapper.cli }
|
|
58
|
+
: { type: wrapper.type }, null, 2);
|
|
59
|
+
}
|
|
25
60
|
async function installAgent(agent) {
|
|
26
61
|
const homeDir = os.homedir();
|
|
27
62
|
const skillDir = path.join(homeDir, `.${agent}/skills/lemonppt`);
|
|
28
63
|
await mkdir(skillDir, { recursive: true });
|
|
29
64
|
const skillMdSource = resolveSkillMdPath();
|
|
30
|
-
|
|
31
|
-
await
|
|
65
|
+
const bundlePath = resolveSkillBundlePath(skillMdSource);
|
|
66
|
+
await cp(bundlePath, skillDir, { recursive: true, force: true });
|
|
67
|
+
// 兼容 OpenAI/Codex 等同时读取根目录 openai.yaml 的框架
|
|
68
|
+
const agentYamlSource = path.join(bundlePath, 'agents', 'openai.yaml');
|
|
69
|
+
if (existsSync(agentYamlSource)) {
|
|
70
|
+
await copyFile(agentYamlSource, path.join(skillDir, 'openai.yaml'));
|
|
71
|
+
}
|
|
72
|
+
const repoRoot = resolveRepoRoot(skillMdSource);
|
|
73
|
+
const wrapper = resolveCliWrapper(repoRoot);
|
|
32
74
|
const scriptsDir = path.join(skillDir, 'scripts');
|
|
33
|
-
await
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
`;
|
|
38
|
-
await writeFile(path.join(scriptsDir, 'lemonppt.sh'), wrapperSh, { mode: 0o755 });
|
|
39
|
-
const wrapperPs1 = `#Requires -Version 5.1
|
|
40
|
-
& npx @lemonppt/cli @args
|
|
41
|
-
`;
|
|
42
|
-
await writeFile(path.join(scriptsDir, 'lemonppt.ps1'), wrapperPs1, { mode: 0o755 });
|
|
43
|
-
console.log(`✓ Installed lemonPPT skill for ${agent} at ${skillDir}`);
|
|
75
|
+
await writeFile(path.join(scriptsDir, 'lemonppt.sh'), wrapper.sh, { mode: 0o755 });
|
|
76
|
+
await writeFile(path.join(scriptsDir, 'lemonppt.ps1'), wrapper.ps1, { mode: 0o755 });
|
|
77
|
+
await writeFile(path.join(scriptsDir, '.cli-path.json'), resolveCliMarker(wrapper));
|
|
78
|
+
console.log(`✓ Installed lemonPPT skill for ${agent} at ${skillDir} (${wrapper.type} mode)`);
|
|
44
79
|
}
|
|
45
80
|
/**
|
|
46
81
|
* 将 lemonPPT skill 安装到常见 AI Agent 的技能目录。
|
|
47
82
|
*/
|
|
48
83
|
export async function installSkill(options = {}) {
|
|
84
|
+
if (options.target) {
|
|
85
|
+
const skillMdSource = resolveSkillMdPath();
|
|
86
|
+
const bundlePath = resolveSkillBundlePath(skillMdSource);
|
|
87
|
+
await mkdir(options.target, { recursive: true });
|
|
88
|
+
await cp(bundlePath, options.target, { recursive: true, force: true });
|
|
89
|
+
const repoRoot = resolveRepoRoot(skillMdSource);
|
|
90
|
+
const wrapper = resolveCliWrapper(repoRoot);
|
|
91
|
+
const scriptsDir = path.join(options.target, 'scripts');
|
|
92
|
+
await writeFile(path.join(scriptsDir, 'lemonppt.sh'), wrapper.sh, { mode: 0o755 });
|
|
93
|
+
await writeFile(path.join(scriptsDir, 'lemonppt.ps1'), wrapper.ps1, { mode: 0o755 });
|
|
94
|
+
await writeFile(path.join(scriptsDir, '.cli-path.json'), resolveCliMarker(wrapper));
|
|
95
|
+
console.log(`✓ Copied skill bundle to ${options.target} (${wrapper.type} mode)`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
49
98
|
const agents = options.agents?.length ? options.agents : DEFAULT_AGENTS;
|
|
50
99
|
for (const agent of agents) {
|
|
51
100
|
await installAgent(agent);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lemonppt/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
},
|
|
32
32
|
"homepage": "https://github.com/lemonforme/lemonPPT#readme",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@lemonppt/agent-prompts": "0.2.
|
|
35
|
-
"@lemonppt/
|
|
36
|
-
"@lemonppt/
|
|
37
|
-
"@lemonppt/themes": "0.2.
|
|
34
|
+
"@lemonppt/agent-prompts": "0.2.1",
|
|
35
|
+
"@lemonppt/core": "0.2.1",
|
|
36
|
+
"@lemonppt/renderer": "0.2.1",
|
|
37
|
+
"@lemonppt/themes": "0.2.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^20.0.0",
|