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 +14 -0
- package/package.json +1 -1
- package/src/commands/add.mjs +2 -2
- package/src/commands/clone.mjs +231 -0
- package/src/commands/create.mjs +47 -71
- package/src/commands/publish.mjs +18 -8
- package/src/index.mjs +6 -0
- package/src/utils/registry.mjs +40 -23
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
package/src/commands/add.mjs
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/create.mjs
CHANGED
|
@@ -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,
|
|
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("
|
|
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
|
|
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(
|
|
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 (
|
|
219
|
-
const slides =
|
|
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:
|
|
227
|
-
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
|
-
//
|
|
246
|
-
|
|
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/commands/publish.mjs
CHANGED
|
@@ -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)
|
package/src/utils/registry.mjs
CHANGED
|
@@ -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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
/**
|