@sanity-labs/slides 0.0.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 (224) hide show
  1. package/README.md +241 -0
  2. package/SKILL.md +119 -0
  3. package/dist/cli.d.ts +38 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +386 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/core/components.d.ts +179 -0
  8. package/dist/core/components.d.ts.map +1 -0
  9. package/dist/core/components.js +40 -0
  10. package/dist/core/components.js.map +1 -0
  11. package/dist/core/fake-runtime.d.ts +138 -0
  12. package/dist/core/fake-runtime.d.ts.map +1 -0
  13. package/dist/core/fake-runtime.js +210 -0
  14. package/dist/core/fake-runtime.js.map +1 -0
  15. package/dist/core/font-resolver.d.ts +28 -0
  16. package/dist/core/font-resolver.d.ts.map +1 -0
  17. package/dist/core/font-resolver.js +30 -0
  18. package/dist/core/font-resolver.js.map +1 -0
  19. package/dist/core/geometry.d.ts +71 -0
  20. package/dist/core/geometry.d.ts.map +1 -0
  21. package/dist/core/geometry.js +44 -0
  22. package/dist/core/geometry.js.map +1 -0
  23. package/dist/core/index.d.ts +19 -0
  24. package/dist/core/index.d.ts.map +1 -0
  25. package/dist/core/index.js +20 -0
  26. package/dist/core/index.js.map +1 -0
  27. package/dist/core/manifest.d.ts +123 -0
  28. package/dist/core/manifest.d.ts.map +1 -0
  29. package/dist/core/manifest.js +43 -0
  30. package/dist/core/manifest.js.map +1 -0
  31. package/dist/core/op-translator-pptx.d.ts +150 -0
  32. package/dist/core/op-translator-pptx.d.ts.map +1 -0
  33. package/dist/core/op-translator-pptx.js +245 -0
  34. package/dist/core/op-translator-pptx.js.map +1 -0
  35. package/dist/core/pptx-runtime.d.ts +103 -0
  36. package/dist/core/pptx-runtime.d.ts.map +1 -0
  37. package/dist/core/pptx-runtime.js +405 -0
  38. package/dist/core/pptx-runtime.js.map +1 -0
  39. package/dist/core/reconciler.d.ts +113 -0
  40. package/dist/core/reconciler.d.ts.map +1 -0
  41. package/dist/core/reconciler.js +453 -0
  42. package/dist/core/reconciler.js.map +1 -0
  43. package/dist/core/runtime.d.ts +161 -0
  44. package/dist/core/runtime.d.ts.map +1 -0
  45. package/dist/core/runtime.js +11 -0
  46. package/dist/core/runtime.js.map +1 -0
  47. package/dist/core/template.d.ts +32 -0
  48. package/dist/core/template.d.ts.map +1 -0
  49. package/dist/core/template.js +3 -0
  50. package/dist/core/template.js.map +1 -0
  51. package/dist/dev/auto-examples.d.ts +6 -0
  52. package/dist/dev/auto-examples.d.ts.map +1 -0
  53. package/dist/dev/auto-examples.js +79 -0
  54. package/dist/dev/auto-examples.js.map +1 -0
  55. package/dist/dev/bin/slides-dev.d.ts +3 -0
  56. package/dist/dev/bin/slides-dev.d.ts.map +1 -0
  57. package/dist/dev/bin/slides-dev.js +87 -0
  58. package/dist/dev/bin/slides-dev.js.map +1 -0
  59. package/dist/dev/bin/slides-dev.mjs +24 -0
  60. package/dist/dev/compose-deck.d.ts +18 -0
  61. package/dist/dev/compose-deck.d.ts.map +1 -0
  62. package/dist/dev/compose-deck.js +19 -0
  63. package/dist/dev/compose-deck.js.map +1 -0
  64. package/dist/dev/deck-viewer.d.ts +19 -0
  65. package/dist/dev/deck-viewer.d.ts.map +1 -0
  66. package/dist/dev/deck-viewer.js +237 -0
  67. package/dist/dev/deck-viewer.js.map +1 -0
  68. package/dist/dev/dev-server/client/entry.d.ts +2 -0
  69. package/dist/dev/dev-server/client/entry.d.ts.map +1 -0
  70. package/dist/dev/dev-server/client/entry.js +12 -0
  71. package/dist/dev/dev-server/client/entry.js.map +1 -0
  72. package/dist/dev/dev-server/output.d.ts +8 -0
  73. package/dist/dev/dev-server/output.d.ts.map +1 -0
  74. package/dist/dev/dev-server/output.js +32 -0
  75. package/dist/dev/dev-server/output.js.map +1 -0
  76. package/dist/dev/dev-server/server-only-stub.d.ts +7 -0
  77. package/dist/dev/dev-server/server-only-stub.d.ts.map +1 -0
  78. package/dist/dev/dev-server/server-only-stub.js +12 -0
  79. package/dist/dev/dev-server/server-only-stub.js.map +1 -0
  80. package/dist/dev/dev-server/start.d.ts +14 -0
  81. package/dist/dev/dev-server/start.d.ts.map +1 -0
  82. package/dist/dev/dev-server/start.js +135 -0
  83. package/dist/dev/dev-server/start.js.map +1 -0
  84. package/dist/dev/index.d.ts +5 -0
  85. package/dist/dev/index.d.ts.map +1 -0
  86. package/dist/dev/index.js +5 -0
  87. package/dist/dev/index.js.map +1 -0
  88. package/dist/dev/lib/cn.d.ts +3 -0
  89. package/dist/dev/lib/cn.d.ts.map +1 -0
  90. package/dist/dev/lib/cn.js +3 -0
  91. package/dist/dev/lib/cn.js.map +1 -0
  92. package/dist/dev/slide-canvas.d.ts +12 -0
  93. package/dist/dev/slide-canvas.d.ts.map +1 -0
  94. package/dist/dev/slide-canvas.js +123 -0
  95. package/dist/dev/slide-canvas.js.map +1 -0
  96. package/dist/dev/styles.css +37 -0
  97. package/dist/dev/ui/icon-button.d.ts +12 -0
  98. package/dist/dev/ui/icon-button.d.ts.map +1 -0
  99. package/dist/dev/ui/icon-button.js +6 -0
  100. package/dist/dev/ui/icon-button.js.map +1 -0
  101. package/dist/dev/ui/kbd.d.ts +6 -0
  102. package/dist/dev/ui/kbd.d.ts.map +1 -0
  103. package/dist/dev/ui/kbd.js +4 -0
  104. package/dist/dev/ui/kbd.js.map +1 -0
  105. package/dist/dev/ui/text-button.d.ts +10 -0
  106. package/dist/dev/ui/text-button.d.ts.map +1 -0
  107. package/dist/dev/ui/text-button.js +6 -0
  108. package/dist/dev/ui/text-button.js.map +1 -0
  109. package/dist/dev/url-state.d.ts +7 -0
  110. package/dist/dev/url-state.d.ts.map +1 -0
  111. package/dist/dev/url-state.js +13 -0
  112. package/dist/dev/url-state.js.map +1 -0
  113. package/dist/dev/use-keyboard-nav.d.ts +17 -0
  114. package/dist/dev/use-keyboard-nav.d.ts.map +1 -0
  115. package/dist/dev/use-keyboard-nav.js +53 -0
  116. package/dist/dev/use-keyboard-nav.js.map +1 -0
  117. package/dist/index.d.ts +17 -0
  118. package/dist/index.d.ts.map +1 -0
  119. package/dist/index.js +17 -0
  120. package/dist/index.js.map +1 -0
  121. package/dist/mcp/errors.d.ts +57 -0
  122. package/dist/mcp/errors.d.ts.map +1 -0
  123. package/dist/mcp/errors.js +44 -0
  124. package/dist/mcp/errors.js.map +1 -0
  125. package/dist/mcp/index.d.ts +29 -0
  126. package/dist/mcp/index.d.ts.map +1 -0
  127. package/dist/mcp/index.js +29 -0
  128. package/dist/mcp/index.js.map +1 -0
  129. package/dist/mcp/naming.d.ts +37 -0
  130. package/dist/mcp/naming.d.ts.map +1 -0
  131. package/dist/mcp/naming.js +43 -0
  132. package/dist/mcp/naming.js.map +1 -0
  133. package/dist/mcp/render.d.ts +45 -0
  134. package/dist/mcp/render.d.ts.map +1 -0
  135. package/dist/mcp/render.js +77 -0
  136. package/dist/mcp/render.js.map +1 -0
  137. package/dist/mcp/schema.d.ts +54 -0
  138. package/dist/mcp/schema.d.ts.map +1 -0
  139. package/dist/mcp/schema.js +55 -0
  140. package/dist/mcp/schema.js.map +1 -0
  141. package/dist/mcp/server.d.ts +63 -0
  142. package/dist/mcp/server.d.ts.map +1 -0
  143. package/dist/mcp/server.js +196 -0
  144. package/dist/mcp/server.js.map +1 -0
  145. package/dist/scaffold/index.d.ts +39 -0
  146. package/dist/scaffold/index.d.ts.map +1 -0
  147. package/dist/scaffold/index.js +84 -0
  148. package/dist/scaffold/index.js.map +1 -0
  149. package/dist/scaffold/template-base/README.md +134 -0
  150. package/dist/scaffold/template-base/_gitignore +4 -0
  151. package/dist/scaffold/template-base/package.json +35 -0
  152. package/dist/scaffold/template-base/src/components/Cover.tsx +30 -0
  153. package/dist/scaffold/template-base/src/index.ts +27 -0
  154. package/dist/scaffold/template-base/src/preview.tsx +9 -0
  155. package/dist/scaffold/template-base/tsconfig.build.json +10 -0
  156. package/dist/scaffold/template-base/tsconfig.json +18 -0
  157. package/package.json +164 -0
  158. package/src/__tests__/fixtures/test-template/index.tsx +77 -0
  159. package/src/__tests__/pptx-mcp.test.ts +85 -0
  160. package/src/__tests__/pptx-smoke.test.ts +45 -0
  161. package/src/__tests__/preview.test.ts +28 -0
  162. package/src/cli.ts +426 -0
  163. package/src/core/__snapshots__/reconciler.test.ts.snap +320 -0
  164. package/src/core/components.test.ts +57 -0
  165. package/src/core/components.ts +196 -0
  166. package/src/core/fake-runtime.test.ts +174 -0
  167. package/src/core/fake-runtime.ts +302 -0
  168. package/src/core/font-resolver.ts +46 -0
  169. package/src/core/geometry.test.ts +58 -0
  170. package/src/core/geometry.ts +91 -0
  171. package/src/core/index.ts +69 -0
  172. package/src/core/manifest.test.ts +33 -0
  173. package/src/core/manifest.ts +150 -0
  174. package/src/core/op-translator-pptx.test.ts +204 -0
  175. package/src/core/op-translator-pptx.ts +365 -0
  176. package/src/core/pptx-runtime.test.ts +137 -0
  177. package/src/core/pptx-runtime.ts +504 -0
  178. package/src/core/reconciler.test.ts +644 -0
  179. package/src/core/reconciler.ts +603 -0
  180. package/src/core/runtime.ts +150 -0
  181. package/src/core/template.test.ts +136 -0
  182. package/src/core/template.ts +37 -0
  183. package/src/dev/auto-examples.ts +89 -0
  184. package/src/dev/bin/slides-dev.mjs +24 -0
  185. package/src/dev/bin/slides-dev.ts +101 -0
  186. package/src/dev/compose-deck.test.ts +68 -0
  187. package/src/dev/compose-deck.ts +40 -0
  188. package/src/dev/deck-viewer.tsx +677 -0
  189. package/src/dev/dev-server/client/entry.tsx +15 -0
  190. package/src/dev/dev-server/client/index.html +24 -0
  191. package/src/dev/dev-server/output.ts +37 -0
  192. package/src/dev/dev-server/server-only-stub.ts +12 -0
  193. package/src/dev/dev-server/start.ts +155 -0
  194. package/src/dev/index.ts +4 -0
  195. package/src/dev/lib/cn.ts +3 -0
  196. package/src/dev/slide-canvas.test.tsx +66 -0
  197. package/src/dev/slide-canvas.tsx +170 -0
  198. package/src/dev/styles.css +37 -0
  199. package/src/dev/ui/icon-button.tsx +31 -0
  200. package/src/dev/ui/kbd.tsx +20 -0
  201. package/src/dev/ui/text-button.tsx +31 -0
  202. package/src/dev/url-state.test.ts +22 -0
  203. package/src/dev/url-state.ts +17 -0
  204. package/src/dev/use-keyboard-nav.ts +64 -0
  205. package/src/index.ts +17 -0
  206. package/src/mcp/errors.test.ts +51 -0
  207. package/src/mcp/errors.ts +76 -0
  208. package/src/mcp/index.ts +45 -0
  209. package/src/mcp/naming.test.ts +39 -0
  210. package/src/mcp/naming.ts +49 -0
  211. package/src/mcp/render.ts +110 -0
  212. package/src/mcp/schema.test.ts +86 -0
  213. package/src/mcp/schema.ts +93 -0
  214. package/src/mcp/server.test.ts +309 -0
  215. package/src/mcp/server.ts +276 -0
  216. package/src/scaffold/index.ts +102 -0
  217. package/src/scaffold/template-base/README.md +134 -0
  218. package/src/scaffold/template-base/_gitignore +4 -0
  219. package/src/scaffold/template-base/package.json +35 -0
  220. package/src/scaffold/template-base/src/components/Cover.tsx +30 -0
  221. package/src/scaffold/template-base/src/index.ts +27 -0
  222. package/src/scaffold/template-base/src/preview.tsx +9 -0
  223. package/src/scaffold/template-base/tsconfig.build.json +10 -0
  224. package/src/scaffold/template-base/tsconfig.json +18 -0
