promptslide 0.2.0 → 0.2.2

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.
@@ -0,0 +1,218 @@
1
+ import { existsSync, writeFileSync, mkdirSync } from "node:fs"
2
+ import { join, dirname, resolve, sep } from "node:path"
3
+
4
+ import { bold, green, cyan, red, dim, yellow } from "../utils/ansi.mjs"
5
+ import { requireAuth } from "../utils/auth.mjs"
6
+ import { fetchRegistryItem, readLockfile, updateLockfileItem, removeLockfileItem, isFileDirty, hashContent } from "../utils/registry.mjs"
7
+ import { confirm, closePrompts } from "../utils/prompts.mjs"
8
+
9
+ export async function update(args) {
10
+ const cwd = process.cwd()
11
+
12
+ console.log()
13
+ console.log(` ${bold("promptslide")} ${dim("update")}`)
14
+ console.log()
15
+
16
+ if (args[0] === "--help" || args[0] === "-h") {
17
+ console.log(` ${bold("Usage:")} promptslide update ${dim("[name] [--all]")}`)
18
+ console.log()
19
+ console.log(` Check for and apply updates to installed registry items.`)
20
+ console.log()
21
+ console.log(` ${bold("Examples:")}`)
22
+ console.log(` promptslide update ${dim("Check for available updates")}`)
23
+ console.log(` promptslide update slide-hero ${dim("Update a specific item")}`)
24
+ console.log(` promptslide update --all ${dim("Update all outdated items")}`)
25
+ console.log()
26
+ process.exit(0)
27
+ }
28
+
29
+ const auth = requireAuth()
30
+ const lock = readLockfile(cwd)
31
+ const slugs = Object.keys(lock.items)
32
+
33
+ if (slugs.length === 0) {
34
+ console.log(` ${dim("No installed items found.")}`)
35
+ console.log(` ${dim("Install items with")} ${cyan("promptslide add <name>")} ${dim("first.")}`)
36
+ console.log()
37
+ process.exit(0)
38
+ }
39
+
40
+ const updateAll = args.includes("--all")
41
+ const targetSlug = args.find(a => !a.startsWith("--"))
42
+
43
+ // Fetch latest versions for all installed items
44
+ console.log(` ${dim("Checking")} ${slugs.length} ${dim("installed item(s)...")}`)
45
+ console.log()
46
+
47
+ const updates = []
48
+
49
+ for (const slug of slugs) {
50
+ const installed = lock.items[slug]
51
+ try {
52
+ const latest = await fetchRegistryItem(slug, auth)
53
+ const latestVersion = latest.version ?? 0
54
+ // Version 0 means "unversioned at install time" — always outdated if registry has a real version
55
+ const outdated = installed.version === 0
56
+ ? latestVersion >= 1
57
+ : latestVersion > installed.version
58
+
59
+ updates.push({
60
+ slug,
61
+ installed: installed.version,
62
+ latest: latestVersion,
63
+ outdated,
64
+ item: outdated ? latest : null,
65
+ storedFiles: installed.files
66
+ })
67
+ } catch (err) {
68
+ const notFound = err.message?.includes("not found") || err.message?.includes("Not found")
69
+ updates.push({
70
+ slug,
71
+ installed: installed.version,
72
+ latest: "?",
73
+ outdated: false,
74
+ item: null,
75
+ error: true,
76
+ notFound
77
+ })
78
+ }
79
+ }
80
+
81
+ // Print table
82
+ const nameWidth = Math.max(6, ...updates.map(u => u.slug.length)) + 2
83
+
84
+ console.log(` ${bold("Name".padEnd(nameWidth))} ${bold("Installed")} ${bold("Latest")} ${bold("Status")}`)
85
+ console.log(` ${"─".repeat(nameWidth)} ${"─".repeat(9)} ${"─".repeat(6)} ${"─".repeat(10)}`)
86
+
87
+ for (const u of updates) {
88
+ const name = u.slug.padEnd(nameWidth)
89
+ const inst = `v${u.installed}`.padEnd(9)
90
+ const lat = (typeof u.latest === "number" ? `v${u.latest}` : u.latest).padEnd(6)
91
+ const status = u.error
92
+ ? u.notFound ? yellow("removed from registry") : red("error")
93
+ : u.outdated
94
+ ? cyan("update available")
95
+ : green("up to date")
96
+ console.log(` ${name} ${inst} ${lat} ${status}`)
97
+ }
98
+
99
+ console.log()
100
+
101
+ // Offer to clean up stale lockfile entries (items deleted from registry)
102
+ const stale = updates.filter(u => u.notFound)
103
+ if (stale.length > 0) {
104
+ console.log(` ${yellow("⚠")} ${bold(`${stale.length}`)} item(s) no longer exist in the registry:`)
105
+ for (const u of stale) {
106
+ console.log(` ${dim("•")} ${u.slug}`)
107
+ }
108
+ console.log()
109
+ const cleanup = await confirm(` Remove stale entries from lockfile? ${dim("(local files are kept)")}`, true)
110
+ if (cleanup) {
111
+ for (const u of stale) {
112
+ removeLockfileItem(cwd, u.slug)
113
+ }
114
+ console.log(` ${green("✓")} Removed ${stale.length} stale lockfile entry(s).`)
115
+ console.log()
116
+ }
117
+ }
118
+
119
+ const outdated = updates.filter(u => u.outdated && u.item)
120
+
121
+ if (outdated.length === 0) {
122
+ if (stale.length === 0) {
123
+ console.log(` ${green("✓")} All items are up to date.`)
124
+ }
125
+ console.log()
126
+ closePrompts()
127
+ return
128
+ }
129
+
130
+ // Determine which items to update
131
+ let toUpdate = []
132
+
133
+ if (targetSlug) {
134
+ const match = outdated.find(u => u.slug === targetSlug)
135
+ if (!match) {
136
+ const exists = updates.find(u => u.slug === targetSlug)
137
+ if (exists && !exists.outdated) {
138
+ console.log(` ${green("✓")} ${bold(targetSlug)} is already up to date.`)
139
+ } else if (!exists) {
140
+ console.log(` ${red("Error:")} ${bold(targetSlug)} is not installed.`)
141
+ }
142
+ console.log()
143
+ closePrompts()
144
+ return
145
+ }
146
+ toUpdate = [match]
147
+ } else if (updateAll) {
148
+ toUpdate = outdated
149
+ } else {
150
+ console.log(` ${bold(`${outdated.length}`)} update(s) available.`)
151
+ console.log(` Run ${cyan("promptslide update --all")} or ${cyan("promptslide update <name>")} to apply.`)
152
+ console.log()
153
+ closePrompts()
154
+ return
155
+ }
156
+
157
+ // Apply updates
158
+ for (const u of toUpdate) {
159
+ const item = u.item
160
+ console.log(` Updating ${bold(u.slug)} ${dim(`v${u.installed} → v${u.latest}`)}...`)
161
+
162
+ if (!item.files?.length) {
163
+ console.log(` ${dim("No files to write, skipping.")}`)
164
+ continue
165
+ }
166
+
167
+ const fileHashes = {}
168
+
169
+ for (const file of item.files) {
170
+ const targetPath = resolve(cwd, file.target, file.path)
171
+ if (!targetPath.startsWith(cwd + sep)) {
172
+ console.log(` ${red("Error:")} Invalid file path: ${file.target}${file.path}`)
173
+ continue
174
+ }
175
+ const targetDir = dirname(targetPath)
176
+ const relativePath = file.target + file.path
177
+ const newHash = hashContent(file.content)
178
+
179
+ // Check for local modifications (only if file exists on disk)
180
+ if (!existsSync(targetPath)) {
181
+ // File was deleted locally — restore it without prompting
182
+ mkdirSync(targetDir, { recursive: true })
183
+ writeFileSync(targetPath, file.content, "utf-8")
184
+ fileHashes[relativePath] = newHash
185
+ console.log(` ${green("✓")} Restored ${cyan(relativePath)}`)
186
+ continue
187
+ }
188
+
189
+ if (u.storedFiles[relativePath]) {
190
+ const dirty = isFileDirty(cwd, relativePath, u.storedFiles[relativePath])
191
+ if (dirty) {
192
+ const overwrite = await confirm(
193
+ ` ${yellow("⚠")} ${relativePath} has local changes. Overwrite?`,
194
+ false
195
+ )
196
+ if (!overwrite) {
197
+ // Carry forward old hash so lockfile stays accurate
198
+ fileHashes[relativePath] = u.storedFiles[relativePath]
199
+ console.log(` ${dim("Skipped")} ${relativePath}`)
200
+ continue
201
+ }
202
+ }
203
+ }
204
+
205
+ mkdirSync(targetDir, { recursive: true })
206
+ writeFileSync(targetPath, file.content, "utf-8")
207
+ fileHashes[relativePath] = newHash
208
+ console.log(` ${green("✓")} Updated ${cyan(relativePath)}`)
209
+ }
210
+
211
+ updateLockfileItem(cwd, u.slug, u.latest, fileHashes)
212
+ }
213
+
214
+ console.log()
215
+ console.log(` ${green("✓")} ${toUpdate.length} item(s) updated.`)
216
+ console.log()
217
+ closePrompts()
218
+ }
@@ -24,11 +24,61 @@ interface SlideDeckProps {
24
24
  directionalTransition?: boolean
25
25
  }
