promptslide 0.3.4 → 0.3.6

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.6](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.5...promptslide-v0.3.6) (2026-03-11)
4
+
5
+
6
+ ### Features
7
+
8
+ * show organization hint in publish command ([#73](https://github.com/prompticeu/promptslide/issues/73)) ([3002f25](https://github.com/prompticeu/promptslide/commit/3002f255f386a5bf646381808324ebda96c57707))
9
+
10
+ ## [0.3.5](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.4...promptslide-v0.3.5) (2026-03-08)
11
+
12
+
13
+ ### Features
14
+
15
+ * bundle shared sources with deck publish and fix deck-config parsing ([5f7fd1d](https://github.com/prompticeu/promptslide/commit/5f7fd1de6e5fc58a0568bd87830094dd40c9eca7))
16
+ * 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))
17
+
3
18
  ## [0.3.4](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.3...promptslide-v0.3.4) (2026-03-07)
4
19
 
5
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptslide",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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
  }
@@ -333,6 +393,11 @@ export async function publish(args) {
333
393
 
334
394
  const auth = requireAuth()
335
395
 
396
+ if (auth.organizationName) {
397
+ console.log(` ${dim("Organization:")} ${bold(auth.organizationName)} ${dim(`· switch with promptslide org`)}`)
398
+ console.log()
399
+ }
400
+
336
401
  // Determine file to publish
337
402
  let typeOverride = null
338
403
  const typeIdx = args.indexOf("--type")
@@ -423,6 +488,9 @@ export async function publish(args) {
423
488
  ? readdirSync(slidesDir).filter(f => f.endsWith(".tsx") || f.endsWith(".ts"))
424
489
  : []
425
490
 
491
+ // Discover shared source files (src/components/, src/lib/, etc.)
492
+ const sharedSources = discoverSharedSources(cwd)
493
+
426
494
  const totalItems = publicAssets.length + (hasTheme ? 1 : 0) + layoutEntries.length + slideEntries.length + 1
427
495
 
428
496
  // Display summary
@@ -431,16 +499,26 @@ export async function publish(args) {
431
499
  if (hasTheme) console.log(` Theme: 1`)
432
500
  if (layoutEntries.length) console.log(` Layouts: ${layoutEntries.length}`)
433
501
  console.log(` Slides: ${slideEntries.length}`)
502
+ if (sharedSources.length) console.log(` Shared: ${sharedSources.length} ${dim("(bundled with deck)")}`)
434
503
  console.log(` Total: ${totalItems} items`)
504
+ // Info if there are slide files on disk not referenced in deck-config.ts
505
+ const deckConfigSlideCount = deckConfig.slides.length
506
+ if (deckConfigSlideCount !== slideEntries.length) {
507
+ console.log()
508
+ console.log(` ${dim("ℹ")} deck-config.ts has ${deckConfigSlideCount} slides, ${slideEntries.length} slide files on disk`)
509
+ console.log(` ${dim("All slides are published. Only deck-config slides appear in the deck preview.")}`)
510
+ }
435
511
  if (deckConfig.transition) {
436
512
  console.log(` Transition: ${deckConfig.transition}${deckConfig.directionalTransition ? " (directional)" : ""}`)
437
513
  }
438
514
  console.log()
439
515
 
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):", "")
516
+ // Collect deck metadata (use stored values as defaults)
517
+ const storedDeckMeta = readDeckMeta(cwd)
518
+ const title = await prompt("Title:", storedDeckMeta.title || titleCase(deckSlug))
519
+ const description = await prompt("Description:", storedDeckMeta.description || "")
520
+ const storedTagsDefault = storedDeckMeta.tags?.length ? storedDeckMeta.tags.join(", ") : ""
521
+ const tagsInput = await prompt("Tags (comma-separated):", storedTagsDefault)
444
522
  const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
445
523
  const releaseNotes = await prompt("Release notes:", "")
446
524
 
@@ -692,12 +770,28 @@ export async function publish(args) {
692
770
 
693
771
  if (captureSession) await captureSession.close()
694
772
 
695
- // ── Phase 5: Deck manifest ──
773
+ // ── Phase 5: Deck manifest (includes shared source modules) ──
696
774
  itemIndex++
697
775
  const slideSlugs = slideEntries.map(f => `${deckSlug}/${f.replace(/\.tsx?$/, "")}`)
698
776
  const assetSlugs = publicAssets.map(a => assetFileToSlug(deckSlug, a.relativePath))
699
777
  const allDeckDeps = [...slideSlugs, ...assetSlugs]
700
778
 
779
+ // Bundle shared source files with the deck item so preview/install can resolve @/ imports
780
+ const sharedFiles = sharedSources.map(s => ({
781
+ path: s.fileName,
782
+ target: s.target,
783
+ content: readFileSync(s.fullPath, "utf-8")
784
+ }))
785
+ if (sharedFiles.length) {
786
+ console.log(` ${dim(`Bundling ${sharedFiles.length} shared source file(s) with deck`)}`)
787
+ }
788
+
789
+ // Collect npm deps from shared source files
790
+ const sharedNpmDeps = {}
791
+ for (const f of sharedFiles) {
792
+ Object.assign(sharedNpmDeps, detectNpmDeps(f.content))
793
+ }
794
+
701
795
  let deckItemId = null
702
796
  try {
703
797
  const result = await publishToRegistry({
@@ -707,7 +801,8 @@ export async function publish(args) {
707
801
  description: description || undefined,
708
802
  tags,
709
803
  deckConfig,
710
- files: [],
804
+ files: sharedFiles,
805
+ npmDependencies: Object.keys(sharedNpmDeps).length ? sharedNpmDeps : undefined,
711
806
  registryDependencies: allDeckDeps.length ? allDeckDeps : undefined,
712
807
  releaseNotes: releaseNotes || undefined,
713
808
  previewImage: previewImage || undefined,
@@ -717,6 +812,8 @@ export async function publish(args) {
717
812
  lockfileUpdates.push({ slug: deckSlug, version: result.version ?? 0, fileHashes: {} })
718
813
  deckItemId = result.id
719
814
  published++
815
+ // Persist deck metadata for next publish
816
+ updateDeckMeta(cwd, { title, description: description || undefined, tags })
720
817
  } catch (err) {
721
818
  console.log(` [${itemIndex}/${totalItems}] ${red("✗")} deck ${dim(deckSlug)}: ${err.message}`)
722
819
  failed++
@@ -725,7 +822,9 @@ export async function publish(args) {
725
822
  // Batch-write all lockfile updates at once
726
823
  const finalLock = readLockfile(cwd)
727
824
  for (const update of lockfileUpdates) {
825
+ const existing = finalLock.items[update.slug] || {}
728
826
  finalLock.items[update.slug] = {
827
+ ...existing,
729
828
  version: update.version,
730
829
  installedAt: new Date().toISOString().split("T")[0],
731
830
  files: update.fileHashes
@@ -864,12 +963,14 @@ export async function publish(args) {
864
963
  }
865
964
  }
866
965
 
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):", "")
966
+ // Collect metadata for main item (use stored values as defaults)
967
+ const storedMeta = readItemMeta(cwd, slug)
968
+ const title = await prompt("Title:", storedMeta.title || titleCase(baseSlug))
969
+ const description = await prompt("Description:", storedMeta.description || "")
970
+ const storedTagsDefault = storedMeta.tags?.length ? storedMeta.tags.join(", ") : ""
971
+ const tagsInput = await prompt("Tags (comma-separated):", storedTagsDefault)
871
972
  const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
872
- const section = await prompt("Section:", "")
973
+ const section = await prompt("Section:", storedMeta.section || "")
873
974
  const releaseNotes = await prompt("Release notes:", "")
874
975
  const previewImagePath = await prompt("Preview image path (leave empty to auto-generate):", "")
875
976
  let previewImage = null
@@ -927,6 +1028,13 @@ export async function publish(args) {
927
1028
  // Track in lockfile
928
1029
  const fileHashes = { [target + fileName]: hashContent(content) }
929
1030
  updateLockfileItem(cwd, slug, result.version ?? 0, fileHashes)
1031
+ // Persist item metadata for next publish
1032
+ updateItemMeta(cwd, slug, {
1033
+ title,
1034
+ description: description || undefined,
1035
+ tags,
1036
+ section: section || undefined
1037
+ })
930
1038
  } catch (err) {
931
1039
  console.error(` ${red("Error:")} ${err.message}`)
932
1040
  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