promptslide 0.2.1 → 0.2.2

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.
@@ -0,0 +1,384 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
2
+ import { join, basename, relative, extname } from "node:path"
3
+
4
+ import { bold, green, cyan, red, dim } from "../utils/ansi.mjs"
5
+ import { requireAuth } from "../utils/auth.mjs"
6
+ import { captureSlideAsDataUri, isPlaywrightAvailable } from "../utils/export.mjs"
7
+ import { publishToRegistry, registryItemExists, updateLockfileItem, hashContent, detectPackageManager } from "../utils/registry.mjs"
8
+ import { prompt, confirm, closePrompts } from "../utils/prompts.mjs"
9
+
10
+ function titleCase(slug) {
11
+ return slug
12
+ .replace(/\.tsx?$/, "")
13
+ .replace(/[-_]/g, " ")
14
+ .replace(/\b\w/g, c => c.toUpperCase())
15
+ }
16
+
17
+ function detectType(filePath) {
18
+ if (filePath.includes("/slides/") || filePath.includes("\\slides\\")) return "slide"
19
+ if (filePath.includes("/layouts/") || filePath.includes("\\layouts\\")) return "layout"
20
+ return null
21
+ }
22
+
23
+ function detectSteps(content) {
24
+ const matches = content.matchAll(/step=\{(\d+)\}/g)
25
+ let max = 0
26
+ for (const m of matches) {
27
+ const n = parseInt(m[1], 10)
28
+ if (n > max) max = n
29
+ }
30
+ return max
31
+ }
32
+
33
+ function detectNpmDeps(content) {
34
+ const deps = {}
35
+ // Match both unscoped (foo) and scoped (@scope/foo) packages, exclude relative imports
36
+ const importRegex = /import\s+.*?\s+from\s+["']((?:@[a-zA-Z0-9-]+\/)?[a-zA-Z0-9-][^"']*)["']/g
37
+ for (const match of content.matchAll(importRegex)) {
38
+ // Get the package name (handle deep imports like "framer-motion/client")
39
+ const full = match[1]
40
+ const pkg = full.startsWith("@") ? full.split("/").slice(0, 2).join("/") : full.split("/")[0]
41
+ // Skip relative imports, react, and internal packages
42
+ if (pkg.startsWith(".") || pkg === "react" || pkg === "react-dom" || pkg.startsWith("@promptslide/")) continue
43
+ deps[pkg] = "latest"
44
+ }
45
+ return deps
46
+ }
47
+
48
+ function detectRegistryDeps(content) {
49
+ const deps = []
50
+ const importRegex = /import\s+.*?\s+from\s+["']@\/layouts\/([^"']+)["']/g
51
+ for (const match of content.matchAll(importRegex)) {
52
+ const name = match[1].replace(/\.tsx?$/, "")
53
+ deps.push(name)
54
+ }
55
+ return deps
56
+ }
57
+
58
+ const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp"])
59
+ const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB
60
+
61
+ function readPreviewImage(imagePath) {
62
+ if (!existsSync(imagePath)) return null
63
+ const ext = extname(imagePath).toLowerCase()
64
+ if (!IMAGE_EXTS.has(ext)) return null
65
+ const stat = statSync(imagePath)
66
+ if (stat.size > MAX_IMAGE_SIZE) return null
67
+ const buf = readFileSync(imagePath)
68
+ const mime = ext === ".png" ? "image/png" : ext === ".webp" ? "image/webp" : "image/jpeg"
69
+ return `data:${mime};base64,${buf.toString("base64")}`
70
+ }
71
+
72
+ function scanForFiles(cwd) {
73
+ const files = []
74
+ const dirs = [
75
+ { dir: join(cwd, "src", "slides"), target: "src/slides/" },
76
+ { dir: join(cwd, "src", "layouts"), target: "src/layouts/" }
77
+ ]
78
+ for (const { dir, target } of dirs) {
79
+ if (!existsSync(dir)) continue
80
+ for (const entry of readdirSync(dir)) {
81
+ if (entry.endsWith(".tsx") || entry.endsWith(".ts")) {
82
+ files.push({ path: entry, target, fullPath: join(dir, entry) })
83
+ }
84
+ }
85
+ }
86
+ return files
87
+ }
88
+
89
+ /**
90
+ * Publish a single item to the registry.
91
+ * @param {{ filePath: string, cwd: string, auth: object, typeOverride?: string, interactive?: boolean }} opts
92
+ * @returns {Promise<{ slug: string, status: string }>}
93
+ */
94
+ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = true }) {
95
+ const fullPath = join(cwd, filePath)
96
+ if (!existsSync(fullPath)) {
97
+ throw new Error(`File not found: ${filePath}`)
98
+ }
99
+
100
+ const content = readFileSync(fullPath, "utf-8")
101
+ const fileName = basename(fullPath)
102
+ const slug = fileName.replace(/\.tsx?$/, "")
103
+
104
+ const type = typeOverride || detectType(filePath) || "slide"
105
+ const steps = detectSteps(content)
106
+ const npmDeps = detectNpmDeps(content)
107
+ const registryDeps = detectRegistryDeps(content)
108
+ const target = type === "layout" ? "src/layouts/" : "src/slides/"
109
+
110
+ let title, description, tags, section, releaseNotes, previewImage
111
+
112
+ if (interactive) {
113
+ title = await prompt("Title:", titleCase(slug))
114
+ description = await prompt("Description:", "")
115
+ const tagsInput = await prompt("Tags (comma-separated):", "")
116
+ tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
117
+ section = await prompt("Section:", "")
118
+ releaseNotes = await prompt("Release notes:", "")
119
+ const imagePath = await prompt("Preview image path (leave empty to auto-generate):", "")
120
+ if (imagePath) {
121
+ const resolved = join(cwd, imagePath)
122
+ previewImage = readPreviewImage(resolved)
123
+ if (!previewImage) {
124
+ console.log(` ${dim("⚠ Skipping image: file not found, unsupported format, or > 2MB")}`)
125
+ }
126
+ } else if (await isPlaywrightAvailable()) {
127
+ console.log(` ${dim("Generating preview image...")}`)
128
+ previewImage = await captureSlideAsDataUri({ cwd, slidePath: filePath })
129
+ if (previewImage) {
130
+ console.log(` ${green("✓")} Preview image generated`)
131
+ } else {
132
+ console.log(` ${dim("⚠ Could not generate preview image")}`)
133
+ }
134
+ }
135
+ } else {
136
+ title = titleCase(slug)
137
+ description = ""
138
+ tags = []
139
+ section = ""
140
+ releaseNotes = ""
141
+ previewImage = await captureSlideAsDataUri({ cwd, slidePath: filePath }).catch(() => null)
142
+ }
143
+
144
+ const payload = {
145
+ type,
146
+ slug,
147
+ title,
148
+ description: description || undefined,
149
+ tags,
150
+ steps,
151
+ section: section || undefined,
152
+ files: [{ path: fileName, target, content }],
153
+ npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
154
+ registryDependencies: registryDeps.length ? registryDeps : undefined,
155
+ releaseNotes: releaseNotes || undefined,
156
+ previewImage: previewImage || undefined
157
+ }
158
+
159
+ const result = await publishToRegistry(payload, auth)
160
+
161
+ // Track in lockfile
162
+ const fileHashes = { [target + fileName]: hashContent(content) }
163
+ updateLockfileItem(cwd, slug, result.version ?? 0, fileHashes)
164
+
165
+ return { slug, status: result.status || "published", version: result.version }
166
+ }
167
+
168
+ export async function publish(args) {
169
+ const cwd = process.cwd()
170
+
171
+ console.log()
172
+ console.log(` ${bold("promptslide")} ${dim("publish")}`)
173
+ console.log()
174
+
175
+ if (args[0] === "--help" || args[0] === "-h") {
176
+ console.log(` ${bold("Usage:")} promptslide publish ${dim("[file] [--type slide|layout|deck|theme]")}`)
177
+ console.log()
178
+ console.log(` Publish a slide or layout to the registry.`)
179
+ console.log()
180
+ console.log(` ${bold("Examples:")}`)
181
+ console.log(` promptslide publish src/slides/slide-hero.tsx`)
182
+ console.log(` promptslide publish --type layout`)
183
+ console.log()
184
+ process.exit(0)
185
+ }
186
+
187
+ const auth = requireAuth()
188
+
189
+ // Determine file to publish
190
+ let typeOverride = null
191
+ const typeIdx = args.indexOf("--type")
192
+ if (typeIdx !== -1 && args[typeIdx + 1]) {
193
+ typeOverride = args[typeIdx + 1]
194
+ }
195
+
196
+ const flagIndices = new Set()
197
+ if (typeIdx !== -1) {
198
+ flagIndices.add(typeIdx)
199
+ flagIndices.add(typeIdx + 1)
200
+ }
201
+ let filePath = args.find((a, i) => !a.startsWith("--") && !flagIndices.has(i))
202
+
203
+ if (!filePath) {
204
+ // Interactive mode: scan for files
205
+ const available = scanForFiles(cwd)
206
+ if (available.length === 0) {
207
+ console.error(` ${red("Error:")} No .tsx files found in src/slides/ or src/layouts/.`)
208
+ process.exit(1)
209
+ }
210
+
211
+ console.log(` ${bold("Available files:")}`)
212
+ available.forEach((f, i) => {
213
+ console.log(` ${dim(`${i + 1}.`)} ${f.target}${f.path}`)
214
+ })
215
+ console.log()
216
+
217
+ const choice = await prompt("Select file number:", "1")
218
+ const idx = parseInt(choice, 10) - 1
219
+ if (idx < 0 || idx >= available.length) {
220
+ console.error(` ${red("Error:")} Invalid selection.`)
221
+ process.exit(1)
222
+ }
223
+
224
+ filePath = relative(cwd, available[idx].fullPath)
225
+ }
226
+
227
+ // Resolve full path and read content
228
+ const fullPath = join(cwd, filePath)
229
+ if (!existsSync(fullPath)) {
230
+ console.error(` ${red("Error:")} File not found: ${filePath}`)
231
+ process.exit(1)
232
+ }
233
+
234
+ const content = readFileSync(fullPath, "utf-8")
235
+ const fileName = basename(fullPath)
236
+ const slug = fileName.replace(/\.tsx?$/, "")
237
+
238
+ // Detect metadata
239
+ const type = typeOverride || detectType(filePath) || "slide"
240
+ const steps = detectSteps(content)
241
+ const npmDeps = detectNpmDeps(content)
242
+ const registryDeps = detectRegistryDeps(content)
243
+ const target = type === "layout" ? "src/layouts/" : "src/slides/"
244
+
245
+ console.log(` File: ${cyan(filePath)}`)
246
+ console.log(` Type: ${type}`)
247
+ console.log(` Steps: ${steps}`)
248
+ if (Object.keys(npmDeps).length) {
249
+ console.log(` Dependencies: ${dim(Object.keys(npmDeps).join(", "))}`)
250
+ }
251
+ if (registryDeps.length) {
252
+ console.log(` Registry deps: ${dim(registryDeps.join(", "))}`)
253
+ }
254
+ console.log()
255
+
256
+ // Check if registry dependencies exist and offer to publish missing ones
257
+ if (registryDeps.length) {
258
+ const missing = []
259
+
260
+ for (const depSlug of registryDeps) {
261
+ const exists = await registryItemExists(depSlug, auth)
262
+ if (!exists) {
263
+ missing.push(depSlug)
264
+ }
265
+ }
266
+
267
+ if (missing.length) {
268
+ console.log(` ${bold("Missing dependencies:")} ${missing.length} registry dep(s) not yet published`)
269
+ for (const depSlug of missing) {
270
+ // Try to find the local file
271
+ const candidates = [
272
+ join(cwd, "src", "layouts", `${depSlug}.tsx`),
273
+ join(cwd, "src", "layouts", `${depSlug}.ts`),
274
+ join(cwd, "src", "slides", `${depSlug}.tsx`),
275
+ join(cwd, "src", "slides", `${depSlug}.ts`),
276
+ ]
277
+ const localFile = candidates.find(f => existsSync(f))
278
+
279
+ if (localFile) {
280
+ const relPath = relative(cwd, localFile)
281
+ const depType = detectType(relPath) || "layout"
282
+ const shouldPublish = await confirm(
283
+ `Publish ${bold(depSlug)} (${cyan(relPath)}) as ${depType}?`
284
+ )
285
+
286
+ if (shouldPublish) {
287
+ console.log()
288
+ console.log(` ${dim("─── Publishing dependency:")} ${bold(depSlug)} ${dim("───")}`)
289
+ console.log()
290
+
291
+ try {
292
+ const result = await publishItem({
293
+ filePath: relPath,
294
+ cwd,
295
+ auth,
296
+ typeOverride: depType,
297
+ interactive: true
298
+ })
299
+ console.log()
300
+ const depVer = result.version ? ` ${dim(`v${result.version}`)}` : ""
301
+ console.log(` ${green("✓")} Published dependency ${bold(result.slug)}${depVer}`)
302
+ console.log()
303
+ } catch (err) {
304
+ console.error(` ${red("Error publishing dependency:")} ${err.message}`)
305
+ console.log(` ${dim("Continuing with main item...")}`)
306
+ console.log()
307
+ }
308
+ }
309
+ } else {
310
+ console.log(` ${dim("⚠")} ${depSlug}: local file not found, skipping`)
311
+ }
312
+ }
313
+
314
+ console.log(` ${dim("─── Main item:")} ${bold(slug)} ${dim("───")}`)
315
+ console.log()
316
+ }
317
+ }
318
+
319
+ // Collect metadata for main item
320
+ const title = await prompt("Title:", titleCase(slug))
321
+ const description = await prompt("Description:", "")
322
+ const tagsInput = await prompt("Tags (comma-separated):", "")
323
+ const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
324
+ const section = await prompt("Section:", "")
325
+ const releaseNotes = await prompt("Release notes:", "")
326
+ const previewImagePath = await prompt("Preview image path (leave empty to auto-generate):", "")
327
+ let previewImage = null
328
+ if (previewImagePath) {
329
+ const resolved = join(cwd, previewImagePath)
330
+ previewImage = readPreviewImage(resolved)
331
+ if (!previewImage) {
332
+ console.log(` ${dim("⚠ Skipping image: file not found, unsupported format, or > 2MB")}`)
333
+ }
334
+ } else if (await isPlaywrightAvailable()) {
335
+ console.log(` ${dim("Generating preview image...")}`)
336
+ previewImage = await captureSlideAsDataUri({ cwd, slidePath: filePath })
337
+ if (previewImage) {
338
+ console.log(` ${green("✓")} Preview image generated`)
339
+ } else {
340
+ console.log(` ${dim("⚠ Could not generate preview image")}`)
341
+ }
342
+ } else {
343
+ const pm = detectPackageManager(cwd)
344
+ const installCmd = pm === "bun" ? "bun add -d playwright" : pm === "pnpm" ? "pnpm add -D playwright" : pm === "yarn" ? "yarn add -D playwright" : "npm install -D playwright"
345
+ console.log(` ${dim("Tip: Install playwright for auto-generated preview images")}`)
346
+ console.log(` ${dim(` ${installCmd} && npx playwright install chromium`)}`)
347
+ }
348
+
349
+ console.log()
350
+
351
+ // Publish main item
352
+ const payload = {
353
+ type,
354
+ slug,
355
+ title,
356
+ description: description || undefined,
357
+ tags,
358
+ steps,
359
+ section: section || undefined,
360
+ files: [{ path: fileName, target, content }],
361
+ npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
362
+ registryDependencies: registryDeps.length ? registryDeps : undefined,
363
+ releaseNotes: releaseNotes || undefined,
364
+ previewImage: previewImage || undefined
365
+ }
366
+
367
+ try {
368
+ const result = await publishToRegistry(payload, auth)
369
+ const verTag = result.version ? ` v${result.version}` : ""
370
+ console.log(` ${green("✓")} Published ${bold(slug)}${verTag} to ${auth.organizationName || "registry"}`)
371
+ console.log(` Status: ${result.status || "published"}`)
372
+ console.log(` Install: ${cyan(`promptslide add ${slug}`)}`)
373
+
374
+ // Track in lockfile
375
+ const fileHashes = { [target + fileName]: hashContent(content) }
376
+ updateLockfileItem(cwd, slug, result.version ?? 0, fileHashes)
377
+ } catch (err) {
378
+ console.error(` ${red("Error:")} ${err.message}`)
379
+ process.exit(1)
380
+ }
381
+
382
+ console.log()
383
+ closePrompts()
384
+ }
@@ -0,0 +1,111 @@
1
+ import { existsSync, unlinkSync } from "node:fs"
2
+ import { join } from "node:path"
3
+
4
+ import { bold, green, cyan, red, yellow, dim } from "../utils/ansi.mjs"
5
+ import { readLockfile, removeLockfileItem, isFileDirty } from "../utils/registry.mjs"
6
+ import { toPascalCase, removeSlideFromDeckConfig } from "../utils/deck-config.mjs"
7
+ import { confirm, closePrompts } from "../utils/prompts.mjs"
8
+
9
+ export async function remove(args) {
10
+ const cwd = process.cwd()
11
+
12
+ console.log()
13
+ console.log(` ${bold("promptslide")} ${dim("remove")}`)
14
+ console.log()
15
+
16
+ const name = args[0]
17
+ if (!name || name === "--help" || name === "-h") {
18
+ console.log(` ${bold("Usage:")} promptslide remove ${dim("<name>")}`)
19
+ console.log()
20
+ console.log(` Remove an installed slide, layout, or deck.`)
21
+ console.log()
22
+ console.log(` ${bold("Examples:")}`)
23
+ console.log(` promptslide remove slide-hero-gradient`)
24
+ console.log(` promptslide remove deck-pitch`)
25
+ console.log()
26
+ process.exit(0)
27
+ }
28
+
29
+ // Check lockfile
30
+ const lock = readLockfile(cwd)
31
+ if (!lock.items[name]) {
32
+ console.error(` ${red("Error:")} "${name}" is not installed (not found in lockfile).`)
33
+ console.log()
34
+ process.exit(1)
35
+ }
36
+
37
+ const lockEntry = lock.items[name]
38
+
39
+ // Use lockfile files map as source of truth
40
+ const filesToRemove = []
41
+ let hasDirtyFiles = false
42
+
43
+ for (const [relativePath, storedHash] of Object.entries(lockEntry.files)) {
44
+ const targetPath = join(cwd, relativePath)
45
+ if (existsSync(targetPath)) {
46
+ const dirty = isFileDirty(cwd, relativePath, storedHash)
47
+ if (dirty) hasDirtyFiles = true
48
+ filesToRemove.push({ path: targetPath, display: relativePath, dirty })
49
+ }
50
+ }
51
+
52
+ // Show what will be removed
53
+ if (filesToRemove.length > 0) {
54
+ console.log(` Files to remove:`)
55
+ for (const f of filesToRemove) {
56
+ const tag = f.dirty ? ` ${yellow("(modified)")}` : ""
57
+ console.log(` ${dim("•")} ${f.display}${tag}`)
58
+ }
59
+ console.log()
60
+ } else {
61
+ console.log(` ${dim("No local files found for this item.")}`)
62
+ console.log()
63
+ }
64
+
65
+ const confirmMsg = hasDirtyFiles
66
+ ? `Remove ${bold(name)}? ${yellow("Some files have local changes that will be lost.")}`
67
+ : `Remove ${bold(name)}?`
68
+ const ok = await confirm(confirmMsg, !hasDirtyFiles)
69
+ if (!ok) {
70
+ console.log(` ${dim("Cancelled.")}`)
71
+ console.log()
72
+ closePrompts()
73
+ return
74
+ }
75
+
76
+ // Delete files
77
+ let removed = 0
78
+ for (const f of filesToRemove) {
79
+ try {
80
+ unlinkSync(f.path)
81
+ console.log(` ${green("✓")} Removed ${cyan(f.display)}`)
82
+ removed++
83
+ } catch (err) {
84
+ console.log(` ${red("⚠")} Could not remove ${f.display}: ${err.message}`)
85
+ }
86
+ }
87
+
88
+ // Remove from deck-config if any files are slides
89
+ for (const relativePath of Object.keys(lockEntry.files)) {
90
+ if (relativePath.includes("slides/")) {
91
+ const fileName = relativePath.split("/").pop().replace(/\.tsx?$/, "")
92
+ const componentName = toPascalCase(fileName)
93
+ const updated = removeSlideFromDeckConfig(cwd, componentName)
94
+ if (updated) {
95
+ console.log(` ${green("✓")} Removed ${componentName} from ${cyan("deck-config.ts")}`)
96
+ }
97
+ }
98
+ }
99
+
100
+ // Remove from lockfile
101
+ removeLockfileItem(cwd, name)
102
+ console.log(` ${green("✓")} Removed from lockfile`)
103
+
104
+ if (removed > 0) {
105
+ console.log()
106
+ console.log(` ${dim(`Removed ${removed} file${removed === 1 ? "" : "s"}.`)}`)
107
+ }
108
+
109
+ console.log()
110
+ closePrompts()
111
+ }
@@ -0,0 +1,57 @@
1
+ import { bold, cyan, red, dim } from "../utils/ansi.mjs"
2
+ import { requireAuth } from "../utils/auth.mjs"
3
+ import { searchRegistry } from "../utils/registry.mjs"
4
+
5
+ function padEnd(str, len) {
6
+ str = String(str)
7
+ return str.length >= len ? str : str + " ".repeat(len - str.length)
8
+ }
9
+
10
+ function printTable(items) {
11
+ if (!items.length) {
12
+ console.log(` ${dim("No items found.")}`)
13
+ return
14
+ }
15
+
16
+ const cols = { name: 24, type: 8, steps: 6, author: 20, downloads: 10 }
17
+
18
+ console.log(
19
+ ` ${dim(padEnd("Name", cols.name))}${dim(padEnd("Type", cols.type))}${dim(padEnd("Steps", cols.steps))}${dim(padEnd("Author", cols.author))}${dim("Downloads")}`
20
+ )
21
+
22
+ for (const item of items) {
23
+ const steps = item.type === "deck" ? "—" : String(item.steps ?? 0)
24
+ console.log(
25
+ ` ${padEnd(item.name, cols.name)}${padEnd(item.type, cols.type)}${padEnd(steps, cols.steps)}${padEnd(item.author || "—", cols.author)}${item.downloads ?? 0}`
26
+ )
27
+ }
28
+ }
29
+
30
+ export async function search(args) {
31
+ console.log()
32
+ console.log(` ${bold("promptslide")} ${dim("search")}`)
33
+ console.log()
34
+
35
+ const query = args.filter(a => !a.startsWith("--")).join(" ")
36
+ if (!query) {
37
+ console.log(` ${bold("Usage:")} promptslide search ${dim("<query>")}`)
38
+ console.log()
39
+ process.exit(0)
40
+ }
41
+
42
+ const auth = requireAuth()
43
+
44
+ try {
45
+ const results = await searchRegistry({ search: query }, auth)
46
+ console.log(` Results for "${cyan(query)}":`)
47
+ console.log()
48
+ printTable(Array.isArray(results) ? results : results.items || [])
49
+ } catch (err) {
50
+ console.error(` ${red("Error:")} ${err.message}`)
51
+ process.exit(1)
52
+ }
53
+
54
+ console.log()
55
+ }
56
+
57
+ export { printTable }
@@ -0,0 +1,52 @@
1
+ import { writeFileSync } from "node:fs"
2
+ import { resolve, basename } from "node:path"
3
+
4
+ import { bold, green, red, dim } from "../utils/ansi.mjs"
5
+ import { captureSlideScreenshot, isPlaywrightAvailable } from "../utils/export.mjs"
6
+
7
+ export async function toImage(args) {
8
+ const slidePath = args[0]
9
+ const outputIndex = args.indexOf("-o")
10
+ const output = outputIndex !== -1 ? args[outputIndex + 1] : null
11
+
12
+ if (!slidePath || slidePath === "--help" || slidePath === "-h") {
13
+ console.log()
14
+ console.log(` ${bold("Usage:")} promptslide to-image <slide> [options]`)
15
+ console.log()
16
+ console.log(` ${bold("Options:")}`)
17
+ console.log(` -o <file> Output file path (default: <slide-name>.png)`)
18
+ console.log()
19
+ console.log(` ${bold("Examples:")}`)
20
+ console.log(` ${dim("promptslide to-image src/slides/slide-title.tsx")}`)
21
+ console.log(` ${dim("promptslide to-image src/slides/slide-title.tsx -o preview.png")}`)
22
+ console.log()
23
+ return
24
+ }
25
+
26
+ if (!(await isPlaywrightAvailable())) {
27
+ console.error()
28
+ console.error(` ${red("Error:")} Playwright is required for image export.`)
29
+ console.error(` Install it with: ${bold("npm install -D playwright && npx playwright install chromium")}`)
30
+ console.error()
31
+ process.exit(1)
32
+ }
33
+
34
+ const outputPath = resolve(output || `${basename(slidePath).replace(/\.tsx?$/, "")}.png`)
35
+
36
+ console.log()
37
+ console.log(` ${dim("Capturing screenshot...")}`)
38
+
39
+ const buffer = await captureSlideScreenshot({
40
+ cwd: process.cwd(),
41
+ slidePath
42
+ })
43
+
44
+ if (!buffer) {
45
+ console.error(` ${red("Error:")} Failed to capture screenshot.`)
46
+ process.exit(1)
47
+ }
48
+
49
+ writeFileSync(outputPath, buffer)
50
+ console.log(` ${green("✓")} Saved to ${bold(outputPath)}`)
51
+ console.log()
52
+ }