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.
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/commands/add.mjs +182 -0
- package/src/commands/build.mjs +1 -1
- package/src/commands/create.mjs +44 -28
- package/src/commands/info.mjs +79 -0
- package/src/commands/list.mjs +37 -0
- package/src/commands/login.mjs +176 -0
- package/src/commands/logout.mjs +17 -0
- package/src/commands/org.mjs +68 -0
- package/src/commands/publish.mjs +384 -0
- package/src/commands/remove.mjs +111 -0
- package/src/commands/search.mjs +57 -0
- package/src/commands/to-image.mjs +52 -0
- package/src/commands/update.mjs +218 -0
- package/src/core/slide-deck.tsx +50 -0
- package/src/index.mjs +70 -0
- package/src/utils/ansi.mjs +1 -0
- package/src/utils/auth.mjs +66 -0
- package/src/utils/colors.mjs +0 -7
- package/src/utils/deck-config.mjs +208 -0
- package/src/utils/export.mjs +115 -0
- package/src/utils/registry.mjs +319 -0
- package/src/vite/config.mjs +1 -1
- package/src/vite/plugin.mjs +92 -11
- package/templates/default/AGENTS.md +42 -408
- package/templates/default/src/globals.css +5 -43
- package/templates/default/src/slides/slide-title.tsx +0 -5
|
@@ -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
|
+
}
|
package/src/core/slide-deck.tsx
CHANGED
|
@@ -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:
|
package/src/utils/ansi.mjs
CHANGED
|
@@ -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 }
|
package/src/utils/colors.mjs
CHANGED
|
@@ -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
|
+
}
|