26
26
 
27
+ // =============================================================================
28
+ // EXPORT VIEW (for Playwright screenshot capture)
29
+ // =============================================================================
30
+
31
+ function SlideExportView({ slides, slideIndex }: { slides: SlideConfig[]; slideIndex: number }) {
32
+ const [ready, setReady] = useState(false)
33
+ const clampedIndex = Math.max(0, Math.min(slideIndex, slides.length - 1))
34
+ const slideConfig = slides[clampedIndex]!
35
+ const SlideComponent = slideConfig.component
36
+
37
+ useEffect(() => {
38
+ setReady(true)
39
+ }, [])
40
+
41
+ return (
42
+ <div
43
+ data-export-ready={ready ? "true" : undefined}
44
+ style={{
45
+ width: SLIDE_DIMENSIONS.width,
46
+ height: SLIDE_DIMENSIONS.height,
47
+ overflow: "hidden",
48
+ position: "relative",
49
+ background: "black"
50
+ }}
51
+ >
52
+ <AnimationProvider
53
+ currentStep={slideConfig.steps}
54
+ totalSteps={slideConfig.steps}
55
+ showAllAnimations={true}
56
+ >
57
+ <SlideErrorBoundary slideIndex={clampedIndex} slideTitle={slideConfig.title}>
58
+ <SlideComponent slideNumber={clampedIndex + 1} totalSlides={slides.length} />
59
+ </SlideErrorBoundary>
60
+ </AnimationProvider>
61
+ </div>
62
+ )
63
+ }
64
+
27
65
  // =============================================================================
