@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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README-ko.md +292 -0
  3. package/README.md +301 -0
  4. package/bin/ohos-postinstall.cjs +118 -0
  5. package/bin/ppt-agent.js +620 -0
  6. package/convert.cjs +20 -0
  7. package/native/@img/sharp-libvips-openharmony-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
  8. package/native/@img/sharp-libvips-openharmony-arm64/lib/index.js +1 -0
  9. package/native/@img/sharp-libvips-openharmony-arm64/lib/libexpat.so +0 -0
  10. package/native/@img/sharp-libvips-openharmony-arm64/lib/libffi.so +0 -0
  11. package/native/@img/sharp-libvips-openharmony-arm64/lib/libjpeg.so +0 -0
  12. package/native/@img/sharp-libvips-openharmony-arm64/lib/libpcre2-8.so.0 +0 -0
  13. package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips-cpp.so.42.20.3 +0 -0
  14. package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips.so.42.20.3 +0 -0
  15. package/native/@img/sharp-libvips-openharmony-arm64/package.json +13 -0
  16. package/native/@img/sharp-openharmony-arm64/lib/sharp-openharmony-arm64.node +0 -0
  17. package/native/@img/sharp-openharmony-arm64/package.json +16 -0
  18. package/package.json +95 -0
  19. package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
  20. package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
  21. package/scripts/build-viewer.js +444 -0
  22. package/scripts/design-gate.js +258 -0
  23. package/scripts/download-video.js +213 -0
  24. package/scripts/editor-server.js +1167 -0
  25. package/scripts/figma-export.js +169 -0
  26. package/scripts/generate-image.js +116 -0
  27. package/scripts/generate-images.js +164 -0
  28. package/scripts/html2pdf.js +822 -0
  29. package/scripts/html2png.js +246 -0
  30. package/scripts/html2pptx.js +162 -0
  31. package/scripts/import-template.js +86 -0
  32. package/scripts/install-runtime.js +216 -0
  33. package/scripts/render-tldraw.js +44 -0
  34. package/scripts/validate-slides.js +221 -0
  35. package/skills/slides-grab/SKILL.md +65 -0
  36. package/skills/slides-grab/references/presentation-workflow-reference.md +61 -0
  37. package/skills/slides-grab-card-news/SKILL.md +35 -0
  38. package/skills/slides-grab-design/SKILL.md +88 -0
  39. package/skills/slides-grab-design/references/beautiful-slide-defaults.md +88 -0
  40. package/skills/slides-grab-design/references/design-gate.md +349 -0
  41. package/skills/slides-grab-design/references/design-rules.md +76 -0
  42. package/skills/slides-grab-design/references/design-system-full.md +568 -0
  43. package/skills/slides-grab-design/references/detailed-design-rules.md +70 -0
  44. package/skills/slides-grab-export/SKILL.md +55 -0
  45. package/skills/slides-grab-export/references/export-rules.md +27 -0
  46. package/skills/slides-grab-export/references/html2pptx.md +627 -0
  47. package/skills/slides-grab-export/references/ooxml.md +427 -0
  48. package/skills/slides-grab-export/references/pptx-skill-reference.md +189 -0
  49. package/skills/slides-grab-html/SKILL.md +67 -0
  50. package/skills/slides-grab-image/SKILL.md +82 -0
  51. package/skills/slides-grab-plan/SKILL.md +68 -0
  52. package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
  53. package/skills/slides-grab-plan/references/outline-format.md +47 -0
  54. package/skills/slides-grab-plan/references/plan-workflow-reference.md +140 -0
  55. package/src/codex-imagen.js +182 -0
  56. package/src/design-diversity-data.js +6932 -0
  57. package/src/design-gate-report.js +244 -0
  58. package/src/design-gate-state.js +329 -0
  59. package/src/design-import.js +164 -0
  60. package/src/design-md-parser.js +415 -0
  61. package/src/design-styles-data.js +1928 -0
  62. package/src/design-styles.js +209 -0
  63. package/src/editor/codex-edit.js +584 -0
  64. package/src/editor/edit-subprocess.js +170 -0
  65. package/src/editor/editor-codex-prompt.md +50 -0
  66. package/src/editor/editor.html +1785 -0
  67. package/src/editor/js/editor-bbox.js +332 -0
  68. package/src/editor/js/editor-chat.js +56 -0
  69. package/src/editor/js/editor-direct-edit.js +238 -0
  70. package/src/editor/js/editor-dom.js +59 -0
  71. package/src/editor/js/editor-init.js +405 -0
  72. package/src/editor/js/editor-navigation.js +54 -0
  73. package/src/editor/js/editor-select.js +558 -0
  74. package/src/editor/js/editor-send.js +175 -0
  75. package/src/editor/js/editor-sse.js +163 -0
  76. package/src/editor/js/editor-state.js +41 -0
  77. package/src/editor/js/editor-type.js +71 -0
  78. package/src/editor/js/editor-utils.js +167 -0
  79. package/src/editor/js/model-registry.js +37 -0
  80. package/src/editor/screenshot.js +82 -0
  81. package/src/export-resolution.cjs +68 -0
  82. package/src/figma.js +71 -0
  83. package/src/html2pptx-scale.cjs +120 -0
  84. package/src/html2pptx.cjs +1262 -0
  85. package/src/image-contract.js +329 -0
  86. package/src/image-native.js +468 -0
  87. package/src/nano-banana.js +841 -0
  88. package/src/ohos-browser-bridge.cjs +272 -0
  89. package/src/pptx-raster-export.cjs +299 -0
  90. package/src/resolve.js +110 -0
  91. package/src/slide-mode.cjs +72 -0
  92. package/src/template-fidelity.js +267 -0
  93. package/src/template-import.js +505 -0
  94. package/src/template-layout.js +261 -0
  95. package/src/template-pack.js +255 -0
  96. package/src/tldraw/render.js +473 -0
  97. package/src/validation/cli.js +120 -0
  98. package/src/validation/core.js +1023 -0
  99. package/templates/chart.html +121 -0
  100. package/templates/closing.html +54 -0
  101. package/templates/content.html +50 -0
  102. package/templates/contents.html +60 -0
  103. package/templates/cover.html +64 -0
  104. package/templates/custom/.gitkeep +0 -0
  105. package/templates/custom/README.md +7 -0
  106. package/templates/design-styles/README.md +20 -0
  107. package/templates/design-styles/preview.html +4438 -0
  108. package/templates/diagram-tldraw.html +56 -0
  109. package/templates/diagram.html +98 -0
  110. package/templates/quote.html +31 -0
  111. package/templates/section-divider.html +43 -0
  112. package/templates/split-layout.html +43 -0
  113. package/templates/statistics.html +55 -0
  114. package/templates/team.html +49 -0
  115. package/templates/timeline.html +59 -0
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, readdir } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { join, resolve } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+ import { chromium } from 'playwright';
8
+
9
+ import { ensureSlidesPassValidation } from './validate-slides.js';
10
+
11
+ const require = createRequire(import.meta.url);
12
+ const {
13
+ getResolutionChoices,
14
+ getResolutionSize,
15
+ normalizeResolutionPreset,
16
+ } = require('../src/export-resolution.cjs');
17
+ const {
18
+ DEFAULT_SLIDE_MODE,
19
+ getSlideModeChoices,
20
+ getSlideModeConfig,
21
+ normalizeSlideMode,
22
+ } = require('../src/slide-mode.cjs');
23
+
24
+ const DEFAULT_SLIDES_DIR = 'slides';
25
+ const DEFAULT_RESOLUTION = '2160p';
26
+ const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
27
+ const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
28
+ const RENDER_SETTLE_MS = 120;
29
+
30
+ function printUsage() {
31
+ process.stdout.write(
32
+ [
33
+ 'Usage: node scripts/html2png.js [options]',
34
+ '',
35
+ 'Options:',
36
+ ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
37
+ ' --output-dir <path> Output directory for PNG files (default: <slides-dir>/out-png)',
38
+ ` --slide-mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
39
+ ` --resolution <preset> Raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_RESOLUTION})`,
40
+ ' -h, --help Show this help message',
41
+ '',
42
+ 'Examples:',
43
+ ' node scripts/html2png.js --slides-dir slides',
44
+ ' node scripts/html2png.js --slides-dir cards --slide-mode card-news',
45
+ ' node scripts/html2png.js --slides-dir slides --resolution 1440p',
46
+ ].join('\n'),
47
+ );
48
+ process.stdout.write('\n');
49
+ }
50
+
51
+ function readOptionValue(args, index, optionName) {
52
+ const next = args[index + 1];
53
+ if (!next || next.startsWith('-')) {
54
+ throw new Error(`Missing value for ${optionName}.`);
55
+ }
56
+ return next;
57
+ }
58
+
59
+ function toSlideOrder(fileName) {
60
+ const match = fileName.match(/\d+/);
61
+ return match ? Number.parseInt(match[0], 10) : Number.POSITIVE_INFINITY;
62
+ }
63
+
64
+ function sortSlideFiles(a, b) {
65
+ const orderA = toSlideOrder(a);
66
+ const orderB = toSlideOrder(b);
67
+ if (orderA !== orderB) return orderA - orderB;
68
+ return a.localeCompare(b);
69
+ }
70
+
71
+ function parseCliArgs(args) {
72
+ const options = {
73
+ slidesDir: DEFAULT_SLIDES_DIR,
74
+ outputDir: '',
75
+ slideMode: DEFAULT_SLIDE_MODE,
76
+ resolution: DEFAULT_RESOLUTION,
77
+ help: false,
78
+ };
79
+
80
+ for (let i = 0; i < args.length; i += 1) {
81
+ const arg = args[i];
82
+
83
+ if (arg === '-h' || arg === '--help') {
84
+ options.help = true;
85
+ continue;
86
+ }
87
+
88
+ if (arg === '--slides-dir') {
89
+ options.slidesDir = readOptionValue(args, i, '--slides-dir');
90
+ i += 1;
91
+ continue;
92
+ }
93
+ if (arg.startsWith('--slides-dir=')) {
94
+ options.slidesDir = arg.slice('--slides-dir='.length);
95
+ continue;
96
+ }
97
+
98
+ if (arg === '--output-dir') {
99
+ options.outputDir = readOptionValue(args, i, '--output-dir');
100
+ i += 1;
101
+ continue;
102
+ }
103
+ if (arg.startsWith('--output-dir=')) {
104
+ options.outputDir = arg.slice('--output-dir='.length);
105
+ continue;
106
+ }
107
+
108
+ if (arg === '--slide-mode') {
109
+ options.slideMode = normalizeSlideMode(
110
+ readOptionValue(args, i, '--slide-mode'),
111
+ { optionName: '--slide-mode' },
112
+ );
113
+ i += 1;
114
+ continue;
115
+ }
116
+ if (arg.startsWith('--slide-mode=')) {
117
+ options.slideMode = normalizeSlideMode(
118
+ arg.slice('--slide-mode='.length),
119
+ { optionName: '--slide-mode' },
120
+ );
121
+ continue;
122
+ }
123
+
124
+ if (arg === '--resolution') {
125
+ options.resolution = normalizeResolutionPreset(
126
+ readOptionValue(args, i, '--resolution'),
127
+ { allowEmpty: false },
128
+ );
129
+ i += 1;
130
+ continue;
131
+ }
132
+ if (arg.startsWith('--resolution=')) {
133
+ options.resolution = normalizeResolutionPreset(
134
+ arg.slice('--resolution='.length),
135
+ { allowEmpty: false },
136
+ );
137
+ continue;
138
+ }
139
+
140
+ throw new Error(`Unknown option: ${arg}`);
141
+ }
142
+
143
+ return options;
144
+ }
145
+
146
+ function computeDeviceScaleFactor(resolution, slideMode) {
147
+ const { framePx } = getSlideModeConfig(slideMode);
148
+ const target = getResolutionSize(resolution, slideMode);
149
+ if (!target) return DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR;
150
+ return target.height / framePx.height;
151
+ }
152
+
153
+ async function discoverSlideFiles(slidesDir) {
154
+ const entries = await readdir(slidesDir);
155
+ return entries
156
+ .filter((name) => SLIDE_FILE_PATTERN.test(name))
157
+ .sort(sortSlideFiles);
158
+ }
159
+
160
+ async function renderSlideToPng(page, slidesDir, slideFile, outputPath) {
161
+ const url = pathToFileURL(join(slidesDir, slideFile)).href;
162
+ await page.goto(url, { waitUntil: 'load' });
163
+ await page.evaluate(async () => {
164
+ if (document.fonts?.ready) {
165
+ await document.fonts.ready;
166
+ }
167
+ });
168
+ await page.waitForTimeout(RENDER_SETTLE_MS);
169
+ await page.screenshot({ path: outputPath, fullPage: false });
170
+ }
171
+
172
+ async function main() {
173
+ let options;
174
+ try {
175
+ options = parseCliArgs(process.argv.slice(2));
176
+ } catch (error) {
177
+ process.stderr.write(`${error.message}\n`);
178
+ printUsage();
179
+ process.exit(2);
180
+ }
181
+
182
+ if (options.help) {
183
+ printUsage();
184
+ return;
185
+ }
186
+
187
+ const slidesDir = resolve(process.cwd(), options.slidesDir);
188
+ const outputDir = resolve(
189
+ process.cwd(),
190
+ options.outputDir || join(options.slidesDir, 'out-png'),
191
+ );
192
+
193
+ await ensureSlidesPassValidation(slidesDir, {
194
+ exportLabel: 'PNG export',
195
+ slideMode: options.slideMode,
196
+ });
197
+
198
+ const slideFiles = await discoverSlideFiles(slidesDir);
199
+ if (slideFiles.length === 0) {
200
+ process.stderr.write(`No slide-*.html files found in ${slidesDir}\n`);
201
+ process.exit(1);
202
+ }
203
+
204
+ await mkdir(outputDir, { recursive: true });
205
+
206
+ const { framePx } = getSlideModeConfig(options.slideMode);
207
+ const deviceScaleFactor = computeDeviceScaleFactor(
208
+ options.resolution,
209
+ options.slideMode,
210
+ );
211
+ const outputSize = getResolutionSize(options.resolution, options.slideMode) || {
212
+ width: framePx.width * deviceScaleFactor,
213
+ height: framePx.height * deviceScaleFactor,
214
+ };
215
+
216
+ process.stdout.write(
217
+ `Rendering ${slideFiles.length} slide(s) at ${outputSize.width}x${outputSize.height} (${options.slideMode})\n`,
218
+ );
219
+
220
+ const browser = await chromium.launch({ headless: true });
221
+ try {
222
+ const context = await browser.newContext({
223
+ viewport: { width: framePx.width, height: framePx.height },
224
+ deviceScaleFactor,
225
+ });
226
+ const page = await context.newPage();
227
+
228
+ for (const slideFile of slideFiles) {
229
+ const outputName = slideFile.replace(/\.html$/i, '.png');
230
+ const outputPath = join(outputDir, outputName);
231
+ await renderSlideToPng(page, slidesDir, slideFile, outputPath);
232
+ process.stdout.write(` ${outputName}\n`);
233
+ }
234
+
235
+ await context.close();
236
+ } finally {
237
+ await browser.close();
238
+ }
239
+
240
+ process.stdout.write(`PNG export complete: ${outputDir}\n`);
241
+ }
242
+
243
+ main().catch((error) => {
244
+ process.stderr.write(`${error?.stack || error?.message || error}\n`);
245
+ process.exit(1);
246
+ });
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readdirSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { resolve } from 'node:path';
6
+
7
+ import PptxGenJS from 'pptxgenjs';
8
+
9
+ import { ensureOutputDirectory, SLIDE_FILE_PATTERN, sortFigmaSlideFiles } from '../src/figma.js';
10
+
11
+ const require = createRequire(import.meta.url);
12
+ const html2pptx = require('../src/html2pptx.cjs');
13
+ const { launchBrowser } = html2pptx;
14
+ const { DEFAULT_SLIDE_MODE, getSlideModeChoices, getSlideModeConfig, normalizeSlideMode } = require('../src/slide-mode.cjs');
15
+
16
+ const DEFAULT_SLIDES_DIR = 'slides';
17
+ const DEFAULT_OUTPUT = 'output.pptx';
18
+
19
+ function printUsage() {
20
+ process.stdout.write(
21
+ [
22
+ 'Usage: node scripts/html2pptx.js [options]',
23
+ '',
24
+ 'Options:',
25
+ ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
26
+ ` --output <path> Output PPTX file (default: ${DEFAULT_OUTPUT})`,
27
+ ` --mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
28
+ ' -h, --help Show this help message',
29
+ '',
30
+ 'Experimental / unstable PPTX export. Treat output as best-effort only.',
31
+ ].join('\n'),
32
+ );
33
+ process.stdout.write('\n');
34
+ }
35
+
36
+ function readOptionValue(args, index, optionName) {
37
+ const next = args[index + 1];
38
+ if (!next || next.startsWith('-')) {
39
+ throw new Error(`Missing value for ${optionName}.`);
40
+ }
41
+ return next;
42
+ }
43
+
44
+ function parseArgs(args) {
45
+ const options = {
46
+ slidesDir: DEFAULT_SLIDES_DIR,
47
+ output: DEFAULT_OUTPUT,
48
+ mode: DEFAULT_SLIDE_MODE,
49
+ help: false,
50
+ };
51
+
52
+ for (let i = 0; i < args.length; i += 1) {
53
+ const arg = args[i];
54
+ if (arg === '-h' || arg === '--help') {
55
+ options.help = true;
56
+ continue;
57
+ }
58
+
59
+ if (arg === '--slides-dir') {
60
+ options.slidesDir = readOptionValue(args, i, '--slides-dir');
61
+ i += 1;
62
+ continue;
63
+ }
64
+
65
+ if (arg.startsWith('--slides-dir=')) {
66
+ options.slidesDir = arg.slice('--slides-dir='.length);
67
+ continue;
68
+ }
69
+
70
+ if (arg === '--output') {
71
+ options.output = readOptionValue(args, i, '--output');
72
+ i += 1;
73
+ continue;
74
+ }
75
+
76
+ if (arg.startsWith('--output=')) {
77
+ options.output = arg.slice('--output='.length);
78
+ continue;
79
+ }
80
+
81
+ if (arg === '--mode') {
82
+ options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
83
+ i += 1;
84
+ continue;
85
+ }
86
+
87
+ if (arg.startsWith('--mode=')) {
88
+ options.mode = normalizeSlideMode(arg.slice('--mode='.length));
89
+ continue;
90
+ }
91
+
92
+ throw new Error(`Unknown option: ${arg}`);
93
+ }
94
+
95
+ if (typeof options.slidesDir !== 'string' || options.slidesDir.trim() === '') {
96
+ throw new Error('--slides-dir must be a non-empty string.');
97
+ }
98
+
99
+ if (typeof options.output !== 'string' || options.output.trim() === '') {
100
+ throw new Error('--output must be a non-empty string.');
101
+ }
102
+
103
+ options.slidesDir = options.slidesDir.trim();
104
+ options.output = options.output.trim();
105
+ options.mode = normalizeSlideMode(options.mode);
106
+ return options;
107
+ }
108
+
109
+ function getHtmlSlides(slidesDir) {
110
+ if (!existsSync(slidesDir)) {
111
+ throw new Error(`Slides directory not found: ${slidesDir}`);
112
+ }
113
+
114
+ const files = readdirSync(slidesDir)
115
+ .filter((fileName) => SLIDE_FILE_PATTERN.test(fileName))
116
+ .sort(sortFigmaSlideFiles);
117
+
118
+ if (files.length === 0) {
119
+ throw new Error(`No slide-*.html files found in ${slidesDir}`);
120
+ }
121
+
122
+ return files;
123
+ }
124
+
125
+ async function main() {
126
+ const options = parseArgs(process.argv.slice(2));
127
+ if (options.help) {
128
+ printUsage();
129
+ return;
130
+ }
131
+
132
+ const slidesDir = resolve(process.cwd(), options.slidesDir);
133
+ const outputFile = resolve(process.cwd(), options.output);
134
+ const files = getHtmlSlides(slidesDir);
135
+
136
+ const pres = new PptxGenJS();
137
+ const { pptxSizeIn } = getSlideModeConfig(options.mode);
138
+ pres.defineLayout({
139
+ name: 'SLIDES_GRAB_HTML2PPTX',
140
+ width: pptxSizeIn.width,
141
+ height: pptxSizeIn.height,
142
+ });
143
+ pres.layout = 'SLIDES_GRAB_HTML2PPTX';
144
+
145
+ const browser = await launchBrowser(process.env.TMPDIR || '/tmp');
146
+ try {
147
+ for (const file of files) {
148
+ await html2pptx(resolve(slidesDir, file), pres, { browser, fitToLayout: true });
149
+ }
150
+ } finally {
151
+ await browser.close();
152
+ }
153
+
154
+ await ensureOutputDirectory(outputFile);
155
+ await pres.writeFile({ fileName: outputFile });
156
+ process.stdout.write(`Generated PPTX: ${outputFile}\n`);
157
+ }
158
+
159
+ main().catch((error) => {
160
+ console.error(`[slides-grab] ${error.message}`);
161
+ process.exit(1);
162
+ });
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve } from 'node:path';
4
+
5
+ import {
6
+ TemplateImportError,
7
+ importTemplatePack,
8
+ parseImportTemplateArgs,
9
+ } from '../src/template-import.js';
10
+
11
+ function printUsage() {
12
+ process.stdout.write(
13
+ [
14
+ 'Usage: node scripts/import-template.js [options]',
15
+ '',
16
+ 'Import one or more template/reference source files into a local template pack',
17
+ 'under <slides-dir>/.slides-grab/. HTML and image sources are analyzed locally',
18
+ '(no network, no LLM). PPTX/PDF are accepted as metadata/visual-reference',
19
+ 'dispatch only; local rendering is not available and a warning is recorded.',
20
+ '',
21
+ 'Options:',
22
+ ' --input <path> Template source file (repeatable; .pptx, .pdf, .html, .htm,',
23
+ ' or common image extensions).',
24
+ ' --slides-dir <path> Target slides directory (default: slides).',
25
+ ' --name <name> Optional template pack display name.',
26
+ ' -h, --help Show this help message.',
27
+ '',
28
+ 'Examples:',
29
+ ' node scripts/import-template.js --input brand.html --input logo.png --slides-dir slides --name "Brand Pack"',
30
+ ].join('\n'),
31
+ );
32
+ process.stdout.write('\n');
33
+ }
34
+
35
+ async function main() {
36
+ const argv = process.argv.slice(2);
37
+ if (argv.includes('-h') || argv.includes('--help')) {
38
+ printUsage();
39
+ return;
40
+ }
41
+
42
+ let parsed;
43
+ try {
44
+ parsed = parseImportTemplateArgs(argv);
45
+ } catch (error) {
46
+ if (error instanceof TemplateImportError) {
47
+ console.error(`[slides-grab] ${error.message}`);
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ throw error;
52
+ }
53
+
54
+ if (parsed.inputs.length === 0) {
55
+ console.error('[slides-grab] At least one --input source is required.');
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ if (!parsed.slidesDir) {
60
+ console.error('[slides-grab] A --slides-dir is required.');
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+
65
+ const result = await importTemplatePack({
66
+ inputs: parsed.inputs,
67
+ slidesDir: resolve(parsed.slidesDir),
68
+ name: parsed.name,
69
+ });
70
+
71
+ console.log(`Saved template pack: ${result.packPath}`);
72
+ console.log(`Wrote ${result.previewPaths.length} preview(s) under ${resolve(result.previewPaths[0] ?? '', '..').replace(/\/$/, '') || '<slides-dir>/.slides-grab/template-previews'}`);
73
+ for (const previewPath of result.previewPaths) {
74
+ console.log(` - ${previewPath}`);
75
+ }
76
+ }
77
+
78
+ main().catch((error) => {
79
+ if (error instanceof TemplateImportError) {
80
+ console.error(`[slides-grab] ${error.message}`);
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+ console.error(`[slides-grab] ${error.stack || error.message}`);
85
+ process.exitCode = 1;
86
+ });
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cp, mkdir, readdir, rm } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const packageRoot = resolve(__dirname, '..');
10
+ const VALID_TARGETS = new Set(['all', 'codex', 'claude-code']);
11
+ const VALID_SCOPES = new Set(['project', 'user']);
12
+ const SKILL_NAMES = [
13
+ 'slides-grab',
14
+ 'slides-grab-plan',
15
+ 'slides-grab-design',
16
+ 'slides-grab-export',
17
+ 'slides-grab-card-news',
18
+ ];
19
+
20
+ function printUsage() {
21
+ process.stdout.write(
22
+ [
23
+ 'Usage: slides-grab install-skills [options]',
24
+ '',
25
+ 'Options:',
26
+ ' --target <target> Runtime target: all|codex|claude-code (default: all)',
27
+ ' --runtime <target> Alias for --target',
28
+ ' --scope <scope> Install scope: project|user (default: project)',
29
+ ' --project-dir <path> Project directory for project scope (default: cwd)',
30
+ ' --target-root <path> Root directory for user-style installs',
31
+ ' --dry-run Print planned writes without copying',
32
+ ' --json Print JSON result',
33
+ ' -h, --help Show this help message',
34
+ ].join('\n'),
35
+ );
36
+ process.stdout.write('\n');
37
+ }
38
+
39
+ function parseArgs(args) {
40
+ const options = {
41
+ target: 'all',
42
+ scope: 'project',
43
+ projectDir: process.cwd(),
44
+ targetRoot: '',
45
+ dryRun: false,
46
+ json: false,
47
+ help: false,
48
+ };
49
+
50
+ for (let index = 0; index < args.length; index += 1) {
51
+ const arg = args[index];
52
+ if (arg === '-h' || arg === '--help') {
53
+ options.help = true;
54
+ continue;
55
+ }
56
+ if (arg === '--dry-run') {
57
+ options.dryRun = true;
58
+ continue;
59
+ }
60
+ if (arg === '--json') {
61
+ options.json = true;
62
+ continue;
63
+ }
64
+ if (OPTION_FIELDS.has(arg)) {
65
+ options[OPTION_FIELDS.get(arg)] = readOptionValue(args, index, arg);
66
+ index += 1;
67
+ continue;
68
+ }
69
+ const equalSignIndex = arg.indexOf('=');
70
+ if (equalSignIndex > -1) {
71
+ const name = arg.slice(0, equalSignIndex);
72
+ if (OPTION_FIELDS.has(name)) {
73
+ options[OPTION_FIELDS.get(name)] = arg.slice(equalSignIndex + 1);
74
+ continue;
75
+ }
76
+ }
77
+ throw new Error(`Unknown option: ${arg}`);
78
+ }
79
+
80
+ if (options.help) return options;
81
+
82
+ options.target = normalizeTarget(options.target);
83
+ options.scope = normalizeScope(options.scope);
84
+ options.projectDir = resolve(process.cwd(), options.projectDir);
85
+ options.targetRoot = options.targetRoot ? resolve(process.cwd(), options.targetRoot) : '';
86
+ return options;
87
+ }
88
+
89
+ const OPTION_FIELDS = new Map([
90
+ ['--target', 'target'],
91
+ ['--runtime', 'target'],
92
+ ['--scope', 'scope'],
93
+ ['--project-dir', 'projectDir'],
94
+ ['--target-root', 'targetRoot'],
95
+ ]);
96
+
97
+ function readOptionValue(args, index, optionName) {
98
+ const value = args[index + 1];
99
+ if (!value || value.startsWith('-')) {
100
+ throw new Error(`Missing value for ${optionName}.`);
101
+ }
102
+ return value;
103
+ }
104
+
105
+ function normalizeTarget(value) {
106
+ const target = String(value || '').trim().toLowerCase();
107
+ if (!VALID_TARGETS.has(target)) {
108
+ throw new Error(`Unknown runtime target "${value}". Expected: all, codex, or claude-code.`);
109
+ }
110
+ return target;
111
+ }
112
+
113
+ function normalizeScope(value) {
114
+ const scope = String(value || '').trim().toLowerCase();
115
+ if (!VALID_SCOPES.has(scope)) {
116
+ throw new Error(`Unknown install scope "${value}". Expected: project or user.`);
117
+ }
118
+ return scope;
119
+ }
120
+
121
+ function selectedTargets(target) {
122
+ return target === 'all' ? ['codex', 'claude-code'] : [target];
123
+ }
124
+
125
+ function runtimeRoots(options, target) {
126
+ if (options.scope === 'project') {
127
+ return {
128
+ codex: {
129
+ skillsDir: join(options.projectDir, '.agents', 'skills'),
130
+ agentsDir: join(options.projectDir, '.codex', 'agents'),
131
+ },
132
+ 'claude-code': {
133
+ skillsDir: join(options.projectDir, '.claude', 'skills'),
134
+ agentsDir: join(options.projectDir, '.claude', 'agents'),
135
+ },
136
+ }[target];
137
+ }
138
+
139
+ const root = options.targetRoot || homedir();
140
+ return {
141
+ codex: {
142
+ skillsDir: join(root, '.agents', 'skills'),
143
+ agentsDir: join(root, '.codex', 'agents'),
144
+ },
145
+ 'claude-code': {
146
+ skillsDir: join(root, '.claude', 'skills'),
147
+ agentsDir: join(root, '.claude', 'agents'),
148
+ },
149
+ }[target];
150
+ }
151
+
152
+ async function copyDirectory(source, destination, dryRun) {
153
+ if (dryRun) return;
154
+ await rm(destination, { recursive: true, force: true });
155
+ await mkdir(dirname(destination), { recursive: true });
156
+ await cp(source, destination, { recursive: true });
157
+ }
158
+
159
+ async function installTarget(options, target) {
160
+ const roots = runtimeRoots(options, target);
161
+ const writes = [];
162
+ if (!options.dryRun) {
163
+ await mkdir(roots.skillsDir, { recursive: true });
164
+ await mkdir(roots.agentsDir, { recursive: true });
165
+ }
166
+
167
+ for (const skillName of SKILL_NAMES) {
168
+ const source = join(packageRoot, 'skills', skillName);
169
+ const destination = join(roots.skillsDir, skillName);
170
+ await copyDirectory(source, destination, options.dryRun);
171
+ writes.push(destination);
172
+ }
173
+
174
+ const adapterSourceDir = join(packageRoot, 'runtimes', target, 'agents');
175
+ const adapterFiles = await readdir(adapterSourceDir);
176
+ for (const adapterFile of adapterFiles) {
177
+ const source = join(adapterSourceDir, adapterFile);
178
+ const destination = join(roots.agentsDir, adapterFile);
179
+ await copyDirectory(source, destination, options.dryRun);
180
+ writes.push(destination);
181
+ }
182
+
183
+ return { target, skillsDir: roots.skillsDir, agentsDir: roots.agentsDir, writes };
184
+ }
185
+
186
+ export async function main(argv = process.argv.slice(2)) {
187
+ const options = parseArgs(argv);
188
+ if (options.help) {
189
+ printUsage();
190
+ return;
191
+ }
192
+
193
+ const installs = [];
194
+ for (const target of selectedTargets(options.target)) {
195
+ installs.push(await installTarget(options, target));
196
+ }
197
+
198
+ if (options.json) {
199
+ process.stdout.write(`${JSON.stringify({ installs, dryRun: options.dryRun }, null, 2)}\n`);
200
+ return;
201
+ }
202
+
203
+ for (const install of installs) {
204
+ const label = install.target === 'claude-code' ? 'Claude Code' : 'Codex';
205
+ process.stdout.write(`Installed slides-grab skills for ${label}\n`);
206
+ process.stdout.write(` Skills: ${install.skillsDir}\n`);
207
+ process.stdout.write(` Agents: ${install.agentsDir}\n`);
208
+ }
209
+ }
210
+
211
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
212
+ main().catch((error) => {
213
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
214
+ process.exit(1);
215
+ });
216
+ }