package/src/cli.ts ADDED
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `slidesctl` — the CLI bin shipped with `@sanity-labs/slides`.
4
+ *
5
+ * Subcommands:
6
+ *
7
+ * serve --template <path-or-specifier> [--output <dir>] [--name <id>]
8
+ * Start an MCP server over stdio. Exposes `slides_list`,
9
+ * `slides_add_<type>`, and `slides_create` derived from the
10
+ * loaded template.
11
+ *
12
+ * generate --template <path-or-specifier> [--output <dir>]
13
+ * Read `{ title, slides: [...] }` JSON from stdin and write
14
+ * the resulting `.pptx`. Prints the absolute path on stdout.
15
+ *
16
+ * list --template <path-or-specifier>
17
+ * Print every slide type the template exposes, with
18
+ * descriptions. Useful for humans inspecting a template
19
+ * without an MCP client.
20
+ *
21
+ * scaffold <dir> [--name <slug>]
22
+ * Scaffold a new template into <dir>. Replaces the old
23
+ * `npm create react-pptx-template` flow.
24
+ *
25
+ * skill [--path]
26
+ * Print the bundled `SKILL.md` to stdout (or its absolute
27
+ * path with `--path`). Paste into a Claude project to teach
28
+ * the model how to drive any react-pptx MCP server.
29
+ *
30
+ * The `--template` flag accepts:
31
+ * - A bare package specifier resolved from the current directory
32
+ * (e.g. `@acme/slide-template`).
33
+ * - A path to a built JS file (`./dist/index.js`).
34
+ * - A path to a directory containing `package.json` (the bin reads
35
+ * `main` / `exports['.']`).
36
+ */
37
+
38
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
39
+ import { dirname, resolve as resolvePath } from 'node:path';
40
+ import { fileURLToPath, pathToFileURL } from 'node:url';
41
+ import type { SlidesRuntime, Template } from './core/index.js';
42
+ import { PptxSlidesRuntime } from './core/index.js';
43
+ import { renderSlides } from './mcp/render.js';
44
+ import { createSlideServer } from './mcp/server.js';
45
+ import { defaultName, scaffoldTemplate, validateName } from './scaffold/index.js';
46
+
47
+ const USAGE = `Usage: slidesctl <command> [options]
48
+
49
+ Commands:
50
+ serve Start an MCP server over stdio.
51
+ generate Read { title, slides } JSON from stdin, write a .pptx file.
52
+ list Print the slide types a template exposes.
53
+ scaffold Scaffold a new template into <dir>.
54
+ skill Print the bundled SKILL.md.
55
+
56
+ Options (serve / generate / list):
57
+ --template, -t <path> Template to load. Accepts a package name, a file
58
+ path, or a directory.
59
+ --output, -o <dir> Output directory for generated .pptx files
60
+ (serve/generate only; defaults to cwd).
61
+ --name, <id> MCP server name override (serve only).
62
+
63
+ Options (scaffold):
64
+ --name <slug> Template name (default: inferred from <dir>).
65
+
66
+ Options (skill):
67
+ --path Print SKILL.md's absolute path only.
68
+
69
+ Global:
70
+ --help, -h Show this message.
71
+ `;
72
+
73
+ type ParsedArgs = {
74
+ readonly command: string | undefined;
75
+ readonly positional: ReadonlyArray<string>;
76
+ readonly template: string | undefined;
77
+ readonly output: string | undefined;
78
+ readonly name: string | undefined;
79
+ readonly path: boolean;
80
+ readonly help: boolean;
81
+ };
82
+
83
+ const parseArgs = (argv: readonly string[]): ParsedArgs => {
84
+ const args: {
85
+ command: string | undefined;
86
+ positional: string[];
87
+ template: string | undefined;
88
+ output: string | undefined;
89
+ name: string | undefined;
90
+ path: boolean;
91
+ help: boolean;
92
+ } = {
93
+ command: undefined,
94
+ positional: [],
95
+ template: undefined,
96
+ output: undefined,
97
+ name: undefined,
98
+ path: false,
99
+ help: false,
100
+ };
101
+
102
+ for (let i = 0; i < argv.length; i++) {
103
+ const arg = argv[i];
104
+ switch (arg) {
105
+ case '--help':
106
+ case '-h':
107
+ args.help = true;
108
+ break;
109
+ case '--template':
110
+ case '-t':
111
+ args.template = argv[++i];
112
+ break;
113
+ case '--output':
114
+ case '-o':
115
+ args.output = argv[++i];
116
+ break;
117
+ case '--name':
118
+ args.name = argv[++i];
119
+ break;
120
+ case '--path':
121
+ args.path = true;
122
+ break;
123
+ default:
124
+ if (arg === undefined) break;
125
+ if (arg.startsWith('-')) throw new CliError(`Unknown flag: ${arg}`);
126
+ if (args.command === undefined) {
127
+ args.command = arg;
128
+ } else {
129
+ args.positional.push(arg);
130
+ }
131
+ }
132
+ }
133
+
134
+ return args;
135
+ };
136
+
137
+ class CliError extends Error {
138
+ constructor(
139
+ message: string,
140
+ readonly exitCode = 2,
141
+ ) {
142
+ super(message);
143
+ this.name = 'CliError';
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Resolve a `--template` argument to a Template instance.
149
+ *
150
+ * The template is the default export of the resolved module, or any named
151
+ * export that matches the Template shape (`name`, `components`, `canvas`).
152
+ */
153
+ const loadTemplate = async (spec: string): Promise<Template> => {
154
+ const cwd = process.cwd();
155
+ let importTarget: string;
156
+
157
+ if (isAbsoluteOrRelative(spec)) {
158
+ const abs = resolvePath(cwd, spec);
159
+ let resolved: string;
160
+ try {
161
+ const stat = statSync(abs);
162
+ resolved = stat.isDirectory() ? resolveDirEntry(abs) : abs;
163
+ } catch (err) {
164
+ throw new CliError(
165
+ `Cannot read --template path "${spec}" (resolved to "${abs}"): ${describeError(err)}`,
166
+ );
167
+ }
168
+ importTarget = pathToFileURL(resolved).href;
169
+ } else {
170
+ importTarget = resolveBareSpecifier(spec, cwd);
171
+ }
172
+
173
+ let mod: unknown;
174
+ try {
175
+ mod = await import(importTarget);
176
+ } catch (err) {
177
+ throw new CliError(`Failed to import template "${spec}": ${describeError(err)}`);
178
+ }
179
+ const template = pickTemplate(mod);
180
+ if (!template) {
181
+ throw new CliError(
182
+ `Module at "${spec}" does not export a Template (need an object with name + components).`,
183
+ );
184
+ }
185
+ return template;
186
+ };
187
+
188
+ const isAbsoluteOrRelative = (spec: string): boolean =>
189
+ spec.startsWith('.') || spec.startsWith('/') || /^[A-Z]:\\/i.test(spec);
190
+
191
+ const resolveDirEntry = (dir: string): string => {
192
+ const pkgPath = resolvePath(dir, 'package.json');
193
+ let pkg: { main?: string; module?: string; exports?: unknown };
194
+ try {
195
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as typeof pkg;
196
+ } catch (err) {
197
+ throw new CliError(
198
+ `Template directory "${dir}" has no readable package.json: ${describeError(err)}`,
199
+ );
200
+ }
201
+ const entry = pkg.module ?? pkg.main ?? exportsDefault(pkg.exports);
202
+ if (!entry) {
203
+ throw new CliError(`Template directory "${dir}" has no main/module/exports entry to import.`);
204
+ }
205
+ return resolvePath(dir, entry);
206
+ };
207
+
208
+ const exportsDefault = (exportsField: unknown): string | undefined => {
209
+ if (typeof exportsField === 'string') return exportsField;
210
+ if (exportsField && typeof exportsField === 'object') {
211
+ const dot = (exportsField as Record<string, unknown>)['.'];
212
+ if (typeof dot === 'string') return dot;
213
+ if (dot && typeof dot === 'object') {
214
+ const sub = dot as Record<string, unknown>;
215
+ const candidate = sub['import'] ?? sub['default'] ?? sub['node'];
216
+ if (typeof candidate === 'string') return candidate;
217
+ }
218
+ }
219
+ return undefined;
220
+ };
221
+
222
+ const resolveBareSpecifier = (spec: string, cwd: string): string => {
223
+ // ESM resolution honours `exports.import` and other conditions that
224
+ // require.resolve() doesn't, which matters for templates that ship
225
+ // conditional exports. Anchor resolution at the user's cwd via a synthetic
226
+ // package.json URL so we look in their node_modules.
227
+ const parentUrl = pathToFileURL(resolvePath(cwd, 'package.json')).href;
228
+ try {
229
+ return import.meta.resolve(spec, parentUrl);
230
+ } catch (err) {
231
+ throw new CliError(
232
+ `Cannot resolve template "${spec}" from ${cwd}: ${describeError(err)}. ` +
233
+ `Install it as a dependency or pass a path with --template.`,
234
+ );
235
+ }
236
+ };
237
+
238
+ const pickTemplate = (mod: unknown): Template | undefined => {
239
+ if (!mod || typeof mod !== 'object') return undefined;
240
+ const candidates: unknown[] = [];
241
+ const m = mod as Record<string, unknown>;
242
+ if (m['default']) candidates.push(m['default']);
243
+ for (const [key, value] of Object.entries(m)) {
244
+ if (key === 'default') continue;
245
+ candidates.push(value);
246
+ }
247
+ for (const c of candidates) {
248
+ if (isTemplate(c)) return c;
249
+ }
250
+ return undefined;
251
+ };
252
+
253
+ const isTemplate = (value: unknown): value is Template => {
254
+ if (!value || typeof value !== 'object') return false;
255
+ const v = value as Record<string, unknown>;
256
+ return typeof v['name'] === 'string' && typeof v['components'] === 'object';
257
+ };
258
+
259
+ const newRuntime = (output: string | undefined): SlidesRuntime =>
260
+ new PptxSlidesRuntime({ outputDir: output ?? process.cwd() });
261
+
262
+ const runServe = async (args: ParsedArgs): Promise<void> => {
263
+ if (!args.template) throw new CliError('serve requires --template <path-or-specifier>.');
264
+ const template = await loadTemplate(args.template);
265
+ const runtime = newRuntime(args.output);
266
+ const server = createSlideServer({
267
+ template,
268
+ runtime,
269
+ ...(args.name ? { serverInfo: { name: args.name, version: '0.1.0' } } : {}),
270
+ });
271
+ await server.start({ transport: 'stdio' });
272
+ };
273
+
274
+ const runList = async (args: ParsedArgs): Promise<void> => {
275
+ if (!args.template) throw new CliError('list requires --template <path-or-specifier>.');
276
+ const template = await loadTemplate(args.template);
277
+ const lines = [`Template: ${template.name}`, '', 'Slide types:'];
278
+ for (const [name, tc] of Object.entries(template.components)) {
279
+ lines.push(` • ${name} — ${tc.description ?? '(no description)'}`);
280
+ }
281
+ process.stdout.write(lines.join('\n') + '\n');
282
+ };
283
+
284
+ const runGenerate = async (args: ParsedArgs): Promise<void> => {
285
+ if (!args.template) throw new CliError('generate requires --template <path-or-specifier>.');
286
+ const template = await loadTemplate(args.template);
287
+ const runtime = newRuntime(args.output);
288
+
289
+ const raw = await readStdin();
290
+ if (raw.trim().length === 0) {
291
+ throw new CliError(
292
+ 'generate: no input on stdin. Pipe JSON of { title, slides: [{ component, props }] }.',
293
+ );
294
+ }
295
+ let input: { title?: unknown; slides?: unknown };
296
+ try {
297
+ input = JSON.parse(raw) as typeof input;
298
+ } catch (err) {
299
+ throw new CliError(`generate: invalid JSON on stdin: ${describeError(err)}`);
300
+ }
301
+ if (typeof input.title !== 'string' || !Array.isArray(input.slides)) {
302
+ throw new CliError('generate: input must be { title: string, slides: array }.');
303
+ }
304
+ const result = await renderSlides({
305
+ template,
306
+ runtime,
307
+ title: input.title,
308
+ slides: input.slides as Array<{ component: string; props: Record<string, unknown> }>,
309
+ });
310
+ if (result.ok) {
311
+ process.stdout.write(`${result.filePath}\n`);
312
+ return;
313
+ }
314
+ throw new CliError(result.message, result.code === 'unknown_component' ? 3 : 2);
315
+ };
316
+
317
+ const runSkill = (args: ParsedArgs): void => {
318
+ const skillPath = resolveSkillPath();
319
+ if (args.path) {
320
+ process.stdout.write(`${skillPath}\n`);
321
+ return;
322
+ }
323
+ process.stdout.write(readFileSync(skillPath, 'utf8'));
324
+ };
325
+
326
+ const runScaffold = (args: ParsedArgs): void => {
327
+ const [target] = args.positional;
328
+ if (!target) {
329
+ throw new CliError('scaffold requires a target directory: `slidesctl scaffold my-template`.');
330
+ }
331
+ const name = args.name ?? defaultName(target);
332
+ const nameError = validateName(name);
333
+ if (nameError) {
334
+ throw new CliError(
335
+ `Invalid template name "${name}": ${nameError}. ` + `Pass --name <slug> with a valid name.`,
336
+ );
337
+ }
338
+ const result = scaffoldTemplate({ target, name });
339
+ process.stdout.write(`Scaffolded ${result.fileCount} files into ${result.targetPath}\n`);
340
+ process.stdout.write(
341
+ `\nNext steps:\n cd ${target}\n pnpm install\n pnpm dev # open the viewer\n pnpm build # emit dist/ so slidesctl can serve it\n`,
342
+ );
343
+ };
344
+
345
+ const resolveSkillPath = (): string => {
346
+ // SKILL.md ships at the package root, one level above dist/cli.js.
347
+ // From src/cli.ts the same relative path resolves correctly because
348
+ // we run from dist/ in production.
349
+ const here = dirname(fileURLToPath(import.meta.url));
350
+ return resolvePath(here, '..', 'SKILL.md');
351
+ };
352
+
353
+ const readStdin = (): Promise<string> => {
354
+ const chunks: Buffer[] = [];
355
+ return new Promise((resolve, reject) => {
356
+ process.stdin.on('data', (chunk: Buffer | string) => {
357
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk);
358
+ });
359
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
360
+ process.stdin.on('error', (err: Error) => reject(err));
361
+ });
362
+ };
363
+
364
+ const describeError = (err: unknown): string => (err instanceof Error ? err.message : String(err));
365
+
366
+ export const main = async (argv: readonly string[]): Promise<void> => {
367
+ let args: ParsedArgs;
368
+ try {
369
+ args = parseArgs(argv);
370
+ } catch (err) {
371
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
372
+ process.stderr.write(USAGE);
373
+ process.exit(2);
374
+ }
375
+
376
+ if (args.help || args.command === undefined) {
377
+ process.stdout.write(USAGE);
378
+ process.exit(args.help ? 0 : 1);
379
+ }
380
+
381
+ try {
382
+ switch (args.command) {
383
+ case 'serve':
384
+ await runServe(args);
385
+ return;
386
+ case 'list':
387
+ await runList(args);
388
+ return;
389
+ case 'generate':
390
+ await runGenerate(args);
391
+ return;
392
+ case 'scaffold':
393
+ runScaffold(args);
394
+ return;
395
+ case 'skill':
396
+ runSkill(args);
397
+ return;
398
+ default:
399
+ process.stderr.write(`Unknown command: ${args.command}\n`);
400
+ process.stderr.write(USAGE);
401
+ process.exit(2);
402
+ }
403
+ } catch (err) {
404
+ if (err instanceof CliError) {
405
+ process.stderr.write(`${err.message}\n`);
406
+ process.exit(err.exitCode);
407
+ }
408
+ process.stderr.write(`slidesctl failed: ${describeError(err)}\n`);
409
+ process.exit(1);
410
+ }
411
+ };
412
+
413
+ // pnpm's content-addressed install layout means `process.argv[1]` is the
414
+ // `.bin` symlink while `import.meta.url` is the realpath under `.pnpm/`.
415
+ // Use realpathSync on both sides so direct invocation and
416
+ // `node_modules/.bin/slidesctl` both detect this file as the entrypoint.
417
+ const argvPath = process.argv[1];
418
+ if (argvPath !== undefined) {
419
+ try {
420
+ const argvReal = realpathSync(argvPath);
421
+ const selfReal = fileURLToPath(import.meta.url);
422
+ if (argvReal === selfReal) void main(process.argv.slice(2));
423
+ } catch {
424
+ // not invoked as a script — module being imported, ignore.
425
+ }
426
+ }