promptslide 0.3.15 → 0.3.16
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 +7 -0
- package/package.json +2 -1
- package/src/commands/add.mjs +9 -2
- package/src/commands/clone.mjs +8 -1
- package/src/commands/create.mjs +8 -1
- package/src/commands/publish.mjs +34 -8
- package/src/commands/pull.mjs +9 -2
- package/src/utils/deck-sync.mjs +78 -0
- package/src/utils/registry.mjs +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.16](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.15...promptslide-v0.3.16) (2026-08-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **cli:** guard deck sync against stale slides ([#103](https://github.com/prompticeu/promptslide/issues/103)) ([f8dddc9](https://github.com/prompticeu/promptslide/commit/f8dddc9fe4177c37dbae729d1efc8971aa46ed92))
|
|
9
|
+
|
|
3
10
|
## [0.3.15](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.14...promptslide-v0.3.15) (2026-04-09)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promptslide",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
4
4
|
"description": "CLI and slide engine for PromptSlide presentations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsup",
|
|
27
|
+
"test": "node --test test/*.test.mjs",
|
|
27
28
|
"prepublishOnly": "npm run build"
|
|
28
29
|
},
|
|
29
30
|
"license": "MIT",
|
package/src/commands/add.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
addSlideToDeckConfig,
|
|
22
22
|
replaceDeckConfig
|
|
23
23
|
} from "../utils/deck-config.mjs"
|
|
24
|
+
import { collectRegistrySlideSlugs, partitionDeckSlides } from "../utils/deck-sync.mjs"
|
|
24
25
|
import { confirm, closePrompts } from "../utils/prompts.mjs"
|
|
25
26
|
|
|
26
27
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -164,9 +165,15 @@ export async function add(args) {
|
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
} else if (item.type === "deck" && item.meta?.slides) {
|
|
168
|
+
const registrySlideSlugs = collectRegistrySlideSlugs(resolved.items)
|
|
169
|
+
const { available, missing } = partitionDeckSlides(item.meta.slides, registrySlideSlugs)
|
|
170
|
+
if (missing.length > 0) {
|
|
171
|
+
console.log(` ${yellow("⚠")} Skipped ${missing.length} slide(s) listed in the deck but not included in this version:`)
|
|
172
|
+
console.log(` ${dim(missing.map(s => s.slug).join(", "))}`)
|
|
173
|
+
}
|
|
167
174
|
const shouldReplace = await confirm(" Replace entire deck-config.ts with this deck?", true)
|
|
168
175
|
if (shouldReplace) {
|
|
169
|
-
const slides =
|
|
176
|
+
const slides = available.map(s => ({
|
|
170
177
|
componentName: s.componentName || toPascalCase(s.slug),
|
|
171
178
|
importPath: `@/slides/${s.slug}`,
|
|
172
179
|
steps: s.steps,
|
|
@@ -179,7 +186,7 @@ export async function add(args) {
|
|
|
179
186
|
console.log(` ${green("✓")} Replaced ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`)
|
|
180
187
|
} else {
|
|
181
188
|
// Append individual slides
|
|
182
|
-
for (const s of
|
|
189
|
+
for (const s of available) {
|
|
183
190
|
const componentName = s.componentName || toPascalCase(s.slug)
|
|
184
191
|
const importPath = `@/slides/${s.slug}`
|
|
185
192
|
const updated = addSlideToDeckConfig(cwd, { componentName, importPath, steps: s.steps })
|
package/src/commands/clone.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { hexToOklch } from "../utils/colors.mjs"
|
|
|
8
8
|
import { closePrompts } from "../utils/prompts.mjs"
|
|
9
9
|
import { fetchRegistryItem, resolveRegistryDependencies, writeLockfile } from "../utils/registry.mjs"
|
|
10
10
|
import { toPascalCase, replaceDeckConfig } from "../utils/deck-config.mjs"
|
|
11
|
+
import { collectRegistrySlideSlugs, partitionDeckSlides } from "../utils/deck-sync.mjs"
|
|
11
12
|
import { ensureTsConfig } from "../utils/tsconfig.mjs"
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url)
|
|
@@ -158,7 +159,13 @@ export async function clone(args) {
|
|
|
158
159
|
|
|
159
160
|
// 6. Generate deck-config.ts
|
|
160
161
|
if (item.meta?.slides) {
|
|
161
|
-
const
|
|
162
|
+
const registrySlideSlugs = collectRegistrySlideSlugs(resolved.items)
|
|
163
|
+
const { available, missing } = partitionDeckSlides(item.meta.slides, registrySlideSlugs)
|
|
164
|
+
if (missing.length > 0) {
|
|
165
|
+
console.log(` ${yellow("!")} Skipped ${missing.length} slide(s) listed in the deck but not included in this version:`)
|
|
166
|
+
console.log(` ${dim(missing.map(s => s.slug).join(", "))}`)
|
|
167
|
+
}
|
|
168
|
+
const slides = available.map(s => ({
|
|
162
169
|
componentName: s.componentName || toPascalCase(s.slug),
|
|
163
170
|
importPath: `@/slides/${s.slug}`,
|
|
164
171
|
steps: s.steps,
|
package/src/commands/create.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { hexToOklch, isValidHex } from "../utils/colors.mjs"
|
|
|
9
9
|
import { prompt, confirm, closePrompts } from "../utils/prompts.mjs"
|
|
10
10
|
import { fetchRegistryItem, resolveRegistryDependencies, writeLockfile } from "../utils/registry.mjs"
|
|
11
11
|
import { toPascalCase, replaceDeckConfig } from "../utils/deck-config.mjs"
|
|
12
|
+
import { collectRegistrySlideSlugs, partitionDeckSlides } from "../utils/deck-sync.mjs"
|
|
12
13
|
import { ensureTsConfig } from "../utils/tsconfig.mjs"
|
|
13
14
|
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url)
|
|
@@ -221,7 +222,13 @@ export async function create(args) {
|
|
|
221
222
|
|
|
222
223
|
// Reconstruct deck-config.ts from deckConfig metadata
|
|
223
224
|
if (fromItem.meta?.slides) {
|
|
224
|
-
const
|
|
225
|
+
const registrySlideSlugs = collectRegistrySlideSlugs(resolved.items)
|
|
226
|
+
const { available, missing } = partitionDeckSlides(fromItem.meta.slides, registrySlideSlugs)
|
|
227
|
+
if (missing.length > 0) {
|
|
228
|
+
console.log(` ${yellow("⚠")} Skipped ${missing.length} slide(s) listed in the deck but not included in this version:`)
|
|
229
|
+
console.log(` ${dim(missing.map(s => s.slug).join(", "))}`)
|
|
230
|
+
}
|
|
231
|
+
const slides = available.map(s => ({
|
|
225
232
|
componentName: s.componentName || toPascalCase(s.slug),
|
|
226
233
|
importPath: `@/slides/${s.slug}`,
|
|
227
234
|
steps: s.steps,
|
package/src/commands/publish.mjs
CHANGED
|
@@ -5,9 +5,10 @@ import { fileURLToPath } from "node:url"
|
|
|
5
5
|
import { bold, green, cyan, red, yellow, dim } from "../utils/ansi.mjs"
|
|
6
6
|
import { requireAuth } from "../utils/auth.mjs"
|
|
7
7
|
import { captureSlideAsDataUri, isPlaywrightAvailable, createCaptureSession } from "../utils/export.mjs"
|
|
8
|
-
import { publishToRegistry, registryItemExists, fetchRegistryItem, searchRegistry, updateLockfileItem, updateLockfilePublishConfig, readLockfile, writeLockfile, hashContent, detectPackageManager, requestUploadTokens, uploadBinaryToBlob, assetFileToSlug, detectAssetDepsInContent, readDeckMeta, updateDeckMeta, readItemMeta, updateItemMeta } from "../utils/registry.mjs"
|
|
8
|
+
import { publishToRegistry, registryItemExists, fetchRegistryItem, searchRegistry, updateLockfileItem, updateLockfilePublishConfig, readLockfile, writeLockfile, hashContent, detectPackageManager, requestUploadTokens, uploadBinaryToBlob, assetFileToSlug, detectAssetDepsInContent, readDeckMeta, updateDeckMeta, readItemMeta, updateItemMeta, pruneMissingLockfileItems } from "../utils/registry.mjs"
|
|
9
9
|
import { prompt, confirm, select, closePrompts } from "../utils/prompts.mjs"
|
|
10
10
|
import { parseDeckConfig } from "../utils/deck-config.mjs"
|
|
11
|
+
import { selectSlidesForPublish } from "../utils/deck-sync.mjs"
|
|
11
12
|
|
|
12
13
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
13
14
|
const CLI_VERSION = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8")).version
|
|
@@ -412,7 +413,7 @@ export async function publish(args) {
|
|
|
412
413
|
console.log()
|
|
413
414
|
|
|
414
415
|
if (args[0] === "--help" || args[0] === "-h") {
|
|
415
|
-
console.log(` ${bold("Usage:")} promptslide publish ${dim("[file] [--type slide|layout|deck|theme]")}`)
|
|
416
|
+
console.log(` ${bold("Usage:")} promptslide publish ${dim("[file] [--type slide|layout|deck|theme] [--all]")}`)
|
|
416
417
|
console.log()
|
|
417
418
|
console.log(` Publish a slide, layout, or entire deck to the registry.`)
|
|
418
419
|
console.log()
|
|
@@ -421,6 +422,9 @@ export async function publish(args) {
|
|
|
421
422
|
console.log(` promptslide publish --type layout`)
|
|
422
423
|
console.log(` promptslide publish --type deck`)
|
|
423
424
|
console.log()
|
|
425
|
+
console.log(` ${bold("Options:")}`)
|
|
426
|
+
console.log(` --all Include slide files not referenced by deck-config.ts in a deck publish`)
|
|
427
|
+
console.log()
|
|
424
428
|
process.exit(0)
|
|
425
429
|
}
|
|
426
430
|
|
|
@@ -433,6 +437,7 @@ export async function publish(args) {
|
|
|
433
437
|
|
|
434
438
|
// Determine file to publish
|
|
435
439
|
let typeOverride = null
|
|
440
|
+
const includeUnreferencedSlides = args.includes("--all")
|
|
436
441
|
const typeIdx = args.indexOf("--type")
|
|
437
442
|
if (typeIdx !== -1 && args[typeIdx + 1]) {
|
|
438
443
|
typeOverride = args[typeIdx + 1]
|
|
@@ -538,9 +543,23 @@ export async function publish(args) {
|
|
|
538
543
|
: []
|
|
539
544
|
|
|
540
545
|
const slidesDir = join(cwd, "src", "slides")
|
|
541
|
-
const
|
|
546
|
+
const slideFilesOnDisk = existsSync(slidesDir)
|
|
542
547
|
? readdirSync(slidesDir).filter(f => f.endsWith(".tsx") || f.endsWith(".ts"))
|
|
543
548
|
: []
|
|
549
|
+
const slideSelection = selectSlidesForPublish(
|
|
550
|
+
slideFilesOnDisk,
|
|
551
|
+
deckConfig.slides,
|
|
552
|
+
includeUnreferencedSlides
|
|
553
|
+
)
|
|
554
|
+
if (slideSelection.missing.length > 0) {
|
|
555
|
+
console.error(` ${red("Error:")} deck-config.ts references slide files that do not exist:`)
|
|
556
|
+
console.error(` ${slideSelection.missing.join(", ")}`)
|
|
557
|
+
console.error(` ${dim("Restore the files or remove their entries from deck-config.ts before publishing.")}`)
|
|
558
|
+
console.log()
|
|
559
|
+
closePrompts()
|
|
560
|
+
process.exit(1)
|
|
561
|
+
}
|
|
562
|
+
const slideEntries = slideSelection.selected
|
|
544
563
|
|
|
545
564
|
// Discover shared source files (src/components/, src/lib/, etc.)
|
|
546
565
|
const sharedSources = discoverSharedSources(cwd)
|
|
@@ -555,12 +574,14 @@ export async function publish(args) {
|
|
|
555
574
|
console.log(` Slides: ${slideEntries.length}`)
|
|
556
575
|
if (sharedSources.length) console.log(` Shared: ${sharedSources.length} ${dim("(bundled with deck)")}`)
|
|
557
576
|
console.log(` Total: ${totalItems} items`)
|
|
558
|
-
|
|
559
|
-
const deckConfigSlideCount = deckConfig.slides.length
|
|
560
|
-
if (deckConfigSlideCount !== slideEntries.length) {
|
|
577
|
+
if (slideSelection.unreferenced.length > 0) {
|
|
561
578
|
console.log()
|
|
562
|
-
|
|
563
|
-
|
|
579
|
+
if (includeUnreferencedSlides) {
|
|
580
|
+
console.log(` ${yellow("!")} Including ${slideSelection.unreferenced.length} slide file(s) not referenced by deck-config.ts because --all was set:`)
|
|
581
|
+
} else {
|
|
582
|
+
console.log(` ${dim("ℹ")} Skipping ${slideSelection.unreferenced.length} slide file(s) not referenced by deck-config.ts:`)
|
|
583
|
+
}
|
|
584
|
+
console.log(` ${dim(slideSelection.unreferenced.join(", "))}`)
|
|
564
585
|
}
|
|
565
586
|
if (deckConfig.transition) {
|
|
566
587
|
console.log(` Transition: ${deckConfig.transition}${deckConfig.directionalTransition ? " (directional)" : ""}`)
|
|
@@ -612,6 +633,11 @@ export async function publish(args) {
|
|
|
612
633
|
|
|
613
634
|
// Read lockfile for skip-if-unchanged
|
|
614
635
|
const lock = readLockfile(cwd)
|
|
636
|
+
const prunedLockItems = pruneMissingLockfileItems(cwd, lock)
|
|
637
|
+
if (prunedLockItems.length > 0) {
|
|
638
|
+
writeLockfile(cwd, lock)
|
|
639
|
+
console.log(` ${dim("Pruned missing files from lockfile:")} ${prunedLockItems.join(", ")}`)
|
|
640
|
+
}
|
|
615
641
|
let published = 0
|
|
616
642
|
let skipped = 0
|
|
617
643
|
let failed = 0
|
package/src/commands/pull.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
addSlideToDeckConfig,
|
|
21
21
|
replaceDeckConfig
|
|
22
22
|
} from "../utils/deck-config.mjs"
|
|
23
|
+
import { collectRegistrySlideSlugs, partitionDeckSlides } from "../utils/deck-sync.mjs"
|
|
23
24
|
import { confirm, closePrompts } from "../utils/prompts.mjs"
|
|
24
25
|
|
|
25
26
|
export async function pull(args) {
|
|
@@ -174,9 +175,15 @@ export async function pull(args) {
|
|
|
174
175
|
|
|
175
176
|
// Update deck-config.ts
|
|
176
177
|
if (deckItem.type === "deck" && deckItem.meta?.slides) {
|
|
178
|
+
const registrySlideSlugs = collectRegistrySlideSlugs(resolved.items)
|
|
179
|
+
const { available, missing } = partitionDeckSlides(deckItem.meta.slides, registrySlideSlugs)
|
|
180
|
+
if (missing.length > 0) {
|
|
181
|
+
console.log(` ${yellow("!")} Skipped ${missing.length} slide(s) listed in the deck but not included in this version:`)
|
|
182
|
+
console.log(` ${dim(missing.map(s => s.slug).join(", "))}`)
|
|
183
|
+
}
|
|
177
184
|
const shouldReplace = await confirm(" Replace deck-config.ts with pulled deck config?", true)
|
|
178
185
|
if (shouldReplace) {
|
|
179
|
-
const slides =
|
|
186
|
+
const slides = available.map(s => ({
|
|
180
187
|
componentName: s.componentName || toPascalCase(s.slug),
|
|
181
188
|
importPath: `@/slides/${s.slug}`,
|
|
182
189
|
steps: s.steps,
|
|
@@ -189,7 +196,7 @@ export async function pull(args) {
|
|
|
189
196
|
console.log(` ${green("+")} Replaced ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`)
|
|
190
197
|
} else {
|
|
191
198
|
// Append individual slides
|
|
192
|
-
for (const s of
|
|
199
|
+
for (const s of available) {
|
|
193
200
|
const componentName = s.componentName || toPascalCase(s.slug)
|
|
194
201
|
const importPath = `@/slides/${s.slug}`
|
|
195
202
|
const updated = addSlideToDeckConfig(cwd, { componentName, importPath, steps: s.steps, section: s.section })
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the slide slugs backed by files in a resolved registry dependency set.
|
|
3
|
+
* Only registry-provided files count; scaffold files already present in a target
|
|
4
|
+
* directory must not make stale deck metadata look valid.
|
|
5
|
+
*
|
|
6
|
+
* @param {object[]} items
|
|
7
|
+
* @returns {Set<string>}
|
|
8
|
+
*/
|
|
9
|
+
export function collectRegistrySlideSlugs(items) {
|
|
10
|
+
const slugs = new Set()
|
|
11
|
+
|
|
12
|
+
for (const item of items ?? []) {
|
|
13
|
+
for (const file of item.files ?? []) {
|
|
14
|
+
const target = String(file.target ?? "")
|
|
15
|
+
.replaceAll("\\", "/")
|
|
16
|
+
.replace(/^\.\//, "")
|
|
17
|
+
const path = String(file.path ?? "")
|
|
18
|
+
.replaceAll("\\", "/")
|
|
19
|
+
.replace(/^\.\//, "")
|
|
20
|
+
const fullPath = `${target.replace(/\/?$/, "/")}${path}`.replace(/\/+/g, "/")
|
|
21
|
+
const match = fullPath.match(/^src\/slides\/(.+)\.tsx?$/)
|
|
22
|
+
if (match) slugs.add(match[1])
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return slugs
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Split deck metadata into entries backed by registry files and stale entries.
|
|
31
|
+
* Array order is preserved so generated deck configs match the published order.
|
|
32
|
+
*
|
|
33
|
+
* @param {object[]} slides
|
|
34
|
+
* @param {Set<string>} availableSlugs
|
|
35
|
+
* @returns {{ available: object[], missing: object[] }}
|
|
36
|
+
*/
|
|
37
|
+
export function partitionDeckSlides(slides, availableSlugs) {
|
|
38
|
+
const available = []
|
|
39
|
+
const missing = []
|
|
40
|
+
|
|
41
|
+
for (const slide of slides ?? []) {
|
|
42
|
+
if (availableSlugs.has(slide.slug)) available.push(slide)
|
|
43
|
+
else missing.push(slide)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { available, missing }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Select slide files for deck publishing. By default, only files referenced by
|
|
51
|
+
* deck-config.ts are included. The opt-in flag retains the legacy publish-all
|
|
52
|
+
* behavior for users who intentionally publish scratch or library slides.
|
|
53
|
+
*
|
|
54
|
+
* @param {string[]} slideEntries
|
|
55
|
+
* @param {{ slug: string }[]} configuredSlides
|
|
56
|
+
* @param {boolean} includeUnreferenced
|
|
57
|
+
* @returns {{ selected: string[], unreferenced: string[], missing: string[] }}
|
|
58
|
+
*/
|
|
59
|
+
export function selectSlidesForPublish(
|
|
60
|
+
slideEntries,
|
|
61
|
+
configuredSlides,
|
|
62
|
+
includeUnreferenced = false
|
|
63
|
+
) {
|
|
64
|
+
const filesBySlug = new Map(slideEntries.map(file => [file.replace(/\.tsx?$/, ""), file]))
|
|
65
|
+
const configuredSlugs = new Set(configuredSlides.map(slide => slide.slug))
|
|
66
|
+
const unreferenced = slideEntries.filter(
|
|
67
|
+
file => !configuredSlugs.has(file.replace(/\.tsx?$/, ""))
|
|
68
|
+
)
|
|
69
|
+
const missing = configuredSlides.map(slide => slide.slug).filter(slug => !filesBySlug.has(slug))
|
|
70
|
+
const selected = includeUnreferenced
|
|
71
|
+
? [...slideEntries]
|
|
72
|
+
: configuredSlides.flatMap(slide => {
|
|
73
|
+
const file = filesBySlug.get(slide.slug)
|
|
74
|
+
return file ? [file] : []
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return { selected, unreferenced, missing }
|
|
78
|
+
}
|
package/src/utils/registry.mjs
CHANGED
|
@@ -150,6 +150,29 @@ export function removeLockfileItem(cwd, slug) {
|
|
|
150
150
|
writeLockfile(cwd, lock)
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Remove lockfile entries whose tracked files have all disappeared locally.
|
|
155
|
+
* Entries with no files (such as the deck manifest) are retained.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} cwd
|
|
158
|
+
* @param {{ items?: Record<string, { files?: Record<string, string> }> }} lock
|
|
159
|
+
* @returns {string[]} Removed item slugs
|
|
160
|
+
*/
|
|
161
|
+
export function pruneMissingLockfileItems(cwd, lock) {
|
|
162
|
+
const removed = []
|
|
163
|
+
lock.items ||= {}
|
|
164
|
+
|
|
165
|
+
for (const [slug, item] of Object.entries(lock.items)) {
|
|
166
|
+
const paths = Object.keys(item.files ?? {})
|
|
167
|
+
if (paths.length > 0 && paths.every(path => !existsSync(join(cwd, path)))) {
|
|
168
|
+
delete lock.items[slug]
|
|
169
|
+
removed.push(slug)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return removed
|
|
174
|
+
}
|
|
175
|
+
|
|
153
176
|
/**
|
|
154
177
|
* Build common auth headers for registry API requests.
|
|
155
178
|
* @param {{ token: string, organizationId?: string }} auth
|