promptslide 0.3.15 → 0.3.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptslide",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "description": "CLI and slide engine for PromptSlide presentations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -24,6 +24,7 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "build": "tsup",
27
+ "test": "node --test test/*.test.mjs",
27
28
  "prepublishOnly": "npm run build"
28
29
  },
29
30
  "license": "MIT",
@@ -21,6 +21,7 @@ import {
21
21
  addSlideToDeckConfig,
22
22
  replaceDeckConfig
23
23
  } from "../utils/deck-config.mjs"
24
+ import { collectRegistrySlideSlugs, partitionDeckSlides } from "../utils/deck-sync.mjs"
24
25
  import { confirm, closePrompts } from "../utils/prompts.mjs"
25
26
 
26
27
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -164,9 +165,15 @@ export async function add(args) {
164
165
  }
165
166
  }
166
167
  } else if (item.type === "deck" && item.meta?.slides) {
168
+ const registrySlideSlugs = collectRegistrySlideSlugs(resolved.items)
169
+ const { available, missing } = partitionDeckSlides(item.meta.slides, registrySlideSlugs)
170
+ if (missing.length > 0) {
171
+ console.log(` ${yellow("⚠")} Skipped ${missing.length} slide(s) listed in the deck but not included in this version:`)
172
+ console.log(` ${dim(missing.map(s => s.slug).join(", "))}`)
173
+ }
167
174
  const shouldReplace = await confirm(" Replace entire deck-config.ts with this deck?", true)
168
175
  if (shouldReplace) {
169
- const slides = item.meta.slides.map(s => ({
176
+ const slides = available.map(s => ({
170
177
  componentName: s.componentName || toPascalCase(s.slug),
171
178
  importPath: `@/slides/${s.slug}`,
172
179
  steps: s.steps,
@@ -179,7 +186,7 @@ export async function add(args) {
179
186
  console.log(` ${green("✓")} Replaced ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`)
180
187
  } else {
181
188
  // Append individual slides
182
- for (const s of item.meta.slides) {
189
+ for (const s of available) {
183
190
  const componentName = s.componentName || toPascalCase(s.slug)
184
191
  const importPath = `@/slides/${s.slug}`
185
192
  const updated = addSlideToDeckConfig(cwd, { componentName, importPath, steps: s.steps })
@@ -5,9 +5,14 @@ import { fileURLToPath } from "node:url"
5
5
  import { bold, green, cyan, red, dim, yellow } from "../utils/ansi.mjs"
6
6
  import { requireAuth } from "../utils/auth.mjs"
7
7
  import { hexToOklch } from "../utils/colors.mjs"
8
- import { closePrompts } from "../utils/prompts.mjs"
9
- import { fetchRegistryItem, resolveRegistryDependencies, writeLockfile } from "../utils/registry.mjs"
10
8
  import { toPascalCase, replaceDeckConfig } from "../utils/deck-config.mjs"
9
+ import { collectRegistrySlideSlugs, partitionDeckSlides } from "../utils/deck-sync.mjs"
10
+ import { closePrompts } from "../utils/prompts.mjs"
11
+ import {
12
+ fetchRegistryItem,
13
+ resolveRegistryDependencies,
14
+ writeLockfile
15
+ } from "../utils/registry.mjs"
11
16
  import { ensureTsConfig } from "../utils/tsconfig.mjs"
12
17
 
13
18
  const __filename = fileURLToPath(import.meta.url)
@@ -62,7 +67,9 @@ export async function clone(args) {
62
67
  }
63
68
 
64
69
  if (item.type !== "deck") {
65
- console.error(` ${red("Error:")} "${slug}" is a ${item.type}, not a deck. Only decks can be cloned.`)
70
+ console.error(
71
+ ` ${red("Error:")} "${slug}" is a ${item.type}, not a deck. Only decks can be cloned.`
72
+ )
66
73
  process.exit(1)
67
74
  }
68
75
 
@@ -74,8 +81,12 @@ export async function clone(args) {
74
81
  const localParts = CLI_VERSION.match(/^(\d+)\.(\d+)/)
75
82
  if (pubParts && localParts && pubParts[2] !== localParts[2]) {
76
83
  console.log()
77
- console.log(` ${yellow("⚠")} This deck was published with promptslide ${bold(`v${item.promptslideVersion}`)}`)
78
- console.log(` You have ${bold(`v${CLI_VERSION}`)} installed — some slides may need updating.`)
84
+ console.log(
85
+ ` ${yellow("⚠")} This deck was published with promptslide ${bold(`v${item.promptslideVersion}`)}`
86
+ )
87
+ console.log(
88
+ ` You have ${bold(`v${CLI_VERSION}`)} installed — some slides may need updating.`
89
+ )
79
90
  }
80
91
  }
81
92
 
@@ -99,7 +110,11 @@ export async function clone(args) {
99
110
  const replacements = [
100
111
  {
101
112
  path: join(targetDir, "package.json"),
102
- values: { "{{PROJECT_SLUG}}": dirName, "{{PROJECT_NAME}}": projectName, "{{PROMPTSLIDE_VERSION}}": `^${CLI_VERSION}` }
113
+ values: {
114
+ "{{PROJECT_SLUG}}": dirName,
115
+ "{{PROJECT_NAME}}": projectName,
116
+ "{{PROMPTSLIDE_VERSION}}": `^${CLI_VERSION}`
117
+ }
103
118
  },
104
119
  {
105
120
  path: join(targetDir, "src", "theme.ts"),
@@ -148,7 +163,10 @@ export async function clone(args) {
148
163
 
149
164
  const dataUriPrefix = file.content.match(/^data:[^;]+;base64,/)
150
165
  if (dataUriPrefix) {
151
- writeFileSync(targetPath, Buffer.from(file.content.slice(dataUriPrefix[0].length), "base64"))
166
+ writeFileSync(
167
+ targetPath,
168
+ Buffer.from(file.content.slice(dataUriPrefix[0].length), "base64")
169
+ )
152
170
  } else {
153
171
  writeFileSync(targetPath, file.content, "utf-8")
154
172
  }
@@ -158,9 +176,16 @@ export async function clone(args) {
158
176
 
159
177
  // 6. Generate deck-config.ts
160
178
  if (item.meta?.slides) {
161
- const slides = item.meta.slides.map(s => ({
179
+ const registrySlideSlugs = collectRegistrySlideSlugs(resolved.items)
180
+ const { available, missing } = partitionDeckSlides(item.meta.slides, registrySlideSlugs)
181
+ if (missing.length > 0) {
182
+ console.log(` ${yellow("!")} Skipped ${missing.length} slide(s) listed in the deck but not included in this version:`)
183
+ console.log(` ${dim(missing.map(s => s.slug).join(", "))}`)
184
+ }
185
+ const slides = available.map(s => ({
162
186
  componentName: s.componentName || toPascalCase(s.slug),
163
187
  importPath: `@/slides/${s.slug}`,
188
+ id: s.id,
164
189
  steps: s.steps,
165
190
  section: s.section
166
191
  }))
@@ -168,7 +193,9 @@ export async function clone(args) {
168
193
  transition: item.meta.transition,
169
194
  directionalTransition: item.meta.directionalTransition
170
195
  })
171
- console.log(` ${green("✓")} Generated ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`)
196
+ console.log(
197
+ ` ${green("✓")} Generated ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`
198
+ )
172
199
  }
173
200
 
174
201
  // 7. Add npm dependencies
@@ -188,24 +215,37 @@ export async function clone(args) {
188
215
  if (item.id) {
189
216
  try {
190
217
  const annotationsRes = await fetch(`${auth.registry}/api/items/${item.id}/annotations`, {
191
- headers: { Authorization: `Bearer ${auth.token}`, ...(auth.organizationId ? { "X-Organization-Id": auth.organizationId } : {}) }
218
+ headers: {
219
+ Authorization: `Bearer ${auth.token}`,
220
+ ...(auth.organizationId ? { "X-Organization-Id": auth.organizationId } : {})
221
+ }
192
222
  })
193
223
  if (annotationsRes.ok) {
194
224
  const data = await annotationsRes.json()
195
225
  const annotations = data.annotations ?? []
196
226
  if (annotations.length > 0) {
197
- const annotationsFile = { version: 1, annotations: annotations.map(a => ({
198
- id: a.id,
199
- slideIndex: a.slideIndex,
200
- slideTitle: a.slideTitle,
201
- target: a.target,
202
- body: a.body,
203
- createdAt: a.createdAt,
204
- status: a.status,
205
- ...(a.resolution ? { resolution: a.resolution } : {})
206
- })) }
207
- writeFileSync(join(targetDir, "annotations.json"), JSON.stringify(annotationsFile, null, 2) + "\n", "utf-8")
208
- console.log(` ${green("✓")} Pulled ${cyan("annotations.json")} ${dim(`(${annotations.length} annotation${annotations.length === 1 ? "" : "s"})`)}`)
227
+ const annotationsFile = {
228
+ version: 1,
229
+ annotations: annotations.map(a => ({
230
+ id: a.id,
231
+ slideIndex: a.slideIndex,
232
+ ...(a.slideId ? { slideId: a.slideId } : {}),
233
+ slideTitle: a.slideTitle,
234
+ target: a.target,
235
+ body: a.body,
236
+ createdAt: a.createdAt,
237
+ status: a.status,
238
+ ...(a.resolution ? { resolution: a.resolution } : {})
239
+ }))
240
+ }
241
+ writeFileSync(
242
+ join(targetDir, "annotations.json"),
243
+ JSON.stringify(annotationsFile, null, 2) + "\n",
244
+ "utf-8"
245
+ )
246
+ console.log(
247
+ ` ${green("✓")} Pulled ${cyan("annotations.json")} ${dim(`(${annotations.length} annotation${annotations.length === 1 ? "" : "s"})`)}`
248
+ )
209
249
  }
210
250
  }
211
251
  } catch {
@@ -9,6 +9,7 @@ import { hexToOklch, isValidHex } from "../utils/colors.mjs"
9
9
  import { prompt, confirm, closePrompts } from "../utils/prompts.mjs"
10
10
  import { fetchRegistryItem, resolveRegistryDependencies, writeLockfile } from "../utils/registry.mjs"
11
11
  import { toPascalCase, replaceDeckConfig } from "../utils/deck-config.mjs"
12
+ import { collectRegistrySlideSlugs, partitionDeckSlides } from "../utils/deck-sync.mjs"
12
13
  import { ensureTsConfig } from "../utils/tsconfig.mjs"
13
14
 
14
15
  const __filename = fileURLToPath(import.meta.url)
@@ -221,7 +222,13 @@ export async function create(args) {
221
222
 
222
223
  // Reconstruct deck-config.ts from deckConfig metadata
223
224
  if (fromItem.meta?.slides) {
224
- const slides = fromItem.meta.slides.map(s => ({
225
+ const registrySlideSlugs = collectRegistrySlideSlugs(resolved.items)
226
+ const { available, missing } = partitionDeckSlides(fromItem.meta.slides, registrySlideSlugs)
227
+ if (missing.length > 0) {
228
+ console.log(` ${yellow("⚠")} Skipped ${missing.length} slide(s) listed in the deck but not included in this version:`)
229
+ console.log(` ${dim(missing.map(s => s.slug).join(", "))}`)
230
+ }
231
+ const slides = available.map(s => ({
225
232
  componentName: s.componentName || toPascalCase(s.slug),
226
233
  importPath: `@/slides/${s.slug}`,
227
234
  steps: s.steps,
@@ -5,9 +5,10 @@ import { fileURLToPath } from "node:url"
5
5
  import { bold, green, cyan, red, yellow, 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, fetchRegistryItem, searchRegistry, updateLockfileItem, updateLockfilePublishConfig, readLockfile, writeLockfile, hashContent, detectPackageManager, requestUploadTokens, uploadBinaryToBlob, assetFileToSlug, detectAssetDepsInContent, readDeckMeta, updateDeckMeta, readItemMeta, updateItemMeta } from "../utils/registry.mjs"
8
+ import { publishToRegistry, registryItemExists, fetchRegistryItem, searchRegistry, updateLockfileItem, updateLockfilePublishConfig, readLockfile, writeLockfile, hashContent, detectPackageManager, requestUploadTokens, uploadBinaryToBlob, assetFileToSlug, detectAssetDepsInContent, readDeckMeta, updateDeckMeta, readItemMeta, updateItemMeta, pruneMissingLockfileItems } 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
+ import { selectSlidesForPublish } from "../utils/deck-sync.mjs"
11
12
 
12
13
  const __dirname = dirname(fileURLToPath(import.meta.url))
13
14
  const CLI_VERSION = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8")).version
@@ -412,7 +413,7 @@ export async function publish(args) {
412
413
  console.log()
413
414
 
414
415
  if (args[0] === "--help" || args[0] === "-h") {
415
- console.log(` ${bold("Usage:")} promptslide publish ${dim("[file] [--type slide|layout|deck|theme]")}`)
416
+ console.log(` ${bold("Usage:")} promptslide publish ${dim("[file] [--type slide|layout|deck|theme] [--all]")}`)
416
417
  console.log()
417
418
  console.log(` Publish a slide, layout, or entire deck to the registry.`)
418
419
  console.log()
@@ -421,6 +422,9 @@ export async function publish(args) {
421
422
  console.log(` promptslide publish --type layout`)
422
423
  console.log(` promptslide publish --type deck`)
423
424
  console.log()
425
+ console.log(` ${bold("Options:")}`)
426
+ console.log(` --all Include slide files not referenced by deck-config.ts in a deck publish`)
427
+ console.log()
424
428
  process.exit(0)
425
429
  }
426
430
 
@@ -433,6 +437,7 @@ export async function publish(args) {
433
437
 
434
438
  // Determine file to publish
435
439
  let typeOverride = null
440
+ const includeUnreferencedSlides = args.includes("--all")
436
441
  const typeIdx = args.indexOf("--type")
437
442
  if (typeIdx !== -1 && args[typeIdx + 1]) {
438
443
  typeOverride = args[typeIdx + 1]
@@ -538,9 +543,23 @@ export async function publish(args) {
538
543
  : []
539
544
 
540
545
  const slidesDir = join(cwd, "src", "slides")
541
- const slideEntries = existsSync(slidesDir)
546
+ const slideFilesOnDisk = existsSync(slidesDir)
542
547
  ? readdirSync(slidesDir).filter(f => f.endsWith(".tsx") || f.endsWith(".ts"))
543
548
  : []
549
+ const slideSelection = selectSlidesForPublish(
550
+ slideFilesOnDisk,
551
+ deckConfig.slides,
552
+ includeUnreferencedSlides
553
+ )
554
+ if (slideSelection.missing.length > 0) {
555
+ console.error(` ${red("Error:")} deck-config.ts references slide files that do not exist:`)
556
+ console.error(` ${slideSelection.missing.join(", ")}`)
557
+ console.error(` ${dim("Restore the files or remove their entries from deck-config.ts before publishing.")}`)
558
+ console.log()
559
+ closePrompts()
560
+ process.exit(1)
561
+ }
562
+ const slideEntries = slideSelection.selected
544
563
 
545
564
  // Discover shared source files (src/components/, src/lib/, etc.)
546
565
  const sharedSources = discoverSharedSources(cwd)
@@ -555,12 +574,14 @@ export async function publish(args) {
555
574
  console.log(` Slides: ${slideEntries.length}`)
556
575
  if (sharedSources.length) console.log(` Shared: ${sharedSources.length} ${dim("(bundled with deck)")}`)
557
576
  console.log(` Total: ${totalItems} items`)
558
- // Info if there are slide files on disk not referenced in deck-config.ts
559
- const deckConfigSlideCount = deckConfig.slides.length
560
- if (deckConfigSlideCount !== slideEntries.length) {
577
+ if (slideSelection.unreferenced.length > 0) {
561
578
  console.log()
562
- console.log(` ${dim("ℹ")} deck-config.ts has ${deckConfigSlideCount} slides, ${slideEntries.length} slide files on disk`)
563
- console.log(` ${dim("All slides are published. Only deck-config slides appear in the deck preview.")}`)
579
+ if (includeUnreferencedSlides) {
580
+ console.log(` ${yellow("!")} Including ${slideSelection.unreferenced.length} slide file(s) not referenced by deck-config.ts because --all was set:`)
581
+ } else {
582
+ console.log(` ${dim("ℹ")} Skipping ${slideSelection.unreferenced.length} slide file(s) not referenced by deck-config.ts:`)
583
+ }
584
+ console.log(` ${dim(slideSelection.unreferenced.join(", "))}`)
564
585
  }
565
586
  if (deckConfig.transition) {
566
587
  console.log(` Transition: ${deckConfig.transition}${deckConfig.directionalTransition ? " (directional)" : ""}`)
@@ -612,6 +633,11 @@ export async function publish(args) {
612
633
 
613
634
  // Read lockfile for skip-if-unchanged
614
635
  const lock = readLockfile(cwd)
636
+ const prunedLockItems = pruneMissingLockfileItems(cwd, lock)
637
+ if (prunedLockItems.length > 0) {
638
+ writeLockfile(cwd, lock)
639
+ console.log(` ${dim("Pruned missing files from lockfile:")} ${prunedLockItems.join(", ")}`)
640
+ }
615
641
  let published = 0
616
642
  let skipped = 0
617
643
  let failed = 0
@@ -1,9 +1,11 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
2
1
  import { execFileSync } from "node:child_process"
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
3
3
  import { join, dirname, resolve, sep } from "node:path"
4
4
 
5
5
  import { bold, green, cyan, red, yellow, dim } from "../utils/ansi.mjs"
6
6
  import { requireAuth } from "../utils/auth.mjs"
7
+ import { toPascalCase, addSlideToDeckConfig, replaceDeckConfig } from "../utils/deck-config.mjs"
8
+ import { confirm, closePrompts } from "../utils/prompts.mjs"
7
9
  import {
8
10
  fetchRegistryItem,
9
11
  resolveRegistryDependencies,
@@ -15,12 +17,7 @@ import {
15
17
  hashFile,
16
18
  isFileDirty
17
19
  } from "../utils/registry.mjs"
18
- import {
19
- toPascalCase,
20
- addSlideToDeckConfig,
21
- replaceDeckConfig
22
- } from "../utils/deck-config.mjs"
23
- import { confirm, closePrompts } from "../utils/prompts.mjs"
20
+ import { collectRegistrySlideSlugs, partitionDeckSlides } from "../utils/deck-sync.mjs"
24
21
 
25
22
  export async function pull(args) {
26
23
  const cwd = process.cwd()
@@ -50,7 +47,9 @@ export async function pull(args) {
50
47
 
51
48
  if (!deckSlug) {
52
49
  console.error(` ${red("Error:")} No deck slug found in lockfile.`)
53
- console.error(` ${dim("Run")} ${cyan("promptslide publish --type deck")} ${dim("first to set it.")}`)
50
+ console.error(
51
+ ` ${dim("Run")} ${cyan("promptslide publish --type deck")} ${dim("first to set it.")}`
52
+ )
54
53
  console.log()
55
54
  process.exit(1)
56
55
  }
@@ -68,7 +67,9 @@ export async function pull(args) {
68
67
  }
69
68
 
70
69
  const versionTag = deckItem.version ? ` ${dim(`v${deckItem.version}`)}` : ""
71
- console.log(` Found ${bold(deckItem.title || deckItem.name)} ${dim(`(${deckItem.type})`)}${versionTag}`)
70
+ console.log(
71
+ ` Found ${bold(deckItem.title || deckItem.name)} ${dim(`(${deckItem.type})`)}${versionTag}`
72
+ )
72
73
 
73
74
  // Resolve all dependencies (slides, layouts, themes, assets)
74
75
  let resolved
@@ -137,7 +138,10 @@ export async function pull(args) {
137
138
  }
138
139
  } else if (!storedHash) {
139
140
  // File exists but not in lockfile (first pull or untracked)
140
- const overwrite = await confirm(` Overwrite ${relativePath}? ${dim("(local changes will be lost)")}`, false)
141
+ const overwrite = await confirm(
142
+ ` Overwrite ${relativePath}? ${dim("(local changes will be lost)")}`,
143
+ false
144
+ )
141
145
  if (!overwrite) {
142
146
  console.log(` ${dim("Skipped")} ${relativePath}`)
143
147
  continue
@@ -150,7 +154,10 @@ export async function pull(args) {
150
154
  mkdirSync(targetDir, { recursive: true })
151
155
  const dataUriPrefix = file.content.match(/^data:[^;]+;base64,/)
152
156
  if (dataUriPrefix) {
153
- writeFileSync(targetPath, Buffer.from(file.content.slice(dataUriPrefix[0].length), "base64"))
157
+ writeFileSync(
158
+ targetPath,
159
+ Buffer.from(file.content.slice(dataUriPrefix[0].length), "base64")
160
+ )
154
161
  } else {
155
162
  writeFileSync(targetPath, file.content, "utf-8")
156
163
  }
@@ -174,11 +181,18 @@ export async function pull(args) {
174
181
 
175
182
  // Update deck-config.ts
176
183
  if (deckItem.type === "deck" && deckItem.meta?.slides) {
184
+ const registrySlideSlugs = collectRegistrySlideSlugs(resolved.items)
185
+ const { available, missing } = partitionDeckSlides(deckItem.meta.slides, registrySlideSlugs)
186
+ if (missing.length > 0) {
187
+ console.log(` ${yellow("!")} Skipped ${missing.length} slide(s) listed in the deck but not included in this version:`)
188
+ console.log(` ${dim(missing.map(s => s.slug).join(", "))}`)
189
+ }
177
190
  const shouldReplace = await confirm(" Replace deck-config.ts with pulled deck config?", true)
178
191
  if (shouldReplace) {
179
- const slides = deckItem.meta.slides.map(s => ({
192
+ const slides = available.map(s => ({
180
193
  componentName: s.componentName || toPascalCase(s.slug),
181
194
  importPath: `@/slides/${s.slug}`,
195
+ id: s.id,
182
196
  steps: s.steps,
183
197
  section: s.section
184
198
  }))
@@ -186,13 +200,21 @@ export async function pull(args) {
186
200
  transition: deckItem.meta.transition,
187
201
  directionalTransition: deckItem.meta.directionalTransition
188
202
  })
189
- console.log(` ${green("+")} Replaced ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`)
203
+ console.log(
204
+ ` ${green("+")} Replaced ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`
205
+ )
190
206
  } else {
191
207
  // Append individual slides
192
- for (const s of deckItem.meta.slides) {
208
+ for (const s of available) {
193
209
  const componentName = s.componentName || toPascalCase(s.slug)
194
210
  const importPath = `@/slides/${s.slug}`
195
- const updated = addSlideToDeckConfig(cwd, { componentName, importPath, steps: s.steps, section: s.section })
211
+ const updated = addSlideToDeckConfig(cwd, {
212
+ componentName,
213
+ importPath,
214
+ steps: s.steps,
215
+ id: s.id,
216
+ section: s.section
217
+ })
196
218
  if (updated) {
197
219
  console.log(` ${green("+")} Added ${componentName} to ${cyan("deck-config.ts")}`)
198
220
  }
@@ -220,24 +242,37 @@ export async function pull(args) {
220
242
  if (deckItem.id) {
221
243
  try {
222
244
  const annotationsRes = await fetch(`${auth.registry}/api/items/${deckItem.id}/annotations`, {
223
- headers: { Authorization: `Bearer ${auth.token}`, ...(auth.organizationId ? { "X-Organization-Id": auth.organizationId } : {}) }
245
+ headers: {
246
+ Authorization: `Bearer ${auth.token}`,
247
+ ...(auth.organizationId ? { "X-Organization-Id": auth.organizationId } : {})
248
+ }
224
249
  })
225
250
  if (annotationsRes.ok) {
226
251
  const data = await annotationsRes.json()
227
252
  const annotations = data.annotations ?? []
228
253
  if (annotations.length > 0) {
229
- const annotationsFile = { version: 1, annotations: annotations.map(a => ({
230
- id: a.id,
231
- slideIndex: a.slideIndex,
232
- slideTitle: a.slideTitle,
233
- target: a.target,
234
- body: a.body,
235
- createdAt: a.createdAt,
236
- status: a.status,
237
- ...(a.resolution ? { resolution: a.resolution } : {})
238
- })) }
239
- writeFileSync(join(cwd, "annotations.json"), JSON.stringify(annotationsFile, null, 2) + "\n", "utf-8")
240
- console.log(` ${green("+")} ${cyan("annotations.json")} ${dim(`(${annotations.length} annotation${annotations.length === 1 ? "" : 's'})`)}`)
254
+ const annotationsFile = {
255
+ version: 1,
256
+ annotations: annotations.map(a => ({
257
+ id: a.id,
258
+ slideIndex: a.slideIndex,
259
+ ...(a.slideId ? { slideId: a.slideId } : {}),
260
+ slideTitle: a.slideTitle,
261
+ target: a.target,
262
+ body: a.body,
263
+ createdAt: a.createdAt,
264
+ status: a.status,
265
+ ...(a.resolution ? { resolution: a.resolution } : {})
266
+ }))
267
+ }
268
+ writeFileSync(
269
+ join(cwd, "annotations.json"),
270
+ JSON.stringify(annotationsFile, null, 2) + "\n",
271
+ "utf-8"
272
+ )
273
+ console.log(
274
+ ` ${green("+")} ${cyan("annotations.json")} ${dim(`(${annotations.length} annotation${annotations.length === 1 ? "" : "s"})`)}`
275
+ )
241
276
  }
242
277
  }
243
278
  } catch {
@@ -250,7 +285,9 @@ export async function pull(args) {
250
285
  if (written.length === 0) {
251
286
  console.log(` ${green("+")} Everything is up to date.`)
252
287
  } else {
253
- console.log(` ${green("+")} Pulled ${bold(String(written.length))} file(s) from ${bold(deckSlug)}`)
288
+ console.log(
289
+ ` ${green("+")} Pulled ${bold(String(written.length))} file(s) from ${bold(deckSlug)}`
290
+ )
254
291
  }
255
292
  console.log()
256
293
  closePrompts()
@@ -1,6 +1,7 @@
1
1
  import { createServer } from "vite"
2
2
 
3
3
  import { bold, dim } from "../utils/ansi.mjs"
4
+ import { registerStudioServer } from "../utils/studio-discovery.mjs"
4
5
  import { ensureTsConfig } from "../utils/tsconfig.mjs"
5
6
  import { createViteConfig } from "../vite/config.mjs"
6
7
 
@@ -29,6 +30,8 @@ export async function studio(args) {
29
30
  })
30
31
 
31
32
  await server.listen()
33
+ const baseUrl = server.resolvedUrls?.local[0]
34
+ if (baseUrl) registerStudioServer(cwd, baseUrl)
32
35
  server.printUrls()
33
36
  server.bindCLIShortcuts({ print: true })
34
37
  }
@@ -8,6 +8,8 @@ export async function toImage(args) {
8
8
  const slidePath = args[0]
9
9
  const outputIndex = args.indexOf("-o")
10
10
  const output = outputIndex !== -1 ? args[outputIndex + 1] : null
11
+ const portArg = args.find(arg => arg.startsWith("--port="))
12
+ const studioPort = portArg ? Number(portArg.slice(7)) : undefined
11
13
 
12
14
  if (!slidePath || slidePath === "--help" || slidePath === "-h") {
13
15
  console.log()
@@ -15,6 +17,7 @@ export async function toImage(args) {
15
17
  console.log()
16
18
  console.log(` ${bold("Options:")}`)
17
19
  console.log(` -o <file> Output file path (default: <slide-name>.png)`)
20
+ console.log(` --port=N Studio port to reuse (default: auto-discover)`)
18
21
  console.log()
19
22
  console.log(` ${bold("Examples:")}`)
20
23
  console.log(` ${dim("promptslide to-image src/slides/slide-title.tsx")}`)
@@ -26,7 +29,9 @@ export async function toImage(args) {
26
29
  if (!(await isPlaywrightAvailable())) {
27
30
  console.error()
28
31
  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")}`)
32
+ console.error(
33
+ ` Install it with: ${bold("npm install -D playwright && npx playwright install chromium")}`
34
+ )
30
35
  console.error()
31
36
  process.exit(1)
32
37
  }
@@ -36,10 +41,18 @@ export async function toImage(args) {
36
41
  console.log()
37
42
  console.log(` ${dim("Capturing screenshot...")}`)
38
43
 
39
- const buffer = await captureSlideScreenshot({
40
- cwd: process.cwd(),
41
- slidePath
42
- })
44
+ let buffer
45
+ try {
46
+ buffer = await captureSlideScreenshot({
47
+ cwd: process.cwd(),
48
+ slidePath,
49
+ studioPort
50
+ })
51
+ } catch (error) {
52
+ console.error(` ${red("Error:")} ${error.message}`)
53
+ process.exitCode = 1
54
+ return
55
+ }
43
56
 
44
57
  if (!buffer) {
45
58
  console.error(` ${red("Error:")} Failed to capture screenshot.`)
@@ -0,0 +1,27 @@
1
+ import { writeFileSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+
4
+ import { bold, green, dim, red } from "../utils/ansi.mjs"
5
+ import { captureDeckPdf } from "../utils/export.mjs"
6
+
7
+ export async function toPdf(args) {
8
+ if (args.includes("--help") || args.includes("-h")) {
9
+ console.log(` ${bold("Usage:")} promptslide to-pdf [-o slides.pdf] [--port=N]`)
10
+ return
11
+ }
12
+ const outputIndex = args.indexOf("-o")
13
+ const output = resolve(outputIndex >= 0 ? args[outputIndex + 1] : "slides.pdf")
14
+ const portArg = args.find(arg => arg.startsWith("--port="))
15
+ const studioPort = portArg ? Number(portArg.slice(7)) : undefined
16
+ console.log(` ${dim("Rendering deck PDF...")}`)
17
+ let pdf
18
+ try {
19
+ pdf = await captureDeckPdf({ cwd: process.cwd(), studioPort })
20
+ } catch (error) {
21
+ console.error(` ${red("Error:")} ${error.message}`)
22
+ process.exitCode = 1
23
+ return
24
+ }
25
+ writeFileSync(output, pdf)
26
+ console.log(` ${green("✓")} Saved to ${bold(output)}`)
27
+ }
@@ -48,7 +48,7 @@ const animationVariants: Record<AnimationType, Variants> = {
48
48
  }
49
49
 
50
50
  interface AnimatedProps {
51
- /** Which step reveals this content (1-indexed) */
51
+ /** Which step reveals this content (0 = visible from slide entry) */
52
52
  step: number
53
53
  /** Animation type */
54
54
  animation?: AnimationType
@@ -83,7 +83,7 @@ export function Animated({
83
83
 
84
84
  return (
85
85
  <motion.div
86
- initial="hidden"
86
+ initial={isVisible ? false : "hidden"}
87
87
  animate={isVisible ? "visible" : "hidden"}
88
88
  variants={animationVariants[animation]}
89
89
  transition={{
@@ -138,7 +138,7 @@ export function AnimatedGroup({
138
138
 
139
139
  return (
140
140
  <motion.div
141
- initial="hidden"
141
+ initial={isVisible ? false : "hidden"}
142
142
  animate={isVisible ? "visible" : "hidden"}
143
143
  variants={containerVariants}
144
144
  className={className}
@@ -38,6 +38,14 @@ export function createHttpAdapter(): AnnotationStorageAdapter {
38
38
  async remove(id) {
39
39
  cache = cache.filter(a => a.id !== id)
40
40
  await saveAll(cache)
41
+ },
42
+ async update(id, patch) {
43
+ cache = cache.map(a => (a.id === id ? { ...a, ...patch } : a))
44
+ await saveAll(cache)
45
+ },
46
+ async replaceAll(annotations) {
47
+ cache = annotations
48
+ await saveAll(cache)
41
49
  }
42
50
  }
43
51
  }