promptslide 0.3.4 → 0.3.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.5](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.4...promptslide-v0.3.5) (2026-03-08)
4
+
5
+
6
+ ### Features
7
+
8
+ * bundle shared sources with deck publish and fix deck-config parsing ([5f7fd1d](https://github.com/prompticeu/promptslide/commit/5f7fd1de6e5fc58a0568bd87830094dd40c9eca7))
9
+ * persist deck and item metadata to lockfile for publish defaults ([#68](https://github.com/prompticeu/promptslide/issues/68)) ([0dc8ca3](https://github.com/prompticeu/promptslide/commit/0dc8ca3f52b86214eb42dacd9e1ead4d05928242))
10
+
3
11
  ## [0.3.4](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.3...promptslide-v0.3.4) (2026-03-07)
4
12
 
5
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptslide",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "CLI and slide engine for PromptSlide presentations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url"
5
5
  import { bold, green, cyan, red, dim } from "../utils/ansi.mjs"
6
6
  import { requireAuth } from "../utils/auth.mjs"
7
7
  import { captureSlideAsDataUri, isPlaywrightAvailable, createCaptureSession } from "../utils/export.mjs"
8
- import { publishToRegistry, registryItemExists, searchRegistry, updateLockfileItem, updateLockfilePublishConfig, readLockfile, writeLockfile, hashContent, detectPackageManager, requestUploadTokens, uploadBinaryToBlob, assetFileToSlug, detectAssetDepsInContent } from "../utils/registry.mjs"
8
+ import { publishToRegistry, registryItemExists, searchRegistry, updateLockfileItem, updateLockfilePublishConfig, readLockfile, writeLockfile, hashContent, detectPackageManager, requestUploadTokens, uploadBinaryToBlob, assetFileToSlug, detectAssetDepsInContent, readDeckMeta, updateDeckMeta, readItemMeta, updateItemMeta } from "../utils/registry.mjs"
9
9
  import { prompt, confirm, select, closePrompts } from "../utils/prompts.mjs"
10
10
  import { parseDeckConfig } from "../utils/deck-config.mjs"
11
11
 
@@ -91,6 +91,57 @@ function detectRegistryDeps(content) {
91
91
  return deps
92
92
  }
93
93
 
94
+ /**
95
+ * Discover shared source files under src/ that aren't slides, layouts, or theme.
96
+ * These are files in directories like src/components/, src/lib/, src/hooks/, etc.
97
+ * Returns an array of { relativePath, fullPath, target } objects.
98
+ */
99
+ function discoverSharedSources(cwd) {
100
+ const srcDir = join(cwd, "src")
101
+ if (!existsSync(srcDir)) return []
102
+
103
+ // Top-level dirs whose top-level files are published as individual items
104
+ // (slides/*.tsx → slide items, layouts/*.tsx → layout items).
105
+ // Subdirectories within these ARE included as shared sources (e.g. slides/commercial/slide1.tsx).
106
+ const ITEM_DIRS = new Set(["slides", "layouts"])
107
+ // Top-level files already handled (theme.ts, globals.css, deck-config.ts, App.tsx)
108
+ const SKIP_FILES = new Set(["theme.ts", "globals.css", "deck-config.ts", "App.tsx", "vite-env.d.ts"])
109
+ const SOURCE_EXTS = new Set([".tsx", ".ts", ".jsx", ".js"])
110
+
111
+ const results = []
112
+
113
+ function walk(dir, relativeDir, skipTopLevelFiles) {
114
+ if (!existsSync(dir)) return
115
+ for (const entry of readdirSync(dir)) {
116
+ if (entry.startsWith(".")) continue
117
+ const full = join(dir, entry)
118
+ const s = statSync(full)
119
+ if (s.isDirectory()) {
120
+ // Enter slides/ and layouts/ but mark that top-level files are handled elsewhere
121
+ if (!relativeDir && ITEM_DIRS.has(entry)) {
122
+ walk(full, entry, true)
123
+ } else {
124
+ walk(full, relativeDir ? `${relativeDir}/${entry}` : entry, false)
125
+ }
126
+ } else if (s.isFile()) {
127
+ // Skip known top-level files in src/
128
+ if (!relativeDir && SKIP_FILES.has(entry)) continue
129
+ // Skip top-level files in slides/ and layouts/ (published as individual items)
130
+ if (skipTopLevelFiles) continue
131
+ const ext = extname(entry).toLowerCase()
132
+ if (!SOURCE_EXTS.has(ext)) continue
133
+ const relativePath = relativeDir ? `${relativeDir}/${entry}` : entry
134
+ // target mirrors the src/ directory structure for @/ import resolution
135
+ const target = relativeDir ? `src/${relativeDir}/` : "src/"
136
+ results.push({ relativePath, fullPath: full, fileName: entry, target })
137
+ }
138
+ }
139
+ }
140
+
141
+ walk(srcDir, "", false)
142
+ return results
143
+ }
144
+
94
145
  const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp"])
95
146
  const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB
96
147
 
@@ -254,11 +305,13 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
254
305
  let title, description, tags, section, releaseNotes, previewImage
255
306
 
256
307
  if (interactive) {
257
- title = await prompt("Title:", titleCase(baseSlug))
258
- description = await prompt("Description:", "")
259
- const tagsInput = await prompt("Tags (comma-separated):", "")
308
+ const storedMeta = readItemMeta(cwd, slug)
309
+ title = await prompt("Title:", storedMeta.title || titleCase(baseSlug))
310
+ description = await prompt("Description:", storedMeta.description || "")
311
+ const storedTagsDefault = storedMeta.tags?.length ? storedMeta.tags.join(", ") : ""
312
+ const tagsInput = await prompt("Tags (comma-separated):", storedTagsDefault)
260
313
  tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
261
- section = await prompt("Section:", "")
314
+ section = await prompt("Section:", storedMeta.section || "")
262
315
  releaseNotes = await prompt("Release notes:", "")
263
316
  const imagePath = await prompt("Preview image path (leave empty to auto-generate):", "")
264
317
  if (imagePath) {
@@ -307,6 +360,13 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
307
360
  // Track in lockfile
308
361
  const fileHashes = { [target + fileName]: hashContent(content) }
309
362
  updateLockfileItem(cwd, slug, result.version ?? 0, fileHashes)
363
+ // Persist item metadata for next publish
364
+ updateItemMeta(cwd, slug, {
365
+ title,
366
+ description: description || undefined,
367
+ tags,
368
+ section: section || undefined
369
+ })
310
370
 
311
371
  return { slug, status: result.status || "published", version: result.version }
312
372
  }
@@ -423,6 +483,9 @@ export async function publish(args) {
423
483
  ? readdirSync(slidesDir).filter(f => f.endsWith(".tsx") || f.endsWith(".ts"))
424
484
  : []
425
485
 
486
+ // Discover shared source files (src/components/, src/lib/, etc.)
487
+ const sharedSources = discoverSharedSources(cwd)
488
+
426
489
  const totalItems = publicAssets.length + (hasTheme ? 1 : 0) + layoutEntries.length + slideEntries.length + 1
427
490
 
428
491
  // Display summary
@@ -431,16 +494,26 @@ export async function publish(args) {
431
494
  if (hasTheme) console.log(` Theme: 1`)
432
495
  if (layoutEntries.length) console.log(` Layouts: ${layoutEntries.length}`)
433
496
  console.log(` Slides: ${slideEntries.length}`)
497
+ if (sharedSources.length) console.log(` Shared: ${sharedSources.length} ${dim("(bundled with deck)")}`)
434
498
  console.log(` Total: ${totalItems} items`)
499
+ // Info if there are slide files on disk not referenced in deck-config.ts
500
+ const deckConfigSlideCount = deckConfig.slides.length
501
+ if (deckConfigSlideCount !== slideEntries.length) {
502
+ console.log()
503
+ console.log(` ${dim("ℹ")} deck-config.ts has ${deckConfigSlideCount} slides, ${slideEntries.length} slide files on disk`)
504
+ console.log(` ${dim("All slides are published. Only deck-config slides appear in the deck preview.")}`)
505
+ }
435
506
  if (deckConfig.transition) {
436
507
  console.log(` Transition: ${deckConfig.transition}${deckConfig.directionalTransition ? " (directional)" : ""}`)
437
508
  }
438
509
  console.log()
439
510
 
440
- // Collect deck metadata
441
- const title = await prompt("Title:", titleCase(deckSlug))
442
- const description = await prompt("Description:", "")
443
- const tagsInput = await prompt("Tags (comma-separated):", "")
511
+ // Collect deck metadata (use stored values as defaults)
512
+ const storedDeckMeta = readDeckMeta(cwd)
513
+ const title = await prompt("Title:", storedDeckMeta.title || titleCase(deckSlug))
514
+ const description = await prompt("Description:", storedDeckMeta.description || "")
515
+ const storedTagsDefault = storedDeckMeta.tags?.length ? storedDeckMeta.tags.join(", ") : ""
516
+ const tagsInput = await prompt("Tags (comma-separated):", storedTagsDefault)
444
517
  const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
445
518
  const releaseNotes = await prompt("Release notes:", "")
446
519
 
@@ -692,12 +765,28 @@ export async function publish(args) {
692
765
 
693
766
  if (captureSession) await captureSession.close()
694
767
 
695
- // ── Phase 5: Deck manifest ──
768
+ // ── Phase 5: Deck manifest (includes shared source modules) ──
696
769
  itemIndex++
697
770
  const slideSlugs = slideEntries.map(f => `${deckSlug}/${f.replace(/\.tsx?$/, "")}`)
698
771
  const assetSlugs = publicAssets.map(a => assetFileToSlug(deckSlug, a.relativePath))
699
772
  const allDeckDeps = [...slideSlugs, ...assetSlugs]
700
773
 
774
+ // Bundle shared source files with the deck item so preview/install can resolve @/ imports
775
+ const sharedFiles = sharedSources.map(s => ({
776
+ path: s.fileName,
777
+ target: s.target,
778
+ content: readFileSync(s.fullPath, "utf-8")
779
+ }))
780
+ if (sharedFiles.length) {
781
+ console.log(` ${dim(`Bundling ${sharedFiles.length} shared source file(s) with deck`)}`)
782
+ }
783
+
784
+ // Collect npm deps from shared source files
785
+ const sharedNpmDeps = {}
786
+ for (const f of sharedFiles) {
787
+ Object.assign(sharedNpmDeps, detectNpmDeps(f.content))
788
+ }
789
+
701
790
  let deckItemId = null
702
791
  try {
703
792
  const result = await publishToRegistry({
@@ -707,7 +796,8 @@ export async function publish(args) {
707
796
  description: description || undefined,
708
797
  tags,
709
798
  deckConfig,
710
- files: [],
799
+ files: sharedFiles,
800
+ npmDependencies: Object.keys(sharedNpmDeps).length ? sharedNpmDeps : undefined,
711
801
  registryDependencies: allDeckDeps.length ? allDeckDeps : undefined,
712
802
  releaseNotes: releaseNotes || undefined,
713
803
  previewImage: previewImage || undefined,
@@ -717,6 +807,8 @@ export async function publish(args) {
717
807
  lockfileUpdates.push({ slug: deckSlug, version: result.version ?? 0, fileHashes: {} })
718
808
  deckItemId = result.id
719
809
  published++
810
+ // Persist deck metadata for next publish
811
+ updateDeckMeta(cwd, { title, description: description || undefined, tags })
720
812
  } catch (err) {
721
813
  console.log(` [${itemIndex}/${totalItems}] ${red("✗")} deck ${dim(deckSlug)}: ${err.message}`)
722
814
  failed++
@@ -725,7 +817,9 @@ export async function publish(args) {
725
817
  // Batch-write all lockfile updates at once
726
818
  const finalLock = readLockfile(cwd)
727
819
  for (const update of lockfileUpdates) {
820
+ const existing = finalLock.items[update.slug] || {}
728
821
  finalLock.items[update.slug] = {
822
+ ...existing,
729
823
  version: update.version,
730
824
  installedAt: new Date().toISOString().split("T")[0],
731
825
  files: update.fileHashes
@@ -864,12 +958,14 @@ export async function publish(args) {
864
958
  }
865
959
  }
866
960
 
867
- // Collect metadata for main item
868
- const title = await prompt("Title:", titleCase(baseSlug))
869
- const description = await prompt("Description:", "")
870
- const tagsInput = await prompt("Tags (comma-separated):", "")
961
+ // Collect metadata for main item (use stored values as defaults)
962
+ const storedMeta = readItemMeta(cwd, slug)
963
+ const title = await prompt("Title:", storedMeta.title || titleCase(baseSlug))
964
+ const description = await prompt("Description:", storedMeta.description || "")
965
+ const storedTagsDefault = storedMeta.tags?.length ? storedMeta.tags.join(", ") : ""
966
+ const tagsInput = await prompt("Tags (comma-separated):", storedTagsDefault)
871
967
  const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
872
- const section = await prompt("Section:", "")
968
+ const section = await prompt("Section:", storedMeta.section || "")
873
969
  const releaseNotes = await prompt("Release notes:", "")
874
970
  const previewImagePath = await prompt("Preview image path (leave empty to auto-generate):", "")
875
971
  let previewImage = null
@@ -927,6 +1023,13 @@ export async function publish(args) {
927
1023
  // Track in lockfile
928
1024
  const fileHashes = { [target + fileName]: hashContent(content) }
929
1025
  updateLockfileItem(cwd, slug, result.version ?? 0, fileHashes)
1026
+ // Persist item metadata for next publish
1027
+ updateItemMeta(cwd, slug, {
1028
+ title,
1029
+ description: description || undefined,
1030
+ tags,
1031
+ section: section || undefined
1032
+ })
930
1033
  } catch (err) {
931
1034
  console.error(` ${red("Error:")} ${err.message}`)
932
1035
  process.exit(1)
@@ -6,13 +6,16 @@ const DECK_CONFIG_PATH = "src/deck-config.ts"
6
6
  /**
7
7
  * Convert a kebab-case filename to PascalCase component name.
8
8
  * e.g. "slide-hero-gradient" → "SlideHeroGradient"
9
+ * e.g. "01-title" → "Slide01Title" (prefixed to ensure valid JS identifier)
9
10
  */
10
11
  export function toPascalCase(kebab) {
11
- return kebab
12
+ const raw = kebab
12
13
  .replace(/\.tsx?$/, "")
13
14
  .split("-")
14
15
  .map(part => part.charAt(0).toUpperCase() + part.slice(1))
15
16
  .join("")
17
+ // JS identifiers cannot start with a digit — prefix with "Slide"
18
+ return /^\d/.test(raw) ? `Slide${raw}` : raw
16
19
  }
17
20
 
18
21
  /**
@@ -222,11 +225,20 @@ export function parseDeckConfig(cwd) {
222
225
  const content = readFileSync(configPath, "utf-8")
223
226
 
224
227
  // 1. Parse imports to build componentName -> slug map
225
- // Pattern: import { SlideTitle } from "@/slides/slide-title"
228
+ // Handles: import { SlideTitle } from "@/slides/slide-title"
229
+ // Handles: import { default as SlideTitle } from "@/slides/slide-title"
230
+ // Handles: import { SlideTitle as Alias } from "@/slides/slide-title"
226
231
  const componentToSlug = {}
227
- const importRegex = /import\s*\{\s*(\w+)\s*\}\s*from\s*["']@\/slides\/([^"']+)["']/g
232
+ const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']@\/slides\/([^"']+)["']/g
228
233
  for (const match of content.matchAll(importRegex)) {
229
- componentToSlug[match[1]] = match[2].replace(/\.tsx?$/, "")
234
+ const importClause = match[1].trim()
235
+ const slug = match[2].replace(/\.tsx?$/, "")
236
+ // Handle "X", "X as Y" (use Y), "default as Y" (use Y)
237
+ const asMatch = importClause.match(/(?:\w+\s+)?as\s+(\w+)/)
238
+ const componentName = asMatch ? asMatch[1] : importClause.match(/(\w+)/)?.[1]
239
+ if (componentName) {
240
+ componentToSlug[componentName] = slug
241
+ }
230
242
  }
231
243
 
232
244
  // 2. Parse transition exports
@@ -238,20 +250,38 @@ export function parseDeckConfig(cwd) {
238
250
  const dirMatch = content.match(/export\s+const\s+directionalTransition\s*=\s*(true|false)/)
239
251
  if (dirMatch) directionalTransition = dirMatch[1] === "true"
240
252
 
241
- // 3. Parse slides array entries (order-independent matching)
253
+ // 3. Extract slides array content, then parse entries within it
242
254
  const slides = []
243
- const entryRegex = /\{([^}]+)\}/g
244
- for (const match of content.matchAll(entryRegex)) {
245
- const body = match[1]
246
- const componentMatch = body.match(/component:\s*(\w+)/)
247
- const stepsMatch = body.match(/steps:\s*(\d+)/)
248
- if (!componentMatch || !stepsMatch) continue
249
- const slug = componentToSlug[componentMatch[1]]
250
- if (!slug) continue // layout or unknown import — skip
251
- const entry = { slug, steps: parseInt(stepsMatch[1], 10) }
252
- const sectionMatch = body.match(/section:\s*["']([^"']+)["']/)
253
- if (sectionMatch) entry.section = sectionMatch[1]
254
- slides.push(entry)
255
+ const slidesArrayMatch = content.match(/export\s+const\s+slides\s*:\s*SlideConfig\[\]\s*=\s*\[/)
256
+ if (slidesArrayMatch) {
257
+ const arrayStartIdx = content.indexOf(slidesArrayMatch[0]) + slidesArrayMatch[0].length
258
+ // Find matching closing bracket
259
+ let bracketDepth = 1
260
+ let arrayEndIdx = arrayStartIdx
261
+ for (let i = arrayStartIdx; i < content.length; i++) {
262
+ if (content[i] === "[") bracketDepth++
263
+ if (content[i] === "]") bracketDepth--
264
+ if (bracketDepth === 0) {
265
+ arrayEndIdx = i
266
+ break
267
+ }
268
+ }
269
+ const arrayContent = content.slice(arrayStartIdx, arrayEndIdx)
270
+
271
+ // Match entries within the slides array only
272
+ const entryRegex = /\{([^}]+)\}/g
273
+ for (const match of arrayContent.matchAll(entryRegex)) {
274
+ const body = match[1]
275
+ const componentMatch = body.match(/component:\s*(\w+)/)
276
+ if (!componentMatch) continue
277
+ const slug = componentToSlug[componentMatch[1]]
278
+ if (!slug) continue // layout or unknown import — skip
279
+ const stepsMatch = body.match(/steps:\s*(\d+)/)
280
+ const entry = { slug, steps: stepsMatch ? parseInt(stepsMatch[1], 10) : 0 }
281
+ const sectionMatch = body.match(/section:\s*["']([^"']+)["']/)
282
+ if (sectionMatch) entry.section = sectionMatch[1]
283
+ slides.push(entry)
284
+ }
255
285
  }
256
286
 
257
287
  if (slides.length === 0) return null
@@ -71,7 +71,9 @@ export function isFileDirty(cwd, relativePath, storedHash) {
71
71
  */
72
72
  export function updateLockfileItem(cwd, slug, version, files) {
73
73
  const lock = readLockfile(cwd)
74
+ const existing = lock.items[slug] || {}
74
75
  lock.items[slug] = {
76
+ ...existing,
75
77
  version,
76
78
  installedAt: new Date().toISOString().split("T")[0],
77
79
  files
@@ -92,6 +94,51 @@ export function updateLockfilePublishConfig(cwd, config) {
92
94
  writeLockfile(cwd, lock)
93
95
  }
94
96
 
97
+ /**
98
+ * Read stored deck-level publish metadata (title, description, tags).
99
+ * @param {string} cwd
100
+ * @returns {{ title?: string, description?: string, tags?: string[] }}
101
+ */
102
+ export function readDeckMeta(cwd) {
103
+ const lock = readLockfile(cwd)
104
+ return lock.deckMeta || {}
105
+ }
106
+
107
+ /**
108
+ * Persist deck-level publish metadata so it can be reused as defaults.
109
+ * @param {string} cwd
110
+ * @param {{ title?: string, description?: string, tags?: string[] }} meta
111
+ */
112
+ export function updateDeckMeta(cwd, meta) {
113
+ const lock = readLockfile(cwd)
114
+ lock.deckMeta = { ...lock.deckMeta, ...meta }
115
+ writeLockfile(cwd, lock)
116
+ }
117
+
118
+ /**
119
+ * Read stored per-item publish metadata (title, description, tags, section).
120
+ * @param {string} cwd
121
+ * @param {string} slug
122
+ * @returns {{ title?: string, description?: string, tags?: string[], section?: string }}
123
+ */
124
+ export function readItemMeta(cwd, slug) {
125
+ const lock = readLockfile(cwd)
126
+ return lock.items[slug]?.meta || {}
127
+ }
128
+
129
+ /**
130
+ * Persist per-item publish metadata alongside the version/files entry.
131
+ * @param {string} cwd
132
+ * @param {string} slug
133
+ * @param {{ title?: string, description?: string, tags?: string[], section?: string }} meta
134
+ */
135
+ export function updateItemMeta(cwd, slug, meta) {
136
+ const lock = readLockfile(cwd)
137
+ if (!lock.items[slug]) lock.items[slug] = {}
138
+ lock.items[slug].meta = meta
139
+ writeLockfile(cwd, lock)
140
+ }
141
+
95
142
  /**
96
143
  * Remove a single item from the lockfile.
97
144
  * @param {string} cwd