promptslide 0.3.11 → 0.3.12

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.12](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.11...promptslide-v0.3.12) (2026-03-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * add clone command, make create --from always template ([#87](https://github.com/prompticeu/promptslide/issues/87)) ([769fa7e](https://github.com/prompticeu/promptslide/commit/769fa7ed0ee77f7473f49f2eb608ca996142f3fd))
9
+
3
10
  ## [0.3.11](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.10...promptslide-v0.3.11) (2026-03-25)
4
11
 
5
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptslide",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "CLI and slide engine for PromptSlide presentations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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: 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 => ({
223
+ if (fromItem.meta?.slides) {
224
+ const slides = fromItem.meta.slides.map(s => ({
220
225
  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
  }
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)