28
66
  // COMPONENT
29
67
  // =============================================================================
30
68
 
31
69
  export function SlideDeck({ slides, transition, directionalTransition }: SlideDeckProps) {
70
+ // Check for export mode via URL params
71
+ const [exportParams] = useState(() => {
72
+ if (typeof window === "undefined") return null
73
+ const params = new URLSearchParams(window.location.search)
74
+ if (params.get("export") !== "true") return null
75
+ return { slideIndex: parseInt(params.get("slide") || "0", 10) }
76
+ })
77
+
78
+ if (exportParams) {
79
+ return <SlideExportView slides={slides} slideIndex={exportParams.slideIndex} />
80
+ }
81
+
32
82
  const [viewMode, setViewMode] = useState<ViewMode>("slide")
33
83
  const [isPresentationMode, setIsPresentationMode] = useState(false)
34
84
  const [scale, setScale] = useState(1)
package/src/index.mjs CHANGED
@@ -24,6 +24,21 @@ function printHelp() {
24
24
  console.log(` build Build for production`)
25
25
  console.log(` preview Preview the production build`)
26
26
  console.log()
27
+ console.log(` ${bold("Registry:")}`)
28
+ console.log(` login Authenticate with the slide registry`)
29
+ console.log(` logout Clear stored credentials`)
30
+ console.log(` org List and switch organizations`)
31
+ console.log(` add ${dim("<name>")} Install a slide/deck from the registry`)
32
+ console.log(` publish ${dim("[file]")} Publish a slide to the registry`)
33
+ console.log(` update ${dim("[name]")} Check for and apply updates`)
34
+ console.log(` remove ${dim("<name>")} Remove an installed item`)
35
+ console.log(` info ${dim("<name>")} Show details about a registry item`)
36
+ console.log(` search ${dim("<query>")} Search the registry`)
37
+ console.log(` list ${dim("[--type]")} List registry items`)
38
+ console.log()
39
+ console.log(` ${bold("Tools:")}`)
40
+ console.log(` to-image ${dim("<slide>")} Export a slide as a PNG image`)
41
+ console.log()
27
42
  console.log(` ${bold("Options:")}`)
28
43
  console.log(` --help, -h Show this help message`)
29
44
  console.log(` --version, -v Show version number`)
@@ -54,6 +69,61 @@ switch (command) {
54
69
  await preview(args)
55
70
  break
56
71
  }
72
+ case "login": {
73
+ const { login } = await import("./commands/login.mjs")
74
+ await login(args)
75
+ break
76
+ }
77
+ case "logout": {
78
+ const { logout } = await import("./commands/logout.mjs")
79
+ await logout(args)
80
+ break
81
+ }
82
+ case "org": {
83
+ const { org } = await import("./commands/org.mjs")
84
+ await org(args)
85
+ break
86
+ }
87
+ case "add": {
88
+ const { add } = await import("./commands/add.mjs")
89
+ await add(args)
90
+ break
91
+ }
92
+ case "publish": {
93
+ const { publish } = await import("./commands/publish.mjs")
94
+ await publish(args)
95
+ break
96
+ }
97
+ case "update": {
98
+ const { update } = await import("./commands/update.mjs")
99
+ await update(args)
100
+ break
101
+ }
102
+ case "search": {
103
+ const { search } = await import("./commands/search.mjs")
104
+ await search(args)
105
+ break
106
+ }
107
+ case "list": {
108
+ const { list } = await import("./commands/list.mjs")
109
+ await list(args)
110
+ break
111
+ }
112
+ case "remove": {
113
+ const { remove } = await import("./commands/remove.mjs")
114
+ await remove(args)
115
+ break
116
+ }
117
+ case "info": {
118
+ const { info } = await import("./commands/info.mjs")
119
+ await info(args)
120
+ break
121
+ }
122
+ case "to-image": {
123
+ const { toImage } = await import("./commands/to-image.mjs")
124
+ await toImage(args)
125
+ break
126
+ }
57
127
  case "--help":
58
128
  case "-h":
59
129
  case undefined:
@@ -1,5 +1,6 @@
1
1
  export const bold = s => `\x1b[1m${s}\x1b[0m`
2
2
  export const green = s => `\x1b[32m${s}\x1b[0m`
3
3
  export const cyan = s => `\x1b[36m${s}\x1b[0m`
4
+ export const yellow = s => `\x1b[33m${s}\x1b[0m`
4
5
  export const red = s => `\x1b[31m${s}\x1b[0m`
5
6
  export const dim = s => `\x1b[2m${s}\x1b[0m`
@@ -0,0 +1,66 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { homedir } from "node:os"
4
+
5
+ const AUTH_DIR = join(homedir(), ".promptslide")
6
+ const AUTH_FILE = join(AUTH_DIR, "auth.json")
7
+
8
+ const DEFAULT_REGISTRY = "https://registry.promptslide.dev"
9
+
10
+ /**
11
+ * Load stored auth credentials.
12
+ * Returns null if no auth file exists.
13
+ */
14
+ export function loadAuth() {
15
+ if (!existsSync(AUTH_FILE)) return null
16
+ try {
17
+ const data = JSON.parse(readFileSync(AUTH_FILE, "utf-8"))
18
+ if (!data.token || !data.registry) return null
19
+ return {
20
+ registry: data.registry || DEFAULT_REGISTRY,
21
+ token: data.token,
22
+ organizationId: data.organizationId || null,
23
+ organizationName: data.organizationName || null
24
+ }
25
+ } catch {
26
+ return null
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Save auth credentials to ~/.promptslide/auth.json.
32
+ */
33
+ export function saveAuth({ registry, token, organizationId, organizationName }) {
34
+ mkdirSync(AUTH_DIR, { recursive: true })
35
+ const data = {
36
+ registry: registry || DEFAULT_REGISTRY,
37
+ token,
38
+ organizationId: organizationId || null,
39
+ organizationName: organizationName || null
40
+ }
41
+ writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 })
42
+ }
43
+
44
+ /**
45
+ * Clear stored auth credentials.
46
+ * Returns true if file was removed, false if it didn't exist.
47
+ */
48
+ export function clearAuth() {
49
+ if (!existsSync(AUTH_FILE)) return false
50
+ unlinkSync(AUTH_FILE)
51
+ return true
52
+ }
53
+
54
+ /**
55
+ * Load auth or exit with an error message.
56
+ */
57
+ export function requireAuth() {
58
+ const auth = loadAuth()
59
+ if (!auth) {
60
+ console.error(" Not authenticated. Run `promptslide login` first.")
61
+ process.exit(1)
62
+ }
63
+ return auth
64
+ }
65
+
66
+ export { DEFAULT_REGISTRY, AUTH_FILE }
@@ -32,13 +32,6 @@ export function hexToOklch(hex) {
32
32
  return `oklch(${round(L)} ${round(C)} ${round(H)})`
33
33
  }
34
34
 
35
- export function hexToOklchDark(hex) {
36
- const oklch = hexToOklch(hex)
37
- const match = oklch.match(/oklch\(([\d.]+) ([\d.]+) ([\d.]+)\)/)
38
- const L = Math.min(1, parseFloat(match[1]) + 0.05)
39
- return `oklch(${+L.toFixed(3)} ${match[2]} ${match[3]})`
40
- }
41
-
42
35
  export function isValidHex(hex) {
43
36
  return /^#?[0-9a-fA-F]{6}$/.test(hex)
44
37
  }
@@ -0,0 +1,208 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs"
2
+ import { join } from "node:path"
3
+
4
+ const DECK_CONFIG_PATH = "src/deck-config.ts"
5
+
6
+ /**
7
+ * Convert a kebab-case filename to PascalCase component name.
8
+ * e.g. "slide-hero-gradient" → "SlideHeroGradient"
9
+ */
10
+ export function toPascalCase(kebab) {
11
+ return kebab
12
+ .replace(/\.tsx?$/, "")
13
+ .split("-")
14
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
15
+ .join("")
16
+ }
17
+
18
+ /**
19
+ * Derive import path from file target and path.
20
+ * e.g. target="src/slides/", path="slide-hero.tsx" → "@/slides/slide-hero"
21
+ */
22
+ export function deriveImportPath(target, filePath) {
23
+ const dir = target.replace(/^src\//, "@/").replace(/\/$/, "")
24
+ const name = filePath.replace(/\.tsx?$/, "")
25
+ return `${dir}/${name}`
26
+ }
27
+
28
+ /**
29
+ * Add a single slide to deck-config.ts.
30
+ * Uses string manipulation to preserve existing content.
31
+ *
32
+ * @param {string} cwd - Project root directory
33
+ * @param {{ componentName: string, importPath: string, steps: number }} opts
34
+ */
35
+ export function addSlideToDeckConfig(cwd, { componentName, importPath, steps }) {
36
+ const configPath = join(cwd, DECK_CONFIG_PATH)
37
+ if (!existsSync(configPath)) {
38
+ console.warn(` Warning: ${DECK_CONFIG_PATH} not found. Skipping deck-config update.`)
39
+ return false
40
+ }
41
+
42
+ let content = readFileSync(configPath, "utf-8")
43
+
44
+ // Check if this component is already imported (handles spacing variations)
45
+ if (new RegExp(`import\\s*\\{\\s*${componentName}\\s*\\}`).test(content)) {
46
+ return false // Already exists
47
+ }
48
+
49
+ // Build import and slide entry strings
50
+ const importLine = `import { ${componentName} } from "${importPath}"`
51
+ const slideEntry = ` { component: ${componentName}, steps: ${steps} },`
52
+
53
+ // Find insertion point for import:
54
+ // Look for the last import from "@/slides/" or "@/layouts/"
55
+ const importLines = content.split("\n")
56
+ let lastSlideImportIdx = -1
57
+ let lastAnyImportIdx = -1
58
+
59
+ for (let i = 0; i < importLines.length; i++) {
60
+ const line = importLines[i]
61
+ if (/^import\s+/.test(line)) {
62
+ lastAnyImportIdx = i
63
+ if (line.includes('"@/slides/') || line.includes('"@/layouts/')) {
64
+ lastSlideImportIdx = i
65
+ }
66
+ }
67
+ }
68
+
69
+ // Insert import after last slide import, or after last import, or at top
70
+ const importInsertIdx = lastSlideImportIdx >= 0
71
+ ? lastSlideImportIdx + 1
72
+ : lastAnyImportIdx >= 0
73
+ ? lastAnyImportIdx + 1
74
+ : 0
75
+
76
+ importLines.splice(importInsertIdx, 0, importLine)
77
+ content = importLines.join("\n")
78
+
79
+ // Find insertion point for slide entry in the slides array.
80
+ // Look for the closing bracket of the slides array.
81
+ // Strategy: find `]` that follows the last `}` or `,` in the slides array.
82
+ const slidesArrayMatch = content.match(/export\s+const\s+slides\s*:\s*SlideConfig\[\]\s*=\s*\[/)
83
+ if (!slidesArrayMatch) {
84
+ console.warn(` Warning: Could not find slides array in ${DECK_CONFIG_PATH}. Skipping.`)
85
+ return false
86
+ }
87
+
88
+ const arrayStartIdx = content.indexOf(slidesArrayMatch[0]) + slidesArrayMatch[0].length
89
+ // Find the matching closing bracket
90
+ let bracketDepth = 1
91
+ let arrayEndIdx = arrayStartIdx
92
+ for (let i = arrayStartIdx; i < content.length; i++) {
93
+ if (content[i] === "[") bracketDepth++
94
+ if (content[i] === "]") bracketDepth--
95
+ if (bracketDepth === 0) {
96
+ arrayEndIdx = i
97
+ break
98
+ }
99
+ }
100
+
101
+ // Insert the slide entry before the closing bracket
102
+ const beforeClose = content.slice(0, arrayEndIdx)
103
+ const afterClose = content.slice(arrayEndIdx)
104
+
105
+ // Check if the array is empty or has existing entries
106
+ const arrayContent = content.slice(arrayStartIdx, arrayEndIdx).trim()
107
+ if (arrayContent.length === 0) {
108
+ // Empty array — insert with newline
109
+ content = beforeClose + "\n" + slideEntry + "\n" + afterClose
110
+ } else {
111
+ // Has entries — ensure trailing newline + add entry
112
+ const trimmedBefore = beforeClose.trimEnd()
113
+ const needsComma = !trimmedBefore.endsWith(",")
114
+ content = trimmedBefore + (needsComma ? "," : "") + "\n" + slideEntry + "\n" + afterClose
115
+ }
116
+
117
+ writeFileSync(configPath, content, "utf-8")
118
+ return true
119
+ }
120
+
121
+ /**
122
+ * Remove a slide from deck-config.ts by component name.
123
+ * Removes the import line and the slides array entry.
124
+ *
125
+ * @param {string} cwd - Project root directory
126
+ * @param {string} componentName - PascalCase component name (e.g. "SlideHeroGradient")
127
+ * @returns {boolean} Whether any changes were made
128
+ */
129
+ export function removeSlideFromDeckConfig(cwd, componentName) {
130
+ const configPath = join(cwd, DECK_CONFIG_PATH)
131
+ if (!existsSync(configPath)) return false
132
+
133
+ let content = readFileSync(configPath, "utf-8")
134
+ let changed = false
135
+
136
+ // Remove import line
137
+ const importRegex = new RegExp(
138
+ `^import\\s*\\{\\s*${componentName}\\s*\\}\\s*from\\s*["'][^"']+["']\\s*;?\\s*\\n?`,
139
+ "m"
140
+ )
141
+ if (importRegex.test(content)) {
142
+ content = content.replace(importRegex, "")
143
+ changed = true
144
+ }
145
+
146
+ // Remove slide entry from array
147
+ const entryRegex = new RegExp(
148
+ `^\\s*\\{[^}]*component:\\s*${componentName}[^}]*\\},?\\s*\\n?`,
149
+ "m"
150
+ )
151
+ if (entryRegex.test(content)) {
152
+ content = content.replace(entryRegex, "")
153
+ changed = true
154
+ }
155
+
156
+ if (changed) {
157
+ // Clean up triple+ blank lines
158
+ content = content.replace(/\n{3,}/g, "\n\n")
159
+ writeFileSync(configPath, content, "utf-8")
160
+ }
161
+
162
+ return changed
163
+ }
164
+
165
+ /**
166
+ * Replace the entire deck-config.ts with a new slide manifest (for deck type).
167
+ *
168
+ * @param {string} cwd - Project root directory
169
+ * @param {{ componentName: string, importPath: string, steps: number, section?: string }[]} slides
170
+ * @param {{ transition?: string, directionalTransition?: boolean }} opts
171
+ */
172
+ export function replaceDeckConfig(cwd, slides, opts = {}) {
173
+ const configPath = join(cwd, DECK_CONFIG_PATH)
174
+
175
+ const imports = [
176
+ `import type { SlideConfig } from "promptslide"`,
177
+ ...slides.map(s => `import { ${s.componentName} } from "${s.importPath}"`)
178
+ ].join("\n")
179
+
180
+ const slideEntries = slides
181
+ .map(s => {
182
+ const parts = [`component: ${s.componentName}`, `steps: ${s.steps}`]
183
+ if (s.section) parts.push(`section: "${s.section}"`)
184
+ return ` { ${parts.join(", ")} },`
185
+ })
186
+ .join("\n")
187
+
188
+ const configLines = []
189
+ if (opts.transition) {
190
+ configLines.push(`export const transition = "${opts.transition}"`)
191
+ }
192
+ if (opts.directionalTransition !== undefined) {
193
+ configLines.push(`export const directionalTransition = ${opts.directionalTransition}`)
194
+ }
195
+ const configBlock = configLines.length > 0 ? "\n" + configLines.join("\n") + "\n" : ""
196
+
197
+ const content = [
198
+ imports,
199
+ configBlock,
200
+ "export const slides: SlideConfig[] = [",
201
+ slideEntries,
202
+ "]",
203
+ ""
204
+ ].join("\n")
205
+
206
+ writeFileSync(configPath, content, "utf-8")
207
+ return true
208
+ }