promptslide 0.3.11 → 0.3.13

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.13](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.12...promptslide-v0.3.13) (2026-03-29)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * pin dependency versions and preserve component names in publish pipeline ([#89](https://github.com/prompticeu/promptslide/issues/89)) ([22b7dcb](https://github.com/prompticeu/promptslide/commit/22b7dcbe33c9852fac2fbfef70943254a0d871cd))
9
+
10
+ ## [0.3.12](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.11...promptslide-v0.3.12) (2026-03-28)
11
+
12
+
13
+ ### Features
14
+
15
+ * add clone command, make create --from always template ([#87](https://github.com/prompticeu/promptslide/issues/87)) ([769fa7e](https://github.com/prompticeu/promptslide/commit/769fa7ed0ee77f7473f49f2eb608ca996142f3fd))
16
+
3
17
  ## [0.3.11](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.10...promptslide-v0.3.11) (2026-03-25)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptslide",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "CLI and slide engine for PromptSlide presentations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -167,7 +167,7 @@ export async function add(args) {
167
167
  const shouldReplace = await confirm(" Replace entire deck-config.ts with this deck?", true)
168
168
  if (shouldReplace) {
169
169
  const slides = item.meta.slides.map(s => ({
170
- componentName: toPascalCase(s.slug),
170
+ componentName: s.componentName || toPascalCase(s.slug),
171
171
  importPath: `@/slides/${s.slug}`,
172
172
  steps: s.steps,
173
173
  section: s.section
@@ -180,7 +180,7 @@ export async function add(args) {
180
180
  } else {
181
181
  // Append individual slides
182
182
  for (const s of item.meta.slides) {
183
- const componentName = toPascalCase(s.slug)
183
+ const componentName = s.componentName || toPascalCase(s.slug)
184
184
  const importPath = `@/slides/${s.slug}`
185
185
  const updated = addSlideToDeckConfig(cwd, { componentName, importPath, steps: s.steps })
186
186
  if (updated) {
@@ -0,0 +1,231 @@
1
+ import { existsSync, cpSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
2
+ import { join, resolve, dirname } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+
5
+ import { bold, green, cyan, red, dim, yellow } from "../utils/ansi.mjs"
6
+ import { requireAuth } from "../utils/auth.mjs"
7
+ import { hexToOklch } from "../utils/colors.mjs"
8
+ import { closePrompts } from "../utils/prompts.mjs"
9
+ import { fetchRegistryItem, resolveRegistryDependencies, writeLockfile } from "../utils/registry.mjs"
10
+ import { toPascalCase, replaceDeckConfig } from "../utils/deck-config.mjs"
11
+ import { ensureTsConfig } from "../utils/tsconfig.mjs"
12
+
13
+ const __filename = fileURLToPath(import.meta.url)
14
+ const __dirname = dirname(__filename)
15
+ const CLI_ROOT = join(__dirname, "..", "..")
16
+ const TEMPLATE_DIR = join(CLI_ROOT, "templates", "default")
17
+ const CLI_VERSION = JSON.parse(readFileSync(join(CLI_ROOT, "package.json"), "utf-8")).version
18
+
19
+ function replaceInFile(filePath, replacements) {
20
+ let content = readFileSync(filePath, "utf-8")
21
+ for (const [placeholder, value] of Object.entries(replacements)) {
22
+ content = content.replaceAll(placeholder, value)
23
+ }
24
+ writeFileSync(filePath, content, "utf-8")
25
+ }
26
+
27
+ export async function clone(args) {
28
+ console.log()
29
+ console.log(` ${bold("promptslide")} ${dim("clone")}`)
30
+ console.log()
31
+
32
+ if (args[0] === "--help" || args[0] === "-h") {
33
+ console.log(` ${bold("Usage:")} promptslide clone ${dim("<deck-slug>")}`)
34
+ console.log()
35
+ console.log(` Clone a published deck to continue working on it locally.`)
36
+ console.log(` The deck stays linked for pull/publish.`)
37
+ console.log()
38
+ console.log(` ${bold("Examples:")}`)
39
+ console.log(` promptslide clone statworx-slide-master`)
40
+ console.log()
41
+ process.exit(0)
42
+ }
43
+
44
+ const slug = args[0]
45
+
46
+ if (!slug) {
47
+ console.error(` ${red("Error:")} Please provide a deck slug.`)
48
+ console.error(` ${dim("Usage:")} promptslide clone ${dim("<deck-slug>")}`)
49
+ console.log()
50
+ process.exit(1)
51
+ }
52
+
53
+ // 1. Authenticate and fetch deck
54
+ const auth = requireAuth()
55
+
56
+ let item
57
+ try {
58
+ item = await fetchRegistryItem(slug, auth)
59
+ } catch (err) {
60
+ console.error(` ${red("Error:")} ${err.message}`)
61
+ process.exit(1)
62
+ }
63
+
64
+ if (item.type !== "deck") {
65
+ console.error(` ${red("Error:")} "${slug}" is a ${item.type}, not a deck. Only decks can be cloned.`)
66
+ process.exit(1)
67
+ }
68
+
69
+ const versionTag = item.version ? ` ${dim(`v${item.version}`)}` : ""
70
+ console.log(` Cloning ${bold(item.title || item.name)}${versionTag}`)
71
+
72
+ if (item.promptslideVersion) {
73
+ const pubParts = item.promptslideVersion.match(/^(\d+)\.(\d+)/)
74
+ const localParts = CLI_VERSION.match(/^(\d+)\.(\d+)/)
75
+ if (pubParts && localParts && pubParts[2] !== localParts[2]) {
76
+ 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.`)
79
+ }
80
+ }
81
+
82
+ console.log()
83
+
84
+ // 2. Use slug as directory name
85
+ const dirName = slug
86
+ const targetDir = resolve(process.cwd(), dirName)
87
+
88
+ if (existsSync(targetDir)) {
89
+ console.error(` ${red("Error:")} Directory "${dirName}" already exists.`)
90
+ process.exit(1)
91
+ }
92
+
93
+ // 3. Scaffold base template
94
+ cpSync(TEMPLATE_DIR, targetDir, { recursive: true })
95
+
96
+ const projectName = item.title || slug
97
+ const primaryOklch = hexToOklch("#3B82F6")
98
+
99
+ const replacements = [
100
+ {
101
+ path: join(targetDir, "package.json"),
102
+ values: { "{{PROJECT_SLUG}}": dirName, "{{PROJECT_NAME}}": projectName, "{{PROMPTSLIDE_VERSION}}": `^${CLI_VERSION}` }
103
+ },
104
+ {
105
+ path: join(targetDir, "src", "theme.ts"),
106
+ values: { "{{PROJECT_NAME}}": projectName }
107
+ },
108
+ {
109
+ path: join(targetDir, "src", "slides", "slide-title.tsx"),
110
+ values: { "{{PROJECT_NAME}}": projectName }
111
+ },
112
+ {
113
+ path: join(targetDir, "README.md"),
114
+ values: { "{{PROJECT_NAME}}": projectName }
115
+ },
116
+ {
117
+ path: join(targetDir, "src", "globals.css"),
118
+ values: { "{{PRIMARY_COLOR}}": primaryOklch }
119
+ }
120
+ ]
121
+
122
+ for (const { path, values } of replacements) {
123
+ replaceInFile(path, values)
124
+ }
125
+
126
+ // 4. Create lockfile linked to the original deck
127
+ writeLockfile(targetDir, {
128
+ deckSlug: slug,
129
+ deckMeta: { title: "", description: "", tags: [] },
130
+ items: {}
131
+ })
132
+
133
+ // 5. Resolve dependencies and write deck files
134
+ let resolved
135
+ try {
136
+ resolved = await resolveRegistryDependencies(item, auth, targetDir)
137
+ } catch (err) {
138
+ console.error(` ${red("Error:")} ${err.message}`)
139
+ process.exit(1)
140
+ }
141
+
142
+ for (const regItem of resolved.items) {
143
+ if (!regItem.files?.length) continue
144
+ for (const file of regItem.files) {
145
+ const targetPath = join(targetDir, file.target, file.path)
146
+ const targetFileDir = dirname(targetPath)
147
+ mkdirSync(targetFileDir, { recursive: true })
148
+
149
+ const dataUriPrefix = file.content.match(/^data:[^;]+;base64,/)
150
+ if (dataUriPrefix) {
151
+ writeFileSync(targetPath, Buffer.from(file.content.slice(dataUriPrefix[0].length), "base64"))
152
+ } else {
153
+ writeFileSync(targetPath, file.content, "utf-8")
154
+ }
155
+ console.log(` ${green("✓")} Added ${cyan(file.target + file.path)}`)
156
+ }
157
+ }
158
+
159
+ // 6. Generate deck-config.ts
160
+ if (item.meta?.slides) {
161
+ const slides = item.meta.slides.map(s => ({
162
+ componentName: s.componentName || toPascalCase(s.slug),
163
+ importPath: `@/slides/${s.slug}`,
164
+ steps: s.steps,
165
+ section: s.section
166
+ }))
167
+ replaceDeckConfig(targetDir, slides, {
168
+ transition: item.meta.transition,
169
+ directionalTransition: item.meta.directionalTransition
170
+ })
171
+ console.log(` ${green("✓")} Generated ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`)
172
+ }
173
+
174
+ // 7. Add npm dependencies
175
+ if (Object.keys(resolved.npmDeps).length > 0) {
176
+ const pkgList = Object.entries(resolved.npmDeps).map(([name, ver]) => `${name}@${ver}`)
177
+ const pkgPath = join(targetDir, "package.json")
178
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"))
179
+ for (const [name, ver] of Object.entries(resolved.npmDeps)) {
180
+ pkg.dependencies = pkg.dependencies || {}
181
+ pkg.dependencies[name] = ver
182
+ }
183
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8")
184
+ console.log(` ${green("✓")} Added ${dim(pkgList.join(", "))} to package.json`)
185
+ }
186
+
187
+ // 8. Fetch annotations
188
+ if (item.id) {
189
+ try {
190
+ const annotationsRes = await fetch(`${auth.registry}/api/items/${item.id}/annotations`, {
191
+ headers: { Authorization: `Bearer ${auth.token}`, ...(auth.organizationId ? { "X-Organization-Id": auth.organizationId } : {}) }
192
+ })
193
+ if (annotationsRes.ok) {
194
+ const data = await annotationsRes.json()
195
+ const annotations = data.annotations ?? []
196
+ 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"})`)}`)
209
+ }
210
+ }
211
+ } catch {
212
+ // Annotations are non-critical; don't fail the clone
213
+ }
214
+ }
215
+
216
+ // 9. Generate tsconfig.json
217
+ ensureTsConfig(targetDir)
218
+
219
+ // 10. Success output
220
+ console.log()
221
+ console.log(` ${green("✓")} Cloned ${bold(projectName)} in ${cyan(dirName)}/`)
222
+ console.log()
223
+ console.log(` ${bold("Next steps:")}`)
224
+ console.log()
225
+ console.log(` cd ${dirName}`)
226
+ console.log(` bun install`)
227
+ console.log(` bun run dev`)
228
+ console.log()
229
+
230
+ closePrompts()
231
+ }
@@ -7,7 +7,7 @@ import { bold, green, cyan, red, dim, yellow } from "../utils/ansi.mjs"
7
7
  import { requireAuth } from "../utils/auth.mjs"
8
8
  import { hexToOklch, isValidHex } from "../utils/colors.mjs"
9
9
  import { prompt, confirm, closePrompts } from "../utils/prompts.mjs"
10
- import { fetchRegistryItem, resolveRegistryDependencies, updateLockfilePublishConfig, writeLockfile } from "../utils/registry.mjs"
10
+ import { fetchRegistryItem, resolveRegistryDependencies, writeLockfile } from "../utils/registry.mjs"
11
11
  import { toPascalCase, replaceDeckConfig } from "../utils/deck-config.mjs"
12
12
  import { ensureTsConfig } from "../utils/tsconfig.mjs"
13
13
 
@@ -48,7 +48,7 @@ export async function create(args) {
48
48
  let dirName = filteredArgs[0]
49
49
 
50
50
  if (dirName === "--help" || dirName === "-h") {
51
- console.log(` ${bold("Usage:")} promptslide create ${dim("<project-directory>")} ${dim("[options]")}`)
51
+ console.log(` ${bold("Usage:")} promptslide create ${dim("[project-directory]")} ${dim("[options]")}`)
52
52
  console.log()
53
53
  console.log(` Scaffolds a new PromptSlide slide deck project.`)
54
54
  console.log()
@@ -59,11 +59,47 @@ export async function create(args) {
59
59
  console.log(` ${bold("Examples:")}`)
60
60
  console.log(` promptslide create my-pitch-deck`)
61
61
  console.log(` promptslide create my-pitch-deck --yes`)
62
- console.log(` promptslide create my-deck --from promptic-pitch-deck`)
62
+ console.log(` promptslide create --from promptic-pitch-deck`)
63
63
  console.log()
64
64
  process.exit(0)
65
65
  }
66
66
 
67
+ // If --from is specified, fetch the deck info before the directory prompt
68
+ let fromItem = null
69
+ let fromAuth = null
70
+ if (fromSlug) {
71
+ fromAuth = requireAuth()
72
+
73
+ try {
74
+ fromItem = await fetchRegistryItem(fromSlug, fromAuth)
75
+ } catch (err) {
76
+ console.error(` ${red("Error:")} ${err.message}`)
77
+ closePrompts()
78
+ process.exit(1)
79
+ }
80
+
81
+ if (fromItem.type !== "deck") {
82
+ console.error(` ${red("Error:")} "${fromSlug}" is a ${fromItem.type}, not a deck. Use --from with a published deck.`)
83
+ closePrompts()
84
+ process.exit(1)
85
+ }
86
+
87
+ const versionTag = fromItem.version ? ` ${dim(`v${fromItem.version}`)}` : ""
88
+ console.log(` Using deck ${bold(fromItem.title || fromItem.name)}${versionTag} as template`)
89
+
90
+ if (fromItem.promptslideVersion) {
91
+ const pubParts = fromItem.promptslideVersion.match(/^(\d+)\.(\d+)/)
92
+ const localParts = CLI_VERSION.match(/^(\d+)\.(\d+)/)
93
+ if (pubParts && localParts && pubParts[2] !== localParts[2]) {
94
+ console.log()
95
+ console.log(` ${yellow("⚠")} This deck was published with promptslide ${bold(`v${fromItem.promptslideVersion}`)}`)
96
+ console.log(` You have ${bold(`v${CLI_VERSION}`)} installed — some slides may need updating.`)
97
+ }
98
+ }
99
+
100
+ console.log()
101
+ }
102
+
67
103
  if (!dirName) {
68
104
  if (useDefaults) {
69
105
  console.error(` ${red("Error:")} Please provide a project directory name when using --yes.`)
@@ -156,40 +192,9 @@ export async function create(args) {
156
192
 
157
193
  // 7. Overlay deck files if --from was specified
158
194
  if (fromSlug) {
159
- const auth = requireAuth()
160
-
161
- let item
162
- try {
163
- item = await fetchRegistryItem(fromSlug, auth)
164
- } catch (err) {
165
- console.error(` ${red("Error:")} ${err.message}`)
166
- closePrompts()
167
- process.exit(1)
168
- }
169
-
170
- if (item.type !== "deck") {
171
- console.error(` ${red("Error:")} "${fromSlug}" is a ${item.type}, not a deck. Use --from with a published deck.`)
172
- closePrompts()
173
- process.exit(1)
174
- }
175
-
176
- const versionTag = item.version ? ` ${dim(`v${item.version}`)}` : ""
177
- console.log(` Using deck ${bold(item.title || item.name)}${versionTag}`)
178
-
179
- // Warn if the deck was published with a different minor version
180
- if (item.promptslideVersion) {
181
- const pubParts = item.promptslideVersion.match(/^(\d+)\.(\d+)/)
182
- const localParts = CLI_VERSION.match(/^(\d+)\.(\d+)/)
183
- if (pubParts && localParts && pubParts[2] !== localParts[2]) {
184
- console.log()
185
- console.log(` ${yellow("⚠")} This deck was published with promptslide ${bold(`v${item.promptslideVersion}`)}`)
186
- console.log(` You have ${bold(`v${CLI_VERSION}`)} installed — some slides may need updating.`)
187
- }
188
- }
189
-
190
195
  let resolved
191
196
  try {
192
- resolved = await resolveRegistryDependencies(item, auth, targetDir)
197
+ resolved = await resolveRegistryDependencies(fromItem, fromAuth, targetDir)
193
198
  } catch (err) {
194
199
  console.error(` ${red("Error:")} ${err.message}`)
195
200
  closePrompts()
@@ -215,16 +220,16 @@ export async function create(args) {
215
220
  }
216
221
 
217
222
  // Reconstruct deck-config.ts from deckConfig metadata
218
- if (item.meta?.slides) {
219
- const slides = item.meta.slides.map(s => ({
220
- componentName: toPascalCase(s.slug),
223
+ if (fromItem.meta?.slides) {
224
+ const slides = fromItem.meta.slides.map(s => ({
225
+ componentName: s.componentName || toPascalCase(s.slug),
221
226
  importPath: `@/slides/${s.slug}`,
222
227
  steps: s.steps,
223
228
  section: s.section
224
229
  }))
225
230
  replaceDeckConfig(targetDir, slides, {
226
- transition: item.meta.transition,
227
- directionalTransition: item.meta.directionalTransition
231
+ transition: fromItem.meta.transition,
232
+ directionalTransition: fromItem.meta.directionalTransition
228
233
  })
229
234
  console.log(` ${green("✓")} Generated ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`)
230
235
  }
@@ -242,37 +247,8 @@ export async function create(args) {
242
247
  console.log(` ${green("✓")} Added ${dim(pkgList.join(", "))} to package.json`)
243
248
  }
244
249
 
245
- // Persist deck slug for future pull/publish in the new project
246
- updateLockfilePublishConfig(targetDir, { deckSlug: fromSlug })
247
-
248
- // Fetch annotations from registry
249
- if (item.id) {
250
- try {
251
- const annotationsRes = await fetch(`${auth.registry}/api/items/${item.id}/annotations`, {
252
- headers: { Authorization: `Bearer ${auth.token}`, ...(auth.organizationId ? { "X-Organization-Id": auth.organizationId } : {}) }
253
- })
254
- if (annotationsRes.ok) {
255
- const data = await annotationsRes.json()
256
- const annotations = data.annotations ?? []
257
- if (annotations.length > 0) {
258
- const annotationsFile = { version: 1, annotations: annotations.map(a => ({
259
- id: a.id,
260
- slideIndex: a.slideIndex,
261
- slideTitle: a.slideTitle,
262
- target: a.target,
263
- body: a.body,
264
- createdAt: a.createdAt,
265
- status: a.status,
266
- ...(a.resolution ? { resolution: a.resolution } : {})
267
- })) }
268
- writeFileSync(join(targetDir, "annotations.json"), JSON.stringify(annotationsFile, null, 2) + "\n", "utf-8")
269
- console.log(` ${green("✓")} Pulled ${cyan("annotations.json")} ${dim(`(${annotations.length} annotation${annotations.length === 1 ? "" : "s"})`)}`)
270
- }
271
- }
272
- } catch {
273
- // Annotations are non-critical; don't fail the create
274
- }
275
- }
250
+ // New deck identity deckSlug stays as dirName (set in initial lockfile scaffold)
251
+ console.log(` ${dim("Deck slug set to")} ${bold(dirName)} ${dim("(publish will create a new deck)")}`)
276
252
 
277
253
  console.log()
278
254
  }
@@ -71,7 +71,7 @@ function detectSteps(content) {
71
71
  return max
72
72
  }
73
73
 
74
- function detectNpmDeps(content) {
74
+ function detectNpmDeps(content, projectDeps = {}) {
75
75
  const deps = {}
76
76
  // Match both unscoped (foo) and scoped (@scope/foo) packages, exclude relative imports
77
77
  const importRegex = /import\s+.*?\s+from\s+["']((?:@[a-zA-Z0-9-]+\/)?[a-zA-Z0-9-][^"']*)["']/g
@@ -81,7 +81,7 @@ function detectNpmDeps(content) {
81
81
  const pkg = full.startsWith("@") ? full.split("/").slice(0, 2).join("/") : full.split("/")[0]
82
82
  // Skip relative imports, react, and internal packages
83
83
  if (pkg.startsWith(".") || pkg === "react" || pkg === "react-dom" || pkg.startsWith("@promptslide/")) continue
84
- deps[pkg] = "latest"
84
+ deps[pkg] = projectDeps[pkg] ?? "latest"
85
85
  }
86
86
  return deps
87
87
  }
@@ -96,6 +96,15 @@ function detectRegistryDeps(content) {
96
96
  return deps
97
97
  }
98
98
 
99
+ function readProjectDeps(cwd) {
100
+ const pkgPath = join(cwd, "package.json")
101
+ if (!existsSync(pkgPath)) return {}
102
+ try {
103
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"))
104
+ return { ...pkg.devDependencies, ...pkg.dependencies }
105
+ } catch { return {} }
106
+ }
107
+
99
108
  /** Detect sibling layout imports — both relative ("./foo") and absolute ("@/layouts/foo"). */
100
109
  function detectLayoutSiblingDeps(content) {
101
110
  const deps = []
@@ -321,7 +330,7 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
321
330
 
322
331
  const type = typeOverride || detectType(filePath) || "slide"
323
332
  const steps = detectSteps(content)
324
- const npmDeps = detectNpmDeps(content)
333
+ const npmDeps = detectNpmDeps(content, readProjectDeps(cwd))
325
334
  const registryDeps = detectRegistryDeps(content)
326
335
  const target = type === "layout" ? "src/layouts/" : "src/slides/"
327
336
 
@@ -396,6 +405,7 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
396
405
 
397
406
  export async function publish(args) {
398
407
  const cwd = process.cwd()
408
+ const projectDeps = readProjectDeps(cwd)
399
409
 
400
410
  console.log()
401
411
  console.log(` ${bold("promptslide")} ${dim("publish")}`)
@@ -692,7 +702,7 @@ export async function publish(args) {
692
702
  skipped++
693
703
  } else {
694
704
  const assetDeps = detectAssetDepsInContent(themeContent, deckSlug, publicFileSet)
695
- const npmDeps = detectNpmDeps(themeContent)
705
+ const npmDeps = detectNpmDeps(themeContent, projectDeps)
696
706
 
697
707
  try {
698
708
  const result = await publishToRegistry({
@@ -730,7 +740,7 @@ export async function publish(args) {
730
740
  }
731
741
 
732
742
  const assetDeps = detectAssetDepsInContent(content, deckSlug, publicFileSet)
733
- const npmDeps = detectNpmDeps(content)
743
+ const npmDeps = detectNpmDeps(content, projectDeps)
734
744
  const siblingDeps = detectLayoutSiblingDeps(content).map(d => `${deckSlug}/${d}`)
735
745
  const regDeps = hasTheme ? [`${deckSlug}/theme`] : []
736
746
  regDeps.push(...siblingDeps, ...assetDeps)
@@ -775,7 +785,7 @@ export async function publish(args) {
775
785
 
776
786
  const assetDeps = detectAssetDepsInContent(content, deckSlug, publicFileSet)
777
787
  const layoutDeps = detectRegistryDeps(content).map(d => `${deckSlug}/${d}`)
778
- const npmDeps = detectNpmDeps(content)
788
+ const npmDeps = detectNpmDeps(content, projectDeps)
779
789
  const steps = detectSteps(content)
780
790
  const slideConfig = deckConfig.slides.find(s => s.slug === slideName)
781
791
  const section = slideConfig?.section || undefined
@@ -836,7 +846,7 @@ export async function publish(args) {
836
846
  // Collect npm deps from shared source files
837
847
  const sharedNpmDeps = {}
838
848
  for (const f of sharedFiles) {
839
- Object.assign(sharedNpmDeps, detectNpmDeps(f.content))
849
+ Object.assign(sharedNpmDeps, detectNpmDeps(f.content, projectDeps))
840
850
  }
841
851
 
842
852
  let deckItemId = null
@@ -905,7 +915,7 @@ export async function publish(args) {
905
915
  // Detect metadata
906
916
  const type = typeOverride || detectType(filePath) || "slide"
907
917
  const steps = detectSteps(content)
908
- const npmDeps = detectNpmDeps(content)
918
+ const npmDeps = detectNpmDeps(content, projectDeps)
909
919
  const registryDeps = detectRegistryDeps(content)
910
920
  const target = type === "layout" ? "src/layouts/" : "src/slides/"
911
921
 
package/src/index.mjs CHANGED
@@ -20,6 +20,7 @@ function printHelp() {
20
20
  console.log()
21
21
  console.log(` ${bold("Commands:")}`)
22
22
  console.log(` create ${dim("<dir>")} Scaffold a new slide deck project`)
23
+ console.log(` clone ${dim("<slug>")} Clone a published deck to work on it`)
23
24
  console.log(` studio Start the development studio`)
24
25
  console.log(` build Build for production`)
25
26
  console.log(` preview Preview the production build`)
@@ -55,6 +56,11 @@ switch (command) {
55
56
  await create(args)
56
57
  break
57
58
  }
59
+ case "clone": {
60
+ const { clone } = await import("./commands/clone.mjs")
61
+ await clone(args)
62
+ break
63
+ }
58
64
  case "studio": {
59
65
  const { studio } = await import("./commands/studio.mjs")
60
66
  await studio(args)
@@ -190,36 +190,53 @@ export async function fetchOrganizations(auth) {
190
190
  * @param {{ registry: string, apiKey: string }} auth - Auth credentials
191
191
  * @returns {Promise<object>} Registry item JSON
192
192
  */
193
- export async function fetchRegistryItem(nameOrUrl, auth) {
193
+ export async function fetchRegistryItem(nameOrUrl, auth, { retries = 2 } = {}) {
194
194
  const url = nameOrUrl.startsWith("http")
195
195
  ? nameOrUrl
196
196
  : `${auth.registry}/api/r/${nameOrUrl}.json`
197
197
 
198
- const res = await fetch(url, {
199
- headers: authHeaders(auth)
200
- })
198
+ let lastError
199
+ for (let attempt = 0; attempt <= retries; attempt++) {
200
+ try {
201
+ const res = await fetch(url, {
202
+ headers: authHeaders(auth)
203
+ })
201
204
 
202
- if (res.status === 401) {
203
- throw new Error("Authentication failed. Run `promptslide login` to re-authenticate.")
204
- }
205
- if (res.status === 403) {
206
- const body = await res.json().catch(() => ({}))
207
- if (body.status === "pending_review") {
208
- throw new Error(`Item "${nameOrUrl}" is pending review. An admin must approve it first.`)
209
- }
210
- if (body.status === "rejected") {
211
- throw new Error(`Item "${nameOrUrl}" was rejected by an admin.`)
205
+ if (res.status === 401) {
206
+ throw new Error("Authentication failed. Run `promptslide login` to re-authenticate.")
207
+ }
208
+ if (res.status === 403) {
209
+ const body = await res.json().catch(() => ({}))
210
+ if (body.status === "pending_review") {
211
+ throw new Error(`Item "${nameOrUrl}" is pending review. An admin must approve it first.`)
212
+ }
213
+ if (body.status === "rejected") {
214
+ throw new Error(`Item "${nameOrUrl}" was rejected by an admin.`)
215
+ }
216
+ throw new Error("Access denied. This item belongs to a different organization.")
217
+ }
218
+ if (res.status === 404) {
219
+ throw new Error(`Item not found: ${nameOrUrl}`)
220
+ }
221
+ if (!res.ok) {
222
+ throw new Error(`Registry error (${res.status}): ${await res.text()}`)
223
+ }
224
+
225
+ return res.json()
226
+ } catch (err) {
227
+ lastError = err
228
+ // Only retry on network errors (fetch failed), not on HTTP-level errors
229
+ if (err.message.includes("Authentication") || err.message.includes("Access denied") ||
230
+ err.message.includes("not found") || err.message.includes("pending review") ||
231
+ err.message.includes("rejected") || err.message.includes("Registry error")) {
232
+ throw err
233
+ }
234
+ if (attempt < retries) {
235
+ await new Promise(r => setTimeout(r, 500 * (attempt + 1)))
236
+ }
212
237
  }
213
- throw new Error("Access denied. This item belongs to a different organization.")
214
- }
215
- if (res.status === 404) {
216
- throw new Error(`Item not found: ${nameOrUrl}`)
217
238
  }
218
- if (!res.ok) {
219
- throw new Error(`Registry error (${res.status}): ${await res.text()}`)
220
- }
221
-
222
- return res.json()
239
+ throw lastError
223
240
  }
224
241
 
225
242
  /**