promptslide 0.2.1 → 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 +43 -27
- 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,384 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
|
|
2
|
+
import { join, basename, relative, extname } from "node:path"
|
|
3
|
+
|
|
4
|
+
import { bold, green, cyan, red, dim } from "../utils/ansi.mjs"
|
|
5
|
+
import { requireAuth } from "../utils/auth.mjs"
|
|
6
|
+
import { captureSlideAsDataUri, isPlaywrightAvailable } from "../utils/export.mjs"
|
|
7
|
+
import { publishToRegistry, registryItemExists, updateLockfileItem, hashContent, detectPackageManager } from "../utils/registry.mjs"
|
|
8
|
+
import { prompt, confirm, closePrompts } from "../utils/prompts.mjs"
|
|
9
|
+
|
|
10
|
+
function titleCase(slug) {
|
|
11
|
+
return slug
|
|
12
|
+
.replace(/\.tsx?$/, "")
|
|
13
|
+
.replace(/[-_]/g, " ")
|
|
14
|
+
.replace(/\b\w/g, c => c.toUpperCase())
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function detectType(filePath) {
|
|
18
|
+
if (filePath.includes("/slides/") || filePath.includes("\\slides\\")) return "slide"
|
|
19
|
+
if (filePath.includes("/layouts/") || filePath.includes("\\layouts\\")) return "layout"
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function detectSteps(content) {
|
|
24
|
+
const matches = content.matchAll(/step=\{(\d+)\}/g)
|
|
25
|
+
let max = 0
|
|
26
|
+
for (const m of matches) {
|
|
27
|
+
const n = parseInt(m[1], 10)
|
|
28
|
+
if (n > max) max = n
|
|
29
|
+
}
|
|
30
|
+
return max
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function detectNpmDeps(content) {
|
|
34
|
+
const deps = {}
|
|
35
|
+
// Match both unscoped (foo) and scoped (@scope/foo) packages, exclude relative imports
|
|
36
|
+
const importRegex = /import\s+.*?\s+from\s+["']((?:@[a-zA-Z0-9-]+\/)?[a-zA-Z0-9-][^"']*)["']/g
|
|
37
|
+
for (const match of content.matchAll(importRegex)) {
|
|
38
|
+
// Get the package name (handle deep imports like "framer-motion/client")
|
|
39
|
+
const full = match[1]
|
|
40
|
+
const pkg = full.startsWith("@") ? full.split("/").slice(0, 2).join("/") : full.split("/")[0]
|
|
41
|
+
// Skip relative imports, react, and internal packages
|
|
42
|
+
if (pkg.startsWith(".") || pkg === "react" || pkg === "react-dom" || pkg.startsWith("@promptslide/")) continue
|
|
43
|
+
deps[pkg] = "latest"
|
|
44
|
+
}
|
|
45
|
+
return deps
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function detectRegistryDeps(content) {
|
|
49
|
+
const deps = []
|
|
50
|
+
const importRegex = /import\s+.*?\s+from\s+["']@\/layouts\/([^"']+)["']/g
|
|
51
|
+
for (const match of content.matchAll(importRegex)) {
|
|
52
|
+
const name = match[1].replace(/\.tsx?$/, "")
|
|
53
|
+
deps.push(name)
|
|
54
|
+
}
|
|
55
|
+
return deps
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp"])
|
|
59
|
+
const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB
|
|
60
|
+
|
|
61
|
+
function readPreviewImage(imagePath) {
|
|
62
|
+
if (!existsSync(imagePath)) return null
|
|
63
|
+
const ext = extname(imagePath).toLowerCase()
|
|
64
|
+
if (!IMAGE_EXTS.has(ext)) return null
|
|
65
|
+
const stat = statSync(imagePath)
|
|
66
|
+
if (stat.size > MAX_IMAGE_SIZE) return null
|
|
67
|
+
const buf = readFileSync(imagePath)
|
|
68
|
+
const mime = ext === ".png" ? "image/png" : ext === ".webp" ? "image/webp" : "image/jpeg"
|
|
69
|
+
return `data:${mime};base64,${buf.toString("base64")}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function scanForFiles(cwd) {
|
|
73
|
+
const files = []
|
|
74
|
+
const dirs = [
|
|
75
|
+
{ dir: join(cwd, "src", "slides"), target: "src/slides/" },
|
|
76
|
+
{ dir: join(cwd, "src", "layouts"), target: "src/layouts/" }
|
|
77
|
+
]
|
|
78
|
+
for (const { dir, target } of dirs) {
|
|
79
|
+
if (!existsSync(dir)) continue
|
|
80
|
+
for (const entry of readdirSync(dir)) {
|
|
81
|
+
if (entry.endsWith(".tsx") || entry.endsWith(".ts")) {
|
|
82
|
+
files.push({ path: entry, target, fullPath: join(dir, entry) })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return files
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Publish a single item to the registry.
|
|
91
|
+
* @param {{ filePath: string, cwd: string, auth: object, typeOverride?: string, interactive?: boolean }} opts
|
|
92
|
+
* @returns {Promise<{ slug: string, status: string }>}
|
|
93
|
+
*/
|
|
94
|
+
async function publishItem({ filePath, cwd, auth, typeOverride, interactive = true }) {
|
|
95
|
+
const fullPath = join(cwd, filePath)
|
|
96
|
+
if (!existsSync(fullPath)) {
|
|
97
|
+
throw new Error(`File not found: ${filePath}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const content = readFileSync(fullPath, "utf-8")
|
|
101
|
+
const fileName = basename(fullPath)
|
|
102
|
+
const slug = fileName.replace(/\.tsx?$/, "")
|
|
103
|
+
|
|
104
|
+
const type = typeOverride || detectType(filePath) || "slide"
|
|
105
|
+
const steps = detectSteps(content)
|
|
106
|
+
const npmDeps = detectNpmDeps(content)
|
|
107
|
+
const registryDeps = detectRegistryDeps(content)
|
|
108
|
+
const target = type === "layout" ? "src/layouts/" : "src/slides/"
|
|
109
|
+
|
|
110
|
+
let title, description, tags, section, releaseNotes, previewImage
|
|
111
|
+
|
|
112
|
+
if (interactive) {
|
|
113
|
+
title = await prompt("Title:", titleCase(slug))
|
|
114
|
+
description = await prompt("Description:", "")
|
|
115
|
+
const tagsInput = await prompt("Tags (comma-separated):", "")
|
|
116
|
+
tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
|
|
117
|
+
section = await prompt("Section:", "")
|
|
118
|
+
releaseNotes = await prompt("Release notes:", "")
|
|
119
|
+
const imagePath = await prompt("Preview image path (leave empty to auto-generate):", "")
|
|
120
|
+
if (imagePath) {
|
|
121
|
+
const resolved = join(cwd, imagePath)
|
|
122
|
+
previewImage = readPreviewImage(resolved)
|
|
123
|
+
if (!previewImage) {
|
|
124
|
+
console.log(` ${dim("⚠ Skipping image: file not found, unsupported format, or > 2MB")}`)
|
|
125
|
+
}
|
|
126
|
+
} else if (await isPlaywrightAvailable()) {
|
|
127
|
+
console.log(` ${dim("Generating preview image...")}`)
|
|
128
|
+
previewImage = await captureSlideAsDataUri({ cwd, slidePath: filePath })
|
|
129
|
+
if (previewImage) {
|
|
130
|
+
console.log(` ${green("✓")} Preview image generated`)
|
|
131
|
+
} else {
|
|
132
|
+
console.log(` ${dim("⚠ Could not generate preview image")}`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
title = titleCase(slug)
|
|
137
|
+
description = ""
|
|
138
|
+
tags = []
|
|
139
|
+
section = ""
|
|
140
|
+
releaseNotes = ""
|
|
141
|
+
previewImage = await captureSlideAsDataUri({ cwd, slidePath: filePath }).catch(() => null)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const payload = {
|
|
145
|
+
type,
|
|
146
|
+
slug,
|
|
147
|
+
title,
|
|
148
|
+
description: description || undefined,
|
|
149
|
+
tags,
|
|
150
|
+
steps,
|
|
151
|
+
section: section || undefined,
|
|
152
|
+
files: [{ path: fileName, target, content }],
|
|
153
|
+
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
|
|
154
|
+
registryDependencies: registryDeps.length ? registryDeps : undefined,
|
|
155
|
+
releaseNotes: releaseNotes || undefined,
|
|
156
|
+
previewImage: previewImage || undefined
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = await publishToRegistry(payload, auth)
|
|
160
|
+
|
|
161
|
+
// Track in lockfile
|
|
162
|
+
const fileHashes = { [target + fileName]: hashContent(content) }
|
|
163
|
+
updateLockfileItem(cwd, slug, result.version ?? 0, fileHashes)
|
|
164
|
+
|
|
165
|
+
return { slug, status: result.status || "published", version: result.version }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function publish(args) {
|
|
169
|
+
const cwd = process.cwd()
|
|
170
|
+
|
|
171
|
+
console.log()
|
|
172
|
+
console.log(` ${bold("promptslide")} ${dim("publish")}`)
|
|
173
|
+
console.log()
|
|
174
|
+
|
|
175
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
176
|
+
console.log(` ${bold("Usage:")} promptslide publish ${dim("[file] [--type slide|layout|deck|theme]")}`)
|
|
177
|
+
console.log()
|
|
178
|
+
console.log(` Publish a slide or layout to the registry.`)
|
|
179
|
+
console.log()
|
|
180
|
+
console.log(` ${bold("Examples:")}`)
|
|
181
|
+
console.log(` promptslide publish src/slides/slide-hero.tsx`)
|
|
182
|
+
console.log(` promptslide publish --type layout`)
|
|
183
|
+
console.log()
|
|
184
|
+
process.exit(0)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const auth = requireAuth()
|
|
188
|
+
|
|
189
|
+
// Determine file to publish
|
|
190
|
+
let typeOverride = null
|
|
191
|
+
const typeIdx = args.indexOf("--type")
|
|
192
|
+
if (typeIdx !== -1 && args[typeIdx + 1]) {
|
|
193
|
+
typeOverride = args[typeIdx + 1]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const flagIndices = new Set()
|
|
197
|
+
if (typeIdx !== -1) {
|
|
198
|
+
flagIndices.add(typeIdx)
|
|
199
|
+
flagIndices.add(typeIdx + 1)
|
|
200
|
+
}
|
|
201
|
+
let filePath = args.find((a, i) => !a.startsWith("--") && !flagIndices.has(i))
|
|
202
|
+
|
|
203
|
+
if (!filePath) {
|
|
204
|
+
// Interactive mode: scan for files
|
|
205
|
+
const available = scanForFiles(cwd)
|
|
206
|
+
if (available.length === 0) {
|
|
207
|
+
console.error(` ${red("Error:")} No .tsx files found in src/slides/ or src/layouts/.`)
|
|
208
|
+
process.exit(1)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(` ${bold("Available files:")}`)
|
|
212
|
+
available.forEach((f, i) => {
|
|
213
|
+
console.log(` ${dim(`${i + 1}.`)} ${f.target}${f.path}`)
|
|
214
|
+
})
|
|
215
|
+
console.log()
|
|
216
|
+
|
|
217
|
+
const choice = await prompt("Select file number:", "1")
|
|
218
|
+
const idx = parseInt(choice, 10) - 1
|
|
219
|
+
if (idx < 0 || idx >= available.length) {
|
|
220
|
+
console.error(` ${red("Error:")} Invalid selection.`)
|
|
221
|
+
process.exit(1)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
filePath = relative(cwd, available[idx].fullPath)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Resolve full path and read content
|
|
228
|
+
const fullPath = join(cwd, filePath)
|
|
229
|
+
if (!existsSync(fullPath)) {
|
|
230
|
+
console.error(` ${red("Error:")} File not found: ${filePath}`)
|
|
231
|
+
process.exit(1)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const content = readFileSync(fullPath, "utf-8")
|
|
235
|
+
const fileName = basename(fullPath)
|
|
236
|
+
const slug = fileName.replace(/\.tsx?$/, "")
|
|
237
|
+
|
|
238
|
+
// Detect metadata
|
|
239
|
+
const type = typeOverride || detectType(filePath) || "slide"
|
|
240
|
+
const steps = detectSteps(content)
|
|
241
|
+
const npmDeps = detectNpmDeps(content)
|
|
242
|
+
const registryDeps = detectRegistryDeps(content)
|
|
243
|
+
const target = type === "layout" ? "src/layouts/" : "src/slides/"
|
|
244
|
+
|
|
245
|
+
console.log(` File: ${cyan(filePath)}`)
|
|
246
|
+
console.log(` Type: ${type}`)
|
|
247
|
+
console.log(` Steps: ${steps}`)
|
|
248
|
+
if (Object.keys(npmDeps).length) {
|
|
249
|
+
console.log(` Dependencies: ${dim(Object.keys(npmDeps).join(", "))}`)
|
|
250
|
+
}
|
|
251
|
+
if (registryDeps.length) {
|
|
252
|
+
console.log(` Registry deps: ${dim(registryDeps.join(", "))}`)
|
|
253
|
+
}
|
|
254
|
+
console.log()
|
|
255
|
+
|
|
256
|
+
// Check if registry dependencies exist and offer to publish missing ones
|
|
257
|
+
if (registryDeps.length) {
|
|
258
|
+
const missing = []
|
|
259
|
+
|
|
260
|
+
for (const depSlug of registryDeps) {
|
|
261
|
+
const exists = await registryItemExists(depSlug, auth)
|
|
262
|
+
if (!exists) {
|
|
263
|
+
missing.push(depSlug)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (missing.length) {
|
|
268
|
+
console.log(` ${bold("Missing dependencies:")} ${missing.length} registry dep(s) not yet published`)
|
|
269
|
+
for (const depSlug of missing) {
|
|
270
|
+
// Try to find the local file
|
|
271
|
+
const candidates = [
|
|
272
|
+
join(cwd, "src", "layouts", `${depSlug}.tsx`),
|
|
273
|
+
join(cwd, "src", "layouts", `${depSlug}.ts`),
|
|
274
|
+
join(cwd, "src", "slides", `${depSlug}.tsx`),
|
|
275
|
+
join(cwd, "src", "slides", `${depSlug}.ts`),
|
|
276
|
+
]
|
|
277
|
+
const localFile = candidates.find(f => existsSync(f))
|
|
278
|
+
|
|
279
|
+
if (localFile) {
|
|
280
|
+
const relPath = relative(cwd, localFile)
|
|
281
|
+
const depType = detectType(relPath) || "layout"
|
|
282
|
+
const shouldPublish = await confirm(
|
|
283
|
+
`Publish ${bold(depSlug)} (${cyan(relPath)}) as ${depType}?`
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if (shouldPublish) {
|
|
287
|
+
console.log()
|
|
288
|
+
console.log(` ${dim("─── Publishing dependency:")} ${bold(depSlug)} ${dim("───")}`)
|
|
289
|
+
console.log()
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const result = await publishItem({
|
|
293
|
+
filePath: relPath,
|
|
294
|
+
cwd,
|
|
295
|
+
auth,
|
|
296
|
+
typeOverride: depType,
|
|
297
|
+
interactive: true
|
|
298
|
+
})
|
|
299
|
+
console.log()
|
|
300
|
+
const depVer = result.version ? ` ${dim(`v${result.version}`)}` : ""
|
|
301
|
+
console.log(` ${green("✓")} Published dependency ${bold(result.slug)}${depVer}`)
|
|
302
|
+
console.log()
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.error(` ${red("Error publishing dependency:")} ${err.message}`)
|
|
305
|
+
console.log(` ${dim("Continuing with main item...")}`)
|
|
306
|
+
console.log()
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
console.log(` ${dim("⚠")} ${depSlug}: local file not found, skipping`)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log(` ${dim("─── Main item:")} ${bold(slug)} ${dim("───")}`)
|
|
315
|
+
console.log()
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Collect metadata for main item
|
|
320
|
+
const title = await prompt("Title:", titleCase(slug))
|
|
321
|
+
const description = await prompt("Description:", "")
|
|
322
|
+
const tagsInput = await prompt("Tags (comma-separated):", "")
|
|
323
|
+
const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
|
|
324
|
+
const section = await prompt("Section:", "")
|
|
325
|
+
const releaseNotes = await prompt("Release notes:", "")
|
|
326
|
+
const previewImagePath = await prompt("Preview image path (leave empty to auto-generate):", "")
|
|
327
|
+
let previewImage = null
|
|
328
|
+
if (previewImagePath) {
|
|
329
|
+
const resolved = join(cwd, previewImagePath)
|
|
330
|
+
previewImage = readPreviewImage(resolved)
|
|
331
|
+
if (!previewImage) {
|
|
332
|
+
console.log(` ${dim("⚠ Skipping image: file not found, unsupported format, or > 2MB")}`)
|
|
333
|
+
}
|
|
334
|
+
} else if (await isPlaywrightAvailable()) {
|
|
335
|
+
console.log(` ${dim("Generating preview image...")}`)
|
|
336
|
+
previewImage = await captureSlideAsDataUri({ cwd, slidePath: filePath })
|
|
337
|
+
if (previewImage) {
|
|
338
|
+
console.log(` ${green("✓")} Preview image generated`)
|
|
339
|
+
} else {
|
|
340
|
+
console.log(` ${dim("⚠ Could not generate preview image")}`)
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
const pm = detectPackageManager(cwd)
|
|
344
|
+
const installCmd = pm === "bun" ? "bun add -d playwright" : pm === "pnpm" ? "pnpm add -D playwright" : pm === "yarn" ? "yarn add -D playwright" : "npm install -D playwright"
|
|
345
|
+
console.log(` ${dim("Tip: Install playwright for auto-generated preview images")}`)
|
|
346
|
+
console.log(` ${dim(` ${installCmd} && npx playwright install chromium`)}`)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
console.log()
|
|
350
|
+
|
|
351
|
+
// Publish main item
|
|
352
|
+
const payload = {
|
|
353
|
+
type,
|
|
354
|
+
slug,
|
|
355
|
+
title,
|
|
356
|
+
description: description || undefined,
|
|
357
|
+
tags,
|
|
358
|
+
steps,
|
|
359
|
+
section: section || undefined,
|
|
360
|
+
files: [{ path: fileName, target, content }],
|
|
361
|
+
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
|
|
362
|
+
registryDependencies: registryDeps.length ? registryDeps : undefined,
|
|
363
|
+
releaseNotes: releaseNotes || undefined,
|
|
364
|
+
previewImage: previewImage || undefined
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const result = await publishToRegistry(payload, auth)
|
|
369
|
+
const verTag = result.version ? ` v${result.version}` : ""
|
|
370
|
+
console.log(` ${green("✓")} Published ${bold(slug)}${verTag} to ${auth.organizationName || "registry"}`)
|
|
371
|
+
console.log(` Status: ${result.status || "published"}`)
|
|
372
|
+
console.log(` Install: ${cyan(`promptslide add ${slug}`)}`)
|
|
373
|
+
|
|
374
|
+
// Track in lockfile
|
|
375
|
+
const fileHashes = { [target + fileName]: hashContent(content) }
|
|
376
|
+
updateLockfileItem(cwd, slug, result.version ?? 0, fileHashes)
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.error(` ${red("Error:")} ${err.message}`)
|
|
379
|
+
process.exit(1)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log()
|
|
383
|
+
closePrompts()
|
|
384
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
|
|
4
|
+
import { bold, green, cyan, red, yellow, dim } from "../utils/ansi.mjs"
|
|
5
|
+
import { readLockfile, removeLockfileItem, isFileDirty } from "../utils/registry.mjs"
|
|
6
|
+
import { toPascalCase, removeSlideFromDeckConfig } from "../utils/deck-config.mjs"
|
|
7
|
+
import { confirm, closePrompts } from "../utils/prompts.mjs"
|
|
8
|
+
|
|
9
|
+
export async function remove(args) {
|
|
10
|
+
const cwd = process.cwd()
|
|
11
|
+
|
|
12
|
+
console.log()
|
|
13
|
+
console.log(` ${bold("promptslide")} ${dim("remove")}`)
|
|
14
|
+
console.log()
|
|
15
|
+
|
|
16
|
+
const name = args[0]
|
|
17
|
+
if (!name || name === "--help" || name === "-h") {
|
|
18
|
+
console.log(` ${bold("Usage:")} promptslide remove ${dim("<name>")}`)
|
|
19
|
+
console.log()
|
|
20
|
+
console.log(` Remove an installed slide, layout, or deck.`)
|
|
21
|
+
console.log()
|
|
22
|
+
console.log(` ${bold("Examples:")}`)
|
|
23
|
+
console.log(` promptslide remove slide-hero-gradient`)
|
|
24
|
+
console.log(` promptslide remove deck-pitch`)
|
|
25
|
+
console.log()
|
|
26
|
+
process.exit(0)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check lockfile
|
|
30
|
+
const lock = readLockfile(cwd)
|
|
31
|
+
if (!lock.items[name]) {
|
|
32
|
+
console.error(` ${red("Error:")} "${name}" is not installed (not found in lockfile).`)
|
|
33
|
+
console.log()
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lockEntry = lock.items[name]
|
|
38
|
+
|
|
39
|
+
// Use lockfile files map as source of truth
|
|
40
|
+
const filesToRemove = []
|
|
41
|
+
let hasDirtyFiles = false
|
|
42
|
+
|
|
43
|
+
for (const [relativePath, storedHash] of Object.entries(lockEntry.files)) {
|
|
44
|
+
const targetPath = join(cwd, relativePath)
|
|
45
|
+
if (existsSync(targetPath)) {
|
|
46
|
+
const dirty = isFileDirty(cwd, relativePath, storedHash)
|
|
47
|
+
if (dirty) hasDirtyFiles = true
|
|
48
|
+
filesToRemove.push({ path: targetPath, display: relativePath, dirty })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Show what will be removed
|
|
53
|
+
if (filesToRemove.length > 0) {
|
|
54
|
+
console.log(` Files to remove:`)
|
|
55
|
+
for (const f of filesToRemove) {
|
|
56
|
+
const tag = f.dirty ? ` ${yellow("(modified)")}` : ""
|
|
57
|
+
console.log(` ${dim("•")} ${f.display}${tag}`)
|
|
58
|
+
}
|
|
59
|
+
console.log()
|
|
60
|
+
} else {
|
|
61
|
+
console.log(` ${dim("No local files found for this item.")}`)
|
|
62
|
+
console.log()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const confirmMsg = hasDirtyFiles
|
|
66
|
+
? `Remove ${bold(name)}? ${yellow("Some files have local changes that will be lost.")}`
|
|
67
|
+
: `Remove ${bold(name)}?`
|
|
68
|
+
const ok = await confirm(confirmMsg, !hasDirtyFiles)
|
|
69
|
+
if (!ok) {
|
|
70
|
+
console.log(` ${dim("Cancelled.")}`)
|
|
71
|
+
console.log()
|
|
72
|
+
closePrompts()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Delete files
|
|
77
|
+
let removed = 0
|
|
78
|
+
for (const f of filesToRemove) {
|
|
79
|
+
try {
|
|
80
|
+
unlinkSync(f.path)
|
|
81
|
+
console.log(` ${green("✓")} Removed ${cyan(f.display)}`)
|
|
82
|
+
removed++
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.log(` ${red("⚠")} Could not remove ${f.display}: ${err.message}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Remove from deck-config if any files are slides
|
|
89
|
+
for (const relativePath of Object.keys(lockEntry.files)) {
|
|
90
|
+
if (relativePath.includes("slides/")) {
|
|
91
|
+
const fileName = relativePath.split("/").pop().replace(/\.tsx?$/, "")
|
|
92
|
+
const componentName = toPascalCase(fileName)
|
|
93
|
+
const updated = removeSlideFromDeckConfig(cwd, componentName)
|
|
94
|
+
if (updated) {
|
|
95
|
+
console.log(` ${green("✓")} Removed ${componentName} from ${cyan("deck-config.ts")}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Remove from lockfile
|
|
101
|
+
removeLockfileItem(cwd, name)
|
|
102
|
+
console.log(` ${green("✓")} Removed from lockfile`)
|
|
103
|
+
|
|
104
|
+
if (removed > 0) {
|
|
105
|
+
console.log()
|
|
106
|
+
console.log(` ${dim(`Removed ${removed} file${removed === 1 ? "" : "s"}.`)}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log()
|
|
110
|
+
closePrompts()
|
|
111
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { bold, cyan, red, dim } from "../utils/ansi.mjs"
|
|
2
|
+
import { requireAuth } from "../utils/auth.mjs"
|
|
3
|
+
import { searchRegistry } from "../utils/registry.mjs"
|
|
4
|
+
|
|
5
|
+
function padEnd(str, len) {
|
|
6
|
+
str = String(str)
|
|
7
|
+
return str.length >= len ? str : str + " ".repeat(len - str.length)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function printTable(items) {
|
|
11
|
+
if (!items.length) {
|
|
12
|
+
console.log(` ${dim("No items found.")}`)
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const cols = { name: 24, type: 8, steps: 6, author: 20, downloads: 10 }
|
|
17
|
+
|
|
18
|
+
console.log(
|
|
19
|
+
` ${dim(padEnd("Name", cols.name))}${dim(padEnd("Type", cols.type))}${dim(padEnd("Steps", cols.steps))}${dim(padEnd("Author", cols.author))}${dim("Downloads")}`
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
for (const item of items) {
|
|
23
|
+
const steps = item.type === "deck" ? "—" : String(item.steps ?? 0)
|
|
24
|
+
console.log(
|
|
25
|
+
` ${padEnd(item.name, cols.name)}${padEnd(item.type, cols.type)}${padEnd(steps, cols.steps)}${padEnd(item.author || "—", cols.author)}${item.downloads ?? 0}`
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function search(args) {
|
|
31
|
+
console.log()
|
|
32
|
+
console.log(` ${bold("promptslide")} ${dim("search")}`)
|
|
33
|
+
console.log()
|
|
34
|
+
|
|
35
|
+
const query = args.filter(a => !a.startsWith("--")).join(" ")
|
|
36
|
+
if (!query) {
|
|
37
|
+
console.log(` ${bold("Usage:")} promptslide search ${dim("<query>")}`)
|
|
38
|
+
console.log()
|
|
39
|
+
process.exit(0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const auth = requireAuth()
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const results = await searchRegistry({ search: query }, auth)
|
|
46
|
+
console.log(` Results for "${cyan(query)}":`)
|
|
47
|
+
console.log()
|
|
48
|
+
printTable(Array.isArray(results) ? results : results.items || [])
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error(` ${red("Error:")} ${err.message}`)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { printTable }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs"
|
|
2
|
+
import { resolve, basename } from "node:path"
|
|
3
|
+
|
|
4
|
+
import { bold, green, red, dim } from "../utils/ansi.mjs"
|
|
5
|
+
import { captureSlideScreenshot, isPlaywrightAvailable } from "../utils/export.mjs"
|
|
6
|
+
|
|
7
|
+
export async function toImage(args) {
|
|
8
|
+
const slidePath = args[0]
|
|
9
|
+
const outputIndex = args.indexOf("-o")
|
|
10
|
+
const output = outputIndex !== -1 ? args[outputIndex + 1] : null
|
|
11
|
+
|
|
12
|
+
if (!slidePath || slidePath === "--help" || slidePath === "-h") {
|
|
13
|
+
console.log()
|
|
14
|
+
console.log(` ${bold("Usage:")} promptslide to-image <slide> [options]`)
|
|
15
|
+
console.log()
|
|
16
|
+
console.log(` ${bold("Options:")}`)
|
|
17
|
+
console.log(` -o <file> Output file path (default: <slide-name>.png)`)
|
|
18
|
+
console.log()
|
|
19
|
+
console.log(` ${bold("Examples:")}`)
|
|
20
|
+
console.log(` ${dim("promptslide to-image src/slides/slide-title.tsx")}`)
|
|
21
|
+
console.log(` ${dim("promptslide to-image src/slides/slide-title.tsx -o preview.png")}`)
|
|
22
|
+
console.log()
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!(await isPlaywrightAvailable())) {
|
|
27
|
+
console.error()
|
|
28
|
+
console.error(` ${red("Error:")} Playwright is required for image export.`)
|
|
29
|
+
console.error(` Install it with: ${bold("npm install -D playwright && npx playwright install chromium")}`)
|
|
30
|
+
console.error()
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const outputPath = resolve(output || `${basename(slidePath).replace(/\.tsx?$/, "")}.png`)
|
|
35
|
+
|
|
36
|
+
console.log()
|
|
37
|
+
console.log(` ${dim("Capturing screenshot...")}`)
|
|
38
|
+
|
|
39
|
+
const buffer = await captureSlideScreenshot({
|
|
40
|
+
cwd: process.cwd(),
|
|
41
|
+
slidePath
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
if (!buffer) {
|
|
45
|
+
console.error(` ${red("Error:")} Failed to capture screenshot.`)
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
writeFileSync(outputPath, buffer)
|
|
50
|
+
console.log(` ${green("✓")} Saved to ${bold(outputPath)}`)
|
|
51
|
+
console.log()
|
|
52
|
+
}
|