@ohos-ports/slides-grab 1.5.0-beta.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.
- package/LICENSE +21 -0
- package/README-ko.md +292 -0
- package/README.md +301 -0
- package/bin/ohos-postinstall.cjs +118 -0
- package/bin/ppt-agent.js +620 -0
- package/convert.cjs +20 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/index.js +1 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libexpat.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libffi.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libjpeg.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libpcre2-8.so.0 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips-cpp.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/package.json +13 -0
- package/native/@img/sharp-openharmony-arm64/lib/sharp-openharmony-arm64.node +0 -0
- package/native/@img/sharp-openharmony-arm64/package.json +16 -0
- package/package.json +95 -0
- package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
- package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
- package/scripts/build-viewer.js +444 -0
- package/scripts/design-gate.js +258 -0
- package/scripts/download-video.js +213 -0
- package/scripts/editor-server.js +1167 -0
- package/scripts/figma-export.js +169 -0
- package/scripts/generate-image.js +116 -0
- package/scripts/generate-images.js +164 -0
- package/scripts/html2pdf.js +822 -0
- package/scripts/html2png.js +246 -0
- package/scripts/html2pptx.js +162 -0
- package/scripts/import-template.js +86 -0
- package/scripts/install-runtime.js +216 -0
- package/scripts/render-tldraw.js +44 -0
- package/scripts/validate-slides.js +221 -0
- package/skills/slides-grab/SKILL.md +65 -0
- package/skills/slides-grab/references/presentation-workflow-reference.md +61 -0
- package/skills/slides-grab-card-news/SKILL.md +35 -0
- package/skills/slides-grab-design/SKILL.md +88 -0
- package/skills/slides-grab-design/references/beautiful-slide-defaults.md +88 -0
- package/skills/slides-grab-design/references/design-gate.md +349 -0
- package/skills/slides-grab-design/references/design-rules.md +76 -0
- package/skills/slides-grab-design/references/design-system-full.md +568 -0
- package/skills/slides-grab-design/references/detailed-design-rules.md +70 -0
- package/skills/slides-grab-export/SKILL.md +55 -0
- package/skills/slides-grab-export/references/export-rules.md +27 -0
- package/skills/slides-grab-export/references/html2pptx.md +627 -0
- package/skills/slides-grab-export/references/ooxml.md +427 -0
- package/skills/slides-grab-export/references/pptx-skill-reference.md +189 -0
- package/skills/slides-grab-html/SKILL.md +67 -0
- package/skills/slides-grab-image/SKILL.md +82 -0
- package/skills/slides-grab-plan/SKILL.md +68 -0
- package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
- package/skills/slides-grab-plan/references/outline-format.md +47 -0
- package/skills/slides-grab-plan/references/plan-workflow-reference.md +140 -0
- package/src/codex-imagen.js +182 -0
- package/src/design-diversity-data.js +6932 -0
- package/src/design-gate-report.js +244 -0
- package/src/design-gate-state.js +329 -0
- package/src/design-import.js +164 -0
- package/src/design-md-parser.js +415 -0
- package/src/design-styles-data.js +1928 -0
- package/src/design-styles.js +209 -0
- package/src/editor/codex-edit.js +584 -0
- package/src/editor/edit-subprocess.js +170 -0
- package/src/editor/editor-codex-prompt.md +50 -0
- package/src/editor/editor.html +1785 -0
- package/src/editor/js/editor-bbox.js +332 -0
- package/src/editor/js/editor-chat.js +56 -0
- package/src/editor/js/editor-direct-edit.js +238 -0
- package/src/editor/js/editor-dom.js +59 -0
- package/src/editor/js/editor-init.js +405 -0
- package/src/editor/js/editor-navigation.js +54 -0
- package/src/editor/js/editor-select.js +558 -0
- package/src/editor/js/editor-send.js +175 -0
- package/src/editor/js/editor-sse.js +163 -0
- package/src/editor/js/editor-state.js +41 -0
- package/src/editor/js/editor-type.js +71 -0
- package/src/editor/js/editor-utils.js +167 -0
- package/src/editor/js/model-registry.js +37 -0
- package/src/editor/screenshot.js +82 -0
- package/src/export-resolution.cjs +68 -0
- package/src/figma.js +71 -0
- package/src/html2pptx-scale.cjs +120 -0
- package/src/html2pptx.cjs +1262 -0
- package/src/image-contract.js +329 -0
- package/src/image-native.js +468 -0
- package/src/nano-banana.js +841 -0
- package/src/ohos-browser-bridge.cjs +272 -0
- package/src/pptx-raster-export.cjs +299 -0
- package/src/resolve.js +110 -0
- package/src/slide-mode.cjs +72 -0
- package/src/template-fidelity.js +267 -0
- package/src/template-import.js +505 -0
- package/src/template-layout.js +261 -0
- package/src/template-pack.js +255 -0
- package/src/tldraw/render.js +473 -0
- package/src/validation/cli.js +120 -0
- package/src/validation/core.js +1023 -0
- package/templates/chart.html +121 -0
- package/templates/closing.html +54 -0
- package/templates/content.html +50 -0
- package/templates/contents.html +60 -0
- package/templates/cover.html +64 -0
- package/templates/custom/.gitkeep +0 -0
- package/templates/custom/README.md +7 -0
- package/templates/design-styles/README.md +20 -0
- package/templates/design-styles/preview.html +4438 -0
- package/templates/diagram-tldraw.html +56 -0
- package/templates/diagram.html +98 -0
- package/templates/quote.html +31 -0
- package/templates/section-divider.html +43 -0
- package/templates/split-layout.html +43 -0
- package/templates/statistics.html +55 -0
- package/templates/team.html +49 -0
- package/templates/timeline.html +59 -0
package/bin/ppt-agent.js
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { Command, Option } from 'commander';
|
|
8
|
+
import {
|
|
9
|
+
getFigmaImportCaveats,
|
|
10
|
+
getFigmaManualImportInstructions,
|
|
11
|
+
} from '../src/figma.js';
|
|
12
|
+
import { assertDesignGateReady } from '../src/design-gate-state.js';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const packageRoot = resolve(__dirname, '..');
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf-8'));
|
|
17
|
+
const figmaHelpText = [
|
|
18
|
+
'',
|
|
19
|
+
'Creates an experimental / unstable PowerPoint file tuned for Figma Slides manual import.',
|
|
20
|
+
'Treat both PPTX and Figma export as best-effort only.',
|
|
21
|
+
'',
|
|
22
|
+
'Manual import:',
|
|
23
|
+
` ${getFigmaManualImportInstructions()}`,
|
|
24
|
+
'',
|
|
25
|
+
'Figma import caveats:',
|
|
26
|
+
...getFigmaImportCaveats().map((caveat) => ` - ${caveat}`),
|
|
27
|
+
].join('\n');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run a Node.js script from the package, with CWD set to the user's directory.
|
|
31
|
+
* Scripts resolve slide paths via --slides-dir and templates via src/resolve.js.
|
|
32
|
+
*/
|
|
33
|
+
function runNodeScript(relativePath, args = []) {
|
|
34
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
35
|
+
const scriptPath = resolve(packageRoot, relativePath);
|
|
36
|
+
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
37
|
+
cwd: process.cwd(),
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
env: {
|
|
40
|
+
...process.env,
|
|
41
|
+
PPT_AGENT_PACKAGE_ROOT: packageRoot,
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
child.on('error', rejectPromise);
|
|
46
|
+
child.on('close', (code, signal) => {
|
|
47
|
+
if (signal) {
|
|
48
|
+
rejectPromise(new Error(`Command terminated by signal ${signal}`));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
resolvePromise(code ?? 0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runCommand(relativePath, args = []) {
|
|
57
|
+
try {
|
|
58
|
+
const code = await runNodeScript(relativePath, args);
|
|
59
|
+
if (code !== 0) {
|
|
60
|
+
process.exitCode = code;
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`[slides-grab] ${error.message}`);
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function ensureDesignGateForExport(slidesDir, label) {
|
|
69
|
+
try {
|
|
70
|
+
await assertDesignGateReady(resolve(process.cwd(), slidesDir), { label });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(`[slides-grab] ${error.message}`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function collectRepeatedOption(value, previous = []) {
|
|
80
|
+
return [...previous, value];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function reportCliError(error) {
|
|
84
|
+
console.error(`[slides-grab] ${error.message}`);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
const program = new Command();
|
|
90
|
+
|
|
91
|
+
program
|
|
92
|
+
.name('slides-grab')
|
|
93
|
+
.description('Agent-first PPT framework CLI')
|
|
94
|
+
.version(packageJson.version);
|
|
95
|
+
|
|
96
|
+
// --- Core workflow commands ---
|
|
97
|
+
|
|
98
|
+
program
|
|
99
|
+
.command('build-viewer')
|
|
100
|
+
.description('Build viewer.html from slide HTML files')
|
|
101
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
102
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
103
|
+
.action(async (options = {}) => {
|
|
104
|
+
const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
105
|
+
await runCommand('scripts/build-viewer.js', args);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
program
|
|
109
|
+
.command('validate')
|
|
110
|
+
.alias('lint')
|
|
111
|
+
.description('Run structured validation on slide HTML files (Playwright-based)')
|
|
112
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
113
|
+
.option('--format <format>', 'Output format: concise, json, json-full', 'concise')
|
|
114
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
115
|
+
.option('--slide <file>', 'Validate only the named slide file (repeatable)', collectRepeatedOption, [])
|
|
116
|
+
.action(async (options = {}) => {
|
|
117
|
+
const args = ['--slides-dir', options.slidesDir, '--format', options.format, '--mode', options.mode];
|
|
118
|
+
for (const slide of options.slide || []) {
|
|
119
|
+
args.push('--slide', String(slide));
|
|
120
|
+
}
|
|
121
|
+
await runCommand('scripts/validate-slides.js', args);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
program
|
|
125
|
+
.command('design-gate')
|
|
126
|
+
.description('Record the required visual QA gate evidence before export')
|
|
127
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
128
|
+
.option('--slide-mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
129
|
+
.option('--resolution <preset>', 'PNG evidence resolution preset: 720p, 1080p, 1440p, 2160p, or 4k', '2160p')
|
|
130
|
+
.requiredOption('--verdict <verdict>', 'Gate verdict: proceed, revise, or rethink')
|
|
131
|
+
.requiredOption('--pass-a-report <path>', 'Pass A review report file')
|
|
132
|
+
.requiredOption('--pass-b-report <path>', 'Pass B review report file')
|
|
133
|
+
.option('--output-dir <path>', 'PNG evidence directory (default: <slides-dir>/.slides-grab/gate-preview)')
|
|
134
|
+
.action(async (options = {}) => {
|
|
135
|
+
const args = [
|
|
136
|
+
'--slides-dir',
|
|
137
|
+
options.slidesDir,
|
|
138
|
+
'--slide-mode',
|
|
139
|
+
options.slideMode,
|
|
140
|
+
'--resolution',
|
|
141
|
+
options.resolution,
|
|
142
|
+
'--verdict',
|
|
143
|
+
options.verdict,
|
|
144
|
+
'--pass-a-report',
|
|
145
|
+
options.passAReport,
|
|
146
|
+
'--pass-b-report',
|
|
147
|
+
options.passBReport,
|
|
148
|
+
];
|
|
149
|
+
if (options.outputDir) {
|
|
150
|
+
args.push('--output-dir', String(options.outputDir));
|
|
151
|
+
}
|
|
152
|
+
await runCommand('scripts/design-gate.js', args);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
function registerInstallSkillsCommand(commandName) {
|
|
156
|
+
program
|
|
157
|
+
.command(commandName)
|
|
158
|
+
.description('Install slides-grab skills and lightweight runtime adapters for Codex and Claude Code')
|
|
159
|
+
.option('--target <target>', 'Runtime target: all, codex, or claude-code', 'all')
|
|
160
|
+
.option('--runtime <target>', 'Alias for --target')
|
|
161
|
+
.option('--scope <scope>', 'Install scope: project or user', 'project')
|
|
162
|
+
.option('--project-dir <path>', 'Project directory for project scope')
|
|
163
|
+
.option('--target-root <path>', 'Root directory for user-style installs')
|
|
164
|
+
.option('--dry-run', 'Print planned writes without copying')
|
|
165
|
+
.option('--json', 'Print JSON result')
|
|
166
|
+
.action(async (options = {}) => {
|
|
167
|
+
const args = [
|
|
168
|
+
'--target',
|
|
169
|
+
String(options.runtime || options.target),
|
|
170
|
+
'--scope',
|
|
171
|
+
String(options.scope),
|
|
172
|
+
];
|
|
173
|
+
if (options.projectDir) args.push('--project-dir', String(options.projectDir));
|
|
174
|
+
if (options.targetRoot) args.push('--target-root', String(options.targetRoot));
|
|
175
|
+
if (options.dryRun) args.push('--dry-run');
|
|
176
|
+
if (options.json) args.push('--json');
|
|
177
|
+
await runCommand('scripts/install-runtime.js', args);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
registerInstallSkillsCommand('install-skills');
|
|
182
|
+
registerInstallSkillsCommand('install-runtime');
|
|
183
|
+
|
|
184
|
+
program
|
|
185
|
+
.command('convert')
|
|
186
|
+
.description('Convert slide HTML files to experimental / unstable PPTX')
|
|
187
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
188
|
+
.option('--output <path>', 'Output PPTX file')
|
|
189
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
190
|
+
.option('--resolution <preset>', 'Raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p, raster engine only)')
|
|
191
|
+
.addOption(
|
|
192
|
+
new Option('--engine <engine>', 'Export engine: raster (default, highest visual fidelity) or text (editable text with best-effort DOM extraction)')
|
|
193
|
+
.choices(['raster', 'text'])
|
|
194
|
+
.default('raster'),
|
|
195
|
+
)
|
|
196
|
+
.action(async (options = {}) => {
|
|
197
|
+
if (options.engine === 'text' && options.resolution) {
|
|
198
|
+
reportCliError(new Error('--resolution can only be used with --engine raster.'));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!(await ensureDesignGateForExport(options.slidesDir, 'PPTX export'))) return;
|
|
202
|
+
const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
203
|
+
if (options.output) {
|
|
204
|
+
args.push('--output', String(options.output));
|
|
205
|
+
}
|
|
206
|
+
if (options.engine === 'text') {
|
|
207
|
+
await runCommand('scripts/html2pptx.js', args);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (options.resolution) {
|
|
211
|
+
args.push('--resolution', String(options.resolution));
|
|
212
|
+
}
|
|
213
|
+
await runCommand('convert.cjs', args);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
program
|
|
217
|
+
.command('pdf')
|
|
218
|
+
.description('Convert slide HTML files to PDF')
|
|
219
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
220
|
+
.option('--output <path>', 'Output PDF file')
|
|
221
|
+
.option('--mode <mode>', 'PDF export mode: capture for visual fidelity, print for searchable text', 'capture')
|
|
222
|
+
.option('--slide-mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
223
|
+
.option('--resolution <preset>', 'Capture raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p in capture mode)')
|
|
224
|
+
.action(async (options = {}) => {
|
|
225
|
+
if (!(await ensureDesignGateForExport(options.slidesDir, 'PDF export'))) return;
|
|
226
|
+
const args = ['--slides-dir', options.slidesDir];
|
|
227
|
+
if (options.output) {
|
|
228
|
+
args.push('--output', String(options.output));
|
|
229
|
+
}
|
|
230
|
+
if (options.mode) {
|
|
231
|
+
args.push('--mode', String(options.mode));
|
|
232
|
+
}
|
|
233
|
+
if (options.slideMode) {
|
|
234
|
+
args.push('--slide-mode', String(options.slideMode));
|
|
235
|
+
}
|
|
236
|
+
if (options.resolution) {
|
|
237
|
+
args.push('--resolution', String(options.resolution));
|
|
238
|
+
}
|
|
239
|
+
await runCommand('scripts/html2pdf.js', args);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
program
|
|
243
|
+
.command('png')
|
|
244
|
+
.description('Render slide HTML files to one PNG per slide')
|
|
245
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
246
|
+
.option('--output-dir <path>', 'Output directory for PNG files (default: <slides-dir>/out-png)')
|
|
247
|
+
.option('--slide-mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
248
|
+
.option('--resolution <preset>', 'Raster size preset: 720p, 1080p, 1440p, 2160p, or 4k', '2160p')
|
|
249
|
+
.action(async (options = {}) => {
|
|
250
|
+
const args = ['--slides-dir', options.slidesDir];
|
|
251
|
+
if (options.outputDir) {
|
|
252
|
+
args.push('--output-dir', String(options.outputDir));
|
|
253
|
+
}
|
|
254
|
+
if (options.slideMode) {
|
|
255
|
+
args.push('--slide-mode', String(options.slideMode));
|
|
256
|
+
}
|
|
257
|
+
if (options.resolution) {
|
|
258
|
+
args.push('--resolution', String(options.resolution));
|
|
259
|
+
}
|
|
260
|
+
await runCommand('scripts/html2png.js', args);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
program
|
|
264
|
+
.command('fetch-video')
|
|
265
|
+
.description('Download a video into <slides-dir>/assets via yt-dlp and print the ./assets reference')
|
|
266
|
+
.requiredOption('--url <url>', 'Video page URL to download with yt-dlp')
|
|
267
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
268
|
+
.option('--output-name <name>', 'Optional output stem inside <slides-dir>/assets/')
|
|
269
|
+
.action(async (options = {}) => {
|
|
270
|
+
const args = ['--url', String(options.url), '--slides-dir', options.slidesDir];
|
|
271
|
+
if (options.outputName) {
|
|
272
|
+
args.push('--output-name', String(options.outputName));
|
|
273
|
+
}
|
|
274
|
+
await runCommand('scripts/download-video.js', args);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
program
|
|
278
|
+
.command('figma')
|
|
279
|
+
.description('Export an experimental / unstable Figma Slides importable PPTX')
|
|
280
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
281
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
282
|
+
.option('--output <path>', 'Output PPTX file (default: <slides-dir>-figma.pptx)')
|
|
283
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
284
|
+
.addHelpText('after', figmaHelpText)
|
|
285
|
+
.action(async (options = {}) => {
|
|
286
|
+
if (!(await ensureDesignGateForExport(options.slidesDir, 'Figma export'))) return;
|
|
287
|
+
const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
288
|
+
if (options.output) {
|
|
289
|
+
args.push('--output', String(options.output));
|
|
290
|
+
}
|
|
291
|
+
await runCommand('scripts/figma-export.js', args);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
program
|
|
295
|
+
.command('tldraw')
|
|
296
|
+
.description('Render a current-format .tldr or store-snapshot JSON file to an exact-size SVG asset for slides')
|
|
297
|
+
.option('--input <path>', 'Input current-format .tldr or snapshot JSON file')
|
|
298
|
+
.option('--output <path>', 'Output SVG asset path')
|
|
299
|
+
.option('--width <number>', 'Exact output width in CSS pixels')
|
|
300
|
+
.option('--height <number>', 'Exact output height in CSS pixels')
|
|
301
|
+
.option('--padding <number>', 'Inner fit padding in CSS pixels')
|
|
302
|
+
.option('--background <css>', 'Optional wrapper background fill')
|
|
303
|
+
.option('--page-id <id>', 'Optional tldraw page id to export')
|
|
304
|
+
.action(async (options = {}) => {
|
|
305
|
+
const args = [];
|
|
306
|
+
if (options.input) args.push('--input', String(options.input));
|
|
307
|
+
if (options.output) args.push('--output', String(options.output));
|
|
308
|
+
if (options.width) args.push('--width', String(options.width));
|
|
309
|
+
if (options.height) args.push('--height', String(options.height));
|
|
310
|
+
if (options.padding) args.push('--padding', String(options.padding));
|
|
311
|
+
if (options.background) args.push('--background', String(options.background));
|
|
312
|
+
if (options.pageId) args.push('--page-id', String(options.pageId));
|
|
313
|
+
await runCommand('scripts/render-tldraw.js', args);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
program
|
|
317
|
+
.command('image')
|
|
318
|
+
.description('Generate a local slide image asset (default: codex-imagen via your Codex ChatGPT login — no OpenAI/Google API key required)')
|
|
319
|
+
.option('--prompt <text>', 'Prompt for image generation')
|
|
320
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
321
|
+
.option('--output <path>', 'Optional output path inside <slides-dir>/assets/')
|
|
322
|
+
.option('--name <slug>', 'Optional asset basename without extension')
|
|
323
|
+
.option('--image-native', 'Also create an editor-compatible slide-XX.html wrapper and regeneration metadata; requires --name slide-XX')
|
|
324
|
+
.option('--provider <name>', 'Image provider: codex (default), openai, or nano-banana. Aliases: codex-cli → codex, openai → openai, gemini → nano-banana')
|
|
325
|
+
.option('--model <id>', 'Model id (default: gpt-5.4 for codex, gpt-image-2 for openai, gemini-3-pro-image-preview for nano-banana)')
|
|
326
|
+
.option('--aspect-ratio <ratio>', 'Aspect ratio; for codex it is injected as a prompt hint, for openai it maps to the nearest supported OpenAI size (default: 16:9)')
|
|
327
|
+
.option('--image-size <size>', 'Nano Banana image size preset: 2K or 4K (default: 4K)')
|
|
328
|
+
.option('--base-url <url>', 'OpenAI-compatible base URL (default: OPENAI_IMAGE_BASE_URL, OPENAI_BASE_URL, then OpenAI)')
|
|
329
|
+
.option('--api-key-env <name>', 'Env var to read for OpenAI-compatible provider API key')
|
|
330
|
+
.option('--reference <path>', 'Reference image path(s) for style-guided generation (codex only, repeatable)', collectRepeatedOption, [])
|
|
331
|
+
.addHelpText('after', [
|
|
332
|
+
'',
|
|
333
|
+
'Auth:',
|
|
334
|
+
' Default (codex): run `codex login` once to populate ~/.codex/auth.json. No OpenAI/Google API key required;',
|
|
335
|
+
' requires a Codex/ChatGPT account entitled to image generation.',
|
|
336
|
+
' OpenAI provider: set OPENAI_API_KEY. Compatible endpoints may use --base-url, OPENAI_IMAGE_BASE_URL, OPENAI_BASE_URL, and --api-key-env.',
|
|
337
|
+
' Nano Banana provider: set GOOGLE_API_KEY or GEMINI_API_KEY.',
|
|
338
|
+
'',
|
|
339
|
+
'WARNING: codex-imagen calls an unsupported private Codex backend that may break without notice.',
|
|
340
|
+
].join('\n'))
|
|
341
|
+
.action(async (options = {}) => {
|
|
342
|
+
const args = ['--slides-dir', options.slidesDir];
|
|
343
|
+
if (options.prompt) args.push('--prompt', String(options.prompt));
|
|
344
|
+
if (options.output) args.push('--output', String(options.output));
|
|
345
|
+
if (options.name) args.push('--name', String(options.name));
|
|
346
|
+
if (options.imageNative) args.push('--image-native');
|
|
347
|
+
if (options.provider) args.push('--provider', String(options.provider));
|
|
348
|
+
if (options.model) args.push('--model', String(options.model));
|
|
349
|
+
if (options.aspectRatio) args.push('--aspect-ratio', String(options.aspectRatio));
|
|
350
|
+
if (options.imageSize) args.push('--image-size', String(options.imageSize));
|
|
351
|
+
if (options.baseUrl) args.push('--base-url', String(options.baseUrl));
|
|
352
|
+
if (options.apiKeyEnv) args.push('--api-key-env', String(options.apiKeyEnv));
|
|
353
|
+
for (const ref of options.reference || []) {
|
|
354
|
+
args.push('--reference', String(ref));
|
|
355
|
+
}
|
|
356
|
+
await runCommand('scripts/generate-image.js', args);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
function registerEditorCommand(commandName, editorType, description) {
|
|
360
|
+
program
|
|
361
|
+
.command(commandName)
|
|
362
|
+
.description(description)
|
|
363
|
+
.option('--port <number>', 'Server port')
|
|
364
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
365
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
366
|
+
.action(async (options = {}) => {
|
|
367
|
+
const args = ['--editor', editorType, '--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
368
|
+
if (options.port) {
|
|
369
|
+
args.push('--port', String(options.port));
|
|
370
|
+
}
|
|
371
|
+
await runCommand('scripts/editor-server.js', args);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
registerEditorCommand('edit', 'html', 'Start interactive HTML slide editor with Codex image-based edit flow');
|
|
376
|
+
registerEditorCommand('edit-image', 'image', 'Start interactive image-native slide editor');
|
|
377
|
+
|
|
378
|
+
// --- Template/style discovery commands ---
|
|
379
|
+
|
|
380
|
+
program
|
|
381
|
+
.command('list-templates')
|
|
382
|
+
.description('List all available slide templates (local overrides + package built-ins)')
|
|
383
|
+
.action(async () => {
|
|
384
|
+
const { listTemplates } = await import('../src/resolve.js');
|
|
385
|
+
const templates = listTemplates();
|
|
386
|
+
if (templates.length === 0) {
|
|
387
|
+
console.log('No templates found.');
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
console.log('Available templates:\n');
|
|
391
|
+
for (const t of templates) {
|
|
392
|
+
const tag = t.source === 'local' ? '(local)' : '(built-in)';
|
|
393
|
+
console.log(` ${t.name.padEnd(20)} ${tag}`);
|
|
394
|
+
}
|
|
395
|
+
console.log(`\nTotal: ${templates.length} templates`);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
program
|
|
399
|
+
.command('list-styles')
|
|
400
|
+
.description('List bundled design styles agents and users can reference during slide generation')
|
|
401
|
+
.option('--all', 'Include source-alias styles that resolve to a builtin (hidden by default)')
|
|
402
|
+
.action(async (options = {}) => {
|
|
403
|
+
try {
|
|
404
|
+
const { listDesignStyles, listSelectableDesignStyles } = await import('../src/design-styles.js');
|
|
405
|
+
const showAll = Boolean(options.all);
|
|
406
|
+
const styles = showAll ? listDesignStyles() : listSelectableDesignStyles();
|
|
407
|
+
|
|
408
|
+
if (styles.length === 0) {
|
|
409
|
+
console.log('No bundled design styles found.');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log('Available design styles:\n');
|
|
414
|
+
for (const style of styles) {
|
|
415
|
+
let tag;
|
|
416
|
+
if (style.classification === 'builtin') {
|
|
417
|
+
tag = '[builtin]';
|
|
418
|
+
} else if (style.classification === 'source-variant') {
|
|
419
|
+
tag = '[variant]';
|
|
420
|
+
} else if (style.classification === 'source-alias') {
|
|
421
|
+
tag = `[alias -> ${style.aliasOf}]`;
|
|
422
|
+
} else {
|
|
423
|
+
tag = '[new]';
|
|
424
|
+
}
|
|
425
|
+
console.log(` ${style.id.padEnd(38)} ${tag} ${style.title}`);
|
|
426
|
+
console.log(` ${style.mood} · ${style.bestFor}`);
|
|
427
|
+
if (style.classification === 'source-variant' && Array.isArray(style.relatedStyleIds) && style.relatedStyleIds.length) {
|
|
428
|
+
console.log(` related: ${style.relatedStyleIds.join(', ')}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const totalResolvable = listDesignStyles().length;
|
|
433
|
+
if (!showAll) {
|
|
434
|
+
const hiddenCount = totalResolvable - styles.length;
|
|
435
|
+
console.log(`\nTotal: ${styles.length} selectable styles`);
|
|
436
|
+
console.log(`${hiddenCount} source-alias slug(s) hidden (resolve to a builtin); use --all to show all ${totalResolvable} resolvable styles`);
|
|
437
|
+
} else {
|
|
438
|
+
const selectableCount = listSelectableDesignStyles().length;
|
|
439
|
+
const aliasCount = totalResolvable - selectableCount;
|
|
440
|
+
console.log(`\nTotal: ${styles.length} resolvable styles (${selectableCount} selectable, ${aliasCount} aliases)`);
|
|
441
|
+
}
|
|
442
|
+
console.log('Preview: slides-grab preview-styles [--style <id>]');
|
|
443
|
+
} catch (error) {
|
|
444
|
+
reportCliError(error);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
program
|
|
449
|
+
.command('preview-styles')
|
|
450
|
+
.description('Print the path to the bundled 95-style visual preview gallery')
|
|
451
|
+
.action(async () => {
|
|
452
|
+
try {
|
|
453
|
+
const { getPreviewHtmlPath } = await import('../src/design-styles.js');
|
|
454
|
+
const previewPath = getPreviewHtmlPath();
|
|
455
|
+
console.log(previewPath);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
reportCliError(error);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
program
|
|
462
|
+
.command('import-template')
|
|
463
|
+
.description('Import one or more template/reference source files (.pptx, .pdf, .html/.htm, image) into a local template pack under <slides-dir>/.slides-grab/. Local analysis only — no network, no LLM. PPTX/PDF are accepted as metadata/visual-reference dispatch with warnings; full rendering is not performed locally.')
|
|
464
|
+
.option('--input <path>', 'Template source file (repeatable)', collectRepeatedOption, [])
|
|
465
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
466
|
+
.option('--name <name>', 'Optional template pack display name')
|
|
467
|
+
.action(async (options = {}) => {
|
|
468
|
+
const args = ['--slides-dir', options.slidesDir];
|
|
469
|
+
for (const input of options.input || []) {
|
|
470
|
+
args.push('--input', String(input));
|
|
471
|
+
}
|
|
472
|
+
if (options.name) {
|
|
473
|
+
args.push('--name', String(options.name));
|
|
474
|
+
}
|
|
475
|
+
await runCommand('scripts/import-template.js', args);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
program
|
|
479
|
+
.command('import-design')
|
|
480
|
+
.description('Fetch a remote DESIGN.md (https only) and save it locally as a custom slide design source')
|
|
481
|
+
.argument('<url>', 'HTTPS URL to a DESIGN.md / markdown design system file')
|
|
482
|
+
.option('--output <path>', 'Output path for the saved file', 'DESIGN.md')
|
|
483
|
+
.option('--max-bytes <number>', 'Maximum allowed response size in bytes', '262144')
|
|
484
|
+
.option('--timeout-ms <number>', 'Fetch timeout in milliseconds', '15000')
|
|
485
|
+
.action(async (url, options = {}) => {
|
|
486
|
+
try {
|
|
487
|
+
const { fetchDesignMarkdown, formatImportedDesignMarkdown, saveImportedDesign } =
|
|
488
|
+
await import('../src/design-import.js');
|
|
489
|
+
const maxBytes = Number(options.maxBytes) || 262144;
|
|
490
|
+
const timeoutMs = Number(options.timeoutMs) || 15000;
|
|
491
|
+
const result = await fetchDesignMarkdown(url, { maxBytes, timeoutMs });
|
|
492
|
+
const markdown = formatImportedDesignMarkdown({
|
|
493
|
+
url: result.url,
|
|
494
|
+
content: result.text,
|
|
495
|
+
fetchedAt: result.fetchedAt,
|
|
496
|
+
});
|
|
497
|
+
const savedPath = saveImportedDesign({ outputPath: options.output, markdown });
|
|
498
|
+
console.log(`Saved ${result.bytes} bytes to ${savedPath}`);
|
|
499
|
+
console.log(
|
|
500
|
+
'Saved web-flavored design source. Next convert it to DESIGN.slides.md, then set ' +
|
|
501
|
+
'"style: ./DESIGN.slides.md" in your slide-outline.md.',
|
|
502
|
+
);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
reportCliError(error);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
program
|
|
509
|
+
.command('show-design')
|
|
510
|
+
.description('Parse a local design markdown file (DESIGN.slides.md preferred, falls back to DESIGN.md) or a bundled style id and print the slides-grab design summary the agent will use')
|
|
511
|
+
.argument('<ref>', 'Path to DESIGN.slides.md / DESIGN.md / a directory containing one, or a bundled style id (e.g. glassmorphism)')
|
|
512
|
+
.action(async (ref) => {
|
|
513
|
+
try {
|
|
514
|
+
const { loadDesignStyleRef, detectLocalDesignMarkdown } = await import('../src/design-styles.js');
|
|
515
|
+
const { renderDesignStyleForPrompt } = await import('../src/design-md-parser.js');
|
|
516
|
+
const { existsSync, statSync } = await import('node:fs');
|
|
517
|
+
const { dirname, resolve } = await import('node:path');
|
|
518
|
+
|
|
519
|
+
let effectiveRef = ref;
|
|
520
|
+
let detection = null;
|
|
521
|
+
let explicitWebShadowed = false;
|
|
522
|
+
const absoluteRef = resolve(ref);
|
|
523
|
+
if (existsSync(absoluteRef) && statSync(absoluteRef).isDirectory()) {
|
|
524
|
+
detection = detectLocalDesignMarkdown({ baseDir: absoluteRef });
|
|
525
|
+
if (!detection.path) {
|
|
526
|
+
console.error(`No DESIGN.slides.md or DESIGN.md found in directory "${ref}".`);
|
|
527
|
+
process.exitCode = 1;
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
effectiveRef = detection.path;
|
|
531
|
+
} else if (existsSync(absoluteRef) && statSync(absoluteRef).isFile() && absoluteRef.endsWith('/DESIGN.md')) {
|
|
532
|
+
const siblingDetection = detectLocalDesignMarkdown({ baseDir: dirname(absoluteRef) });
|
|
533
|
+
if (siblingDetection.kind === 'slides') {
|
|
534
|
+
detection = siblingDetection;
|
|
535
|
+
explicitWebShadowed = true;
|
|
536
|
+
effectiveRef = siblingDetection.path;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const style = loadDesignStyleRef(effectiveRef);
|
|
541
|
+
if (!style) {
|
|
542
|
+
console.error(`No design style or design markdown file found at "${ref}".`);
|
|
543
|
+
process.exitCode = 1;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const isCustom = style?.source?.type === 'design-md';
|
|
547
|
+
if (isCustom) {
|
|
548
|
+
if (detection) {
|
|
549
|
+
const activeName = detection.kind === 'slides' ? 'DESIGN.slides.md' : 'DESIGN.md';
|
|
550
|
+
const otherPresent = detection.kind === 'slides' && detection.webPath;
|
|
551
|
+
console.log(`# Active file: ${activeName} (${detection.path})`);
|
|
552
|
+
if (otherPresent) {
|
|
553
|
+
const explicitPrefix = explicitWebShadowed ? ' explicit DESIGN.md reference was redirected;' : '';
|
|
554
|
+
console.log(`# Note:${explicitPrefix} DESIGN.md is also present at ${detection.webPath} but is shadowed by DESIGN.slides.md.`);
|
|
555
|
+
} else if (detection.kind === 'web') {
|
|
556
|
+
console.log('# Note: only the web-flavored DESIGN.md was found. Consider converting it to DESIGN.slides.md before generating slides (see skills/slides-grab-plan/references/design-md-to-slides-conversion.md).');
|
|
557
|
+
}
|
|
558
|
+
console.log('');
|
|
559
|
+
}
|
|
560
|
+
console.log(renderDesignStyleForPrompt(style));
|
|
561
|
+
} else {
|
|
562
|
+
console.log(`# Bundled style: ${style.title} (${style.id})`);
|
|
563
|
+
console.log(`Mood: ${style.mood}`);
|
|
564
|
+
console.log(`Best for: ${style.bestFor}`);
|
|
565
|
+
if (style.source?.repo) console.log(`Source: ${style.source.repo}`);
|
|
566
|
+
if (style.source?.url) console.log(`Source URL: ${style.source.url}`);
|
|
567
|
+
if (style.classification) console.log(`Classification: ${style.classification}`);
|
|
568
|
+
if (style.aliasOf) console.log(`Alias of: ${style.aliasOf}`);
|
|
569
|
+
if (Array.isArray(style.aliases) && style.aliases.length) console.log(`Aliases: ${style.aliases.join(', ')}`);
|
|
570
|
+
if (Array.isArray(style.relatedStyleIds) && style.relatedStyleIds.length) console.log(`Related styles: ${style.relatedStyleIds.join(', ')}`);
|
|
571
|
+
if (Array.isArray(style.background)) {
|
|
572
|
+
console.log('\n## Background');
|
|
573
|
+
for (const b of style.background) console.log(`- ${b}`);
|
|
574
|
+
}
|
|
575
|
+
if (Array.isArray(style.colors)) {
|
|
576
|
+
console.log('\n## Colors');
|
|
577
|
+
for (const c of style.colors) console.log(`- ${c.role}: ${c.label} (${c.hex})`);
|
|
578
|
+
}
|
|
579
|
+
if (Array.isArray(style.fonts)) {
|
|
580
|
+
console.log('\n## Typography');
|
|
581
|
+
for (const f of style.fonts) console.log(`- ${f}`);
|
|
582
|
+
}
|
|
583
|
+
if (Array.isArray(style.layout)) {
|
|
584
|
+
console.log('\n## Layout');
|
|
585
|
+
for (const l of style.layout) console.log(`- ${l}`);
|
|
586
|
+
}
|
|
587
|
+
if (Array.isArray(style.signature)) {
|
|
588
|
+
console.log('\n## Signature');
|
|
589
|
+
for (const s of style.signature) console.log(`- ${s}`);
|
|
590
|
+
}
|
|
591
|
+
if (Array.isArray(style.avoid)) {
|
|
592
|
+
console.log('\n## Avoid');
|
|
593
|
+
for (const a of style.avoid) console.log(`- ${a}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
} catch (error) {
|
|
597
|
+
reportCliError(error);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
program
|
|
602
|
+
.command('show-template')
|
|
603
|
+
.description('Print the contents of a template file')
|
|
604
|
+
.argument('<name>', 'Template name (e.g. "cover", "content", "chart")')
|
|
605
|
+
.action(async (name) => {
|
|
606
|
+
const { resolveTemplate } = await import('../src/resolve.js');
|
|
607
|
+
const result = resolveTemplate(name);
|
|
608
|
+
if (!result) {
|
|
609
|
+
console.error(`Template "${name}" not found.`);
|
|
610
|
+
process.exitCode = 1;
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const content = readFileSync(result.path, 'utf-8');
|
|
614
|
+
console.log(`# Template: ${name} (${result.source})`);
|
|
615
|
+
console.log(`# Path: ${result.path}\n`);
|
|
616
|
+
console.log(content);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
await program.parseAsync(process.argv);
|
package/convert.cjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const {
|
|
2
|
+
buildPageOptions,
|
|
3
|
+
getTargetRasterSize,
|
|
4
|
+
main,
|
|
5
|
+
parseArgs,
|
|
6
|
+
} = require('./src/pptx-raster-export.cjs');
|
|
7
|
+
|
|
8
|
+
if (require.main === module) {
|
|
9
|
+
main().catch((err) => {
|
|
10
|
+
console.error('Fatal error:', err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
buildPageOptions,
|
|
17
|
+
getTargetRasterSize,
|
|
18
|
+
main,
|
|
19
|
+
parseArgs,
|
|
20
|
+
};
|