@cyber-dash-tech/revela 0.7.8 → 0.8.1
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/README.md +14 -14
- package/README.zh-CN.md +14 -14
- package/designs/starter/DESIGN.md +118 -14
- package/designs/starter/preview.html +48 -12
- package/lib/agents/research-prompt.ts +4 -4
- package/lib/commands/designs-new.ts +14 -0
- package/lib/commands/edit.ts +3 -6
- package/lib/commands/help.ts +2 -2
- package/lib/commands/init.ts +6 -5
- package/lib/commands/review.ts +5 -8
- package/lib/decks-state.ts +58 -12
- package/lib/design/designs.ts +20 -0
- package/lib/edit/deck-state.ts +8 -2
- package/lib/edit/open.ts +31 -3
- package/lib/edit/resolve-deck.ts +20 -75
- package/lib/edit/server.ts +196 -54
- package/package.json +1 -1
- package/plugin.ts +37 -3
- package/skill/SKILL.md +31 -26
- package/tools/decks.ts +13 -11
- package/tools/edit.ts +6 -10
- package/tools/media-batch-save.ts +1 -1
- package/tools/media-save.ts +1 -1
- package/tools/research-images-list.ts +1 -1
- package/tools/research-save.ts +10 -10
package/lib/commands/init.ts
CHANGED
|
@@ -16,9 +16,9 @@ export function buildInitPrompt({
|
|
|
16
16
|
Goal:
|
|
17
17
|
- Build or update ${DECKS_STATE_FILE}, the workspace-level machine-readable state file for slide deck work.
|
|
18
18
|
- Use the \`revela-decks\` tool for state updates. Do not write or patch ${DECKS_STATE_FILE} directly.
|
|
19
|
-
- Capture stable project context, available source materials,
|
|
20
|
-
- Do not treat initialization as permission to write a slide deck;
|
|
21
|
-
- ${DECKS_STATE_FILE} is the source of truth for
|
|
19
|
+
- Capture stable project context, available source materials, the current deck spec, slide plan, and open questions for future sessions.
|
|
20
|
+
- Do not treat initialization as permission to write a slide deck; the current deck must pass a later readiness review.
|
|
21
|
+
- ${DECKS_STATE_FILE} is the source of truth for the single current workspace deck.
|
|
22
22
|
|
|
23
23
|
Current state:
|
|
24
24
|
- ${mode}
|
|
@@ -40,12 +40,12 @@ Workflow:
|
|
|
40
40
|
- \`presentations/**/*.html\`
|
|
41
41
|
- \`decks/**/*.pdf\`
|
|
42
42
|
- \`slides/**/*.pdf\`
|
|
43
|
-
Run these searches only inside the current workspace root. These are generated/output decks, not necessarily source materials.
|
|
43
|
+
Run these searches only inside the current workspace root. These are generated/output decks, not necessarily source materials. If \`decks/\` contains exactly one HTML file, treat it as the current deck artifact. If \`decks/\` contains multiple HTML files, stop and ask the user to move extra decks to separate workspaces before adopting one.
|
|
44
44
|
3. Select the files that look most relevant for future slide decks. Prioritize source decks, PDFs, Word docs, spreadsheets, CSVs, Markdown, text notes, and relevant existing generated decks.
|
|
45
45
|
4. For selected PDF/PPTX/DOCX/XLSX files, call \`revela-extract-document-materials\` before deciding what to summarize.
|
|
46
46
|
5. Read only the materials needed to form a conservative workspace memory. Do not exhaustively read every file if the workspace is large.
|
|
47
47
|
6. Call \`revela-decks\` with action \`init\` to create ${DECKS_STATE_FILE} if needed.
|
|
48
|
-
7. If this conversation already contains a concrete deck task, call \`revela-decks\` with action \`upsertDeck\` and later \`upsertSlides\` only for explicit deck spec information. Do not mark readiness ready during init.
|
|
48
|
+
7. If this conversation already contains a concrete deck task, call \`revela-decks\` with action \`upsertDeck\` and later \`upsertSlides\` only for explicit deck spec information. Do not pass or ask for a deck key; the tool uses the workspace folder name internally. Do not mark readiness ready during init.
|
|
49
49
|
8. Report what was initialized or updated and list any open questions.
|
|
50
50
|
|
|
51
51
|
Memory rules:
|
|
@@ -54,6 +54,7 @@ Memory rules:
|
|
|
54
54
|
- Do not infer personal preferences from one-off requests.
|
|
55
55
|
- Do not store secrets, credentials, API keys, tokens, account details, or sensitive personal information.
|
|
56
56
|
- Do not mark writeReadiness as ready during init unless the current deck has already passed an explicit \`revela-decks\` review.
|
|
57
|
+
- Treat this workspace as a single deck project. If the user wants another deck, guide them to create another workspace/folder rather than adding a second deck record.
|
|
57
58
|
- If new evidence conflicts with existing memory, preserve both briefly and add an Open Question instead of silently overwriting.
|
|
58
59
|
|
|
59
60
|
Start now by scanning the workspace.`
|
package/lib/commands/review.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { DECKS_STATE_FILE } from "../decks-state"
|
|
2
2
|
|
|
3
3
|
export function buildReviewPrompt({
|
|
4
|
-
slug,
|
|
5
4
|
exists,
|
|
6
5
|
workspaceRoot,
|
|
7
6
|
}: {
|
|
8
|
-
slug?: string
|
|
9
7
|
exists: boolean
|
|
10
8
|
workspaceRoot?: string
|
|
11
9
|
}): string {
|
|
12
|
-
const target = slug?.trim()
|
|
13
|
-
const deckTarget = target ? `the deck slug or output path matching \`${target}\`` : "the current active deck"
|
|
14
10
|
const state = exists
|
|
15
11
|
? `${DECKS_STATE_FILE} exists. Read it through the revela-decks tool.`
|
|
16
12
|
: `${DECKS_STATE_FILE} does not exist yet. Create it through the revela-decks tool if there is enough deck context.`
|
|
@@ -18,7 +14,7 @@ export function buildReviewPrompt({
|
|
|
18
14
|
return `Review Revela deck write readiness.
|
|
19
15
|
|
|
20
16
|
Goal:
|
|
21
|
-
- Use ${DECKS_STATE_FILE} as the source of truth for whether
|
|
17
|
+
- Use ${DECKS_STATE_FILE} as the source of truth for whether the current workspace deck is ready to be written to \`decks/*.html\`.
|
|
22
18
|
- Preserve the deck spec for future sessions: every slide's content, layout, components, evidence, visuals, and production status.
|
|
23
19
|
- Do not write, patch, or directly edit ${DECKS_STATE_FILE}. Use the \`revela-decks\` tool for all state changes.
|
|
24
20
|
- Let \`revela-decks\` action \`review\` compute writeReadiness; do not manually set readiness to ready.
|
|
@@ -34,11 +30,11 @@ Workspace boundary rules:
|
|
|
34
30
|
- For Glob/file searches, use the current workspace as the search root. Do not set the search root to a parent directory or home directory.
|
|
35
31
|
|
|
36
32
|
Workflow:
|
|
37
|
-
1. Call \`revela-decks\` with action \`read\` for
|
|
38
|
-
2. If no
|
|
33
|
+
1. Call \`revela-decks\` with action \`read\` for the current workspace deck.
|
|
34
|
+
2. If no current deck exists but the conversation contains enough deck context, call \`revela-decks\` action \`upsertDeck\` with goal, outputPath, theme, requiredInputs, and researchPlan. Do not invent or ask for a deck key; the tool uses the workspace folder name internally.
|
|
39
35
|
3. If a user-confirmed slide plan is available, call \`revela-decks\` action \`upsertSlides\` with every slide's title, purpose, layout, components, structured content, evidence, visuals, and status.
|
|
40
36
|
4. Only set requiredInputs fields true when explicit conversation state, files read, research findings read, selected design, fetched layouts/components, or user confirmation supports them. Do not infer completion.
|
|
41
|
-
5. Call \`revela-decks\` action \`review
|
|
37
|
+
5. Call \`revela-decks\` action \`review\`. The tool computes and writes \`writeReadiness\` for the current workspace deck.
|
|
42
38
|
6. Briefly report whether the deck is ready. If blocked, list the exact blockers returned by the tool.
|
|
43
39
|
|
|
44
40
|
Minimum conditions for \`ready\`:
|
|
@@ -53,6 +49,7 @@ Minimum conditions for \`ready\`:
|
|
|
53
49
|
|
|
54
50
|
Rules:
|
|
55
51
|
- Do not write or overwrite \`decks/*.html\` during review.
|
|
52
|
+
- Treat the workspace as one deck project. If the user wants another deck, tell them to use a separate workspace/folder.
|
|
56
53
|
- Do not write, patch, or directly edit ${DECKS_STATE_FILE}; use \`revela-decks\`.
|
|
57
54
|
- Do not store secrets, credentials, tokens, or sensitive personal information.
|
|
58
55
|
- Do not add inferred user preferences to long-term preference state.
|
package/lib/decks-state.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
|
|
2
|
-
import { basename, dirname, join } from "path"
|
|
2
|
+
import { basename, dirname, join, resolve } from "path"
|
|
3
3
|
|
|
4
4
|
export const DECKS_STATE_FILE = "DECKS.json"
|
|
5
5
|
|
|
@@ -154,6 +154,29 @@ export function createEmptyDecksState(): DecksState {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
export function workspaceDeckSlug(workspaceRoot: string): string {
|
|
158
|
+
return normalizeSlug(basename(resolve(workspaceRoot)) || "deck") || "deck"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function normalizeWorkspaceDeckState(state: DecksState, workspaceRoot: string): DecksState {
|
|
162
|
+
const normalized = normalizeDecksState(state)
|
|
163
|
+
const keys = Object.keys(normalized.decks)
|
|
164
|
+
if (keys.length !== 1) return normalized
|
|
165
|
+
|
|
166
|
+
const slug = workspaceDeckSlug(workspaceRoot)
|
|
167
|
+
const existingKey = keys[0]
|
|
168
|
+
if (existingKey === slug) {
|
|
169
|
+
normalized.activeDeck = slug
|
|
170
|
+
return normalized
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const deck = createDeckSpec({ ...normalized.decks[existingKey], slug })
|
|
174
|
+
delete normalized.decks[existingKey]
|
|
175
|
+
normalized.decks[slug] = deck
|
|
176
|
+
normalized.activeDeck = slug
|
|
177
|
+
return normalized
|
|
178
|
+
}
|
|
179
|
+
|
|
157
180
|
export function defaultRequiredInputs(overrides?: Partial<RequiredInputs>): RequiredInputs {
|
|
158
181
|
return {
|
|
159
182
|
topicClarified: false,
|
|
@@ -210,6 +233,10 @@ export function readOrCreateDecksState(workspaceRoot: string): DecksState {
|
|
|
210
233
|
export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug: string }): DecksState {
|
|
211
234
|
const normalized = normalizeDecksState(state)
|
|
212
235
|
const slug = normalizeSlug(input.slug)
|
|
236
|
+
const existingKey = currentDeckKey(normalized)
|
|
237
|
+
if (existingKey && slug !== existingKey && !normalized.decks[slug]) {
|
|
238
|
+
throw new Error(`${DECKS_STATE_FILE} already has a current deck (${existingKey}). Use a separate workspace for another deck.`)
|
|
239
|
+
}
|
|
213
240
|
const existing = normalized.decks[slug]
|
|
214
241
|
const next = createDeckSpec({ ...existing, ...input, slug })
|
|
215
242
|
normalized.decks[slug] = next
|
|
@@ -220,6 +247,10 @@ export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug:
|
|
|
220
247
|
export function upsertSlides(state: DecksState, slug: string, slides: SlideSpec[]): DecksState {
|
|
221
248
|
const normalized = normalizeDecksState(state)
|
|
222
249
|
const key = normalizeSlug(slug)
|
|
250
|
+
const existingKey = currentDeckKey(normalized)
|
|
251
|
+
if (existingKey && key !== existingKey && !normalized.decks[key]) {
|
|
252
|
+
throw new Error(`${DECKS_STATE_FILE} already has a current deck (${existingKey}). Use a separate workspace for another deck.`)
|
|
253
|
+
}
|
|
223
254
|
const deck = normalized.decks[key] ?? createDeckSpec({ slug: key })
|
|
224
255
|
const byIndex = new Map(deck.slides.map((slide) => [slide.index, slide]))
|
|
225
256
|
for (const slide of normalizeSlides(slides)) byIndex.set(slide.index, slide)
|
|
@@ -231,7 +262,7 @@ export function upsertSlides(state: DecksState, slug: string, slides: SlideSpec[
|
|
|
231
262
|
|
|
232
263
|
export function reviewDeckState(state: DecksState, slug?: string): { state: DecksState; result: DeckStateReadinessResult } {
|
|
233
264
|
const normalized = normalizeDecksState(state)
|
|
234
|
-
const key = normalizeSlug(slug || normalized
|
|
265
|
+
const key = normalizeSlug(slug || currentDeckKey(normalized) || "")
|
|
235
266
|
const deck = key ? normalized.decks[key] : undefined
|
|
236
267
|
if (!deck) {
|
|
237
268
|
const missing = key || "active deck"
|
|
@@ -276,13 +307,14 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
276
307
|
const targetPath = normalizeDeckPath(filePath)
|
|
277
308
|
const targetSlug = deckSlugFromPath(targetPath)
|
|
278
309
|
const normalized = normalizeDecksState(state)
|
|
279
|
-
const
|
|
310
|
+
const key = currentDeckKey(normalized)
|
|
311
|
+
const deck = key ? normalized.decks[key] : undefined
|
|
280
312
|
if (!deck) {
|
|
281
313
|
return {
|
|
282
314
|
ready: false,
|
|
283
315
|
slug: targetSlug,
|
|
284
|
-
blocker:
|
|
285
|
-
blockers: [
|
|
316
|
+
blocker: currentDeckBlocker(normalized),
|
|
317
|
+
blockers: [currentDeckBlocker(normalized)],
|
|
286
318
|
}
|
|
287
319
|
}
|
|
288
320
|
|
|
@@ -324,16 +356,17 @@ export function extractDecksStateTargetsFromPatch(patchText: string): string[] {
|
|
|
324
356
|
export function buildDecksStatePromptLayer(workspaceRoot: string, maxChars = 14000): string {
|
|
325
357
|
if (!hasDecksState(workspaceRoot)) return ""
|
|
326
358
|
const state = readDecksState(workspaceRoot)
|
|
327
|
-
const
|
|
359
|
+
const activeKey = currentDeckKey(state)
|
|
360
|
+
const active = activeKey ? state.decks[activeKey] : undefined
|
|
328
361
|
const compact = {
|
|
329
362
|
sourceOfTruth: DECKS_STATE_FILE,
|
|
330
|
-
activeDeck:
|
|
363
|
+
activeDeck: activeKey,
|
|
331
364
|
workspace: state.workspace,
|
|
332
365
|
deck: active,
|
|
333
366
|
}
|
|
334
367
|
let text = JSON.stringify(compact, null, 2)
|
|
335
368
|
if (text.length > maxChars) text = text.slice(0, maxChars).trimEnd() + "\n[DECKS.json state truncated for prompt size.]"
|
|
336
|
-
return `---\n\n# Revela Workspace State From ${DECKS_STATE_FILE}\n\n\`\`\`json\n${text}\n\`\`\`\n\nRules for this state layer:\n- Treat ${DECKS_STATE_FILE} as the source of truth for deck specs, slide
|
|
369
|
+
return `---\n\n# Revela Workspace State From ${DECKS_STATE_FILE}\n\n\`\`\`json\n${text}\n\`\`\`\n\nRules for this state layer:\n- Treat ${DECKS_STATE_FILE} as the source of truth for the single current deck's specs, slide plan, and write readiness.\n- The decks map is compatibility storage; operate only on the current workspace deck.\n- Do not edit ${DECKS_STATE_FILE} directly; use the revela-decks tool.\n- Before writing decks/*.html, the current deck must have writeReadiness.status=ready and a complete slide spec, and its outputPath must match the target file.`
|
|
337
370
|
}
|
|
338
371
|
|
|
339
372
|
function normalizeDecksState(input: DecksState): DecksState {
|
|
@@ -357,9 +390,26 @@ function normalizeDecksState(input: DecksState): DecksState {
|
|
|
357
390
|
state.decks[normalizedSlug] = createDeckSpec({ ...deck, slug: normalizedSlug })
|
|
358
391
|
}
|
|
359
392
|
if (state.activeDeck && !state.decks[state.activeDeck]) state.activeDeck = undefined
|
|
393
|
+
if (!state.activeDeck) {
|
|
394
|
+
const keys = Object.keys(state.decks)
|
|
395
|
+
if (keys.length === 1) state.activeDeck = keys[0]
|
|
396
|
+
}
|
|
360
397
|
return state
|
|
361
398
|
}
|
|
362
399
|
|
|
400
|
+
function currentDeckKey(state: DecksState): string | undefined {
|
|
401
|
+
if (state.activeDeck && state.decks[state.activeDeck]) return state.activeDeck
|
|
402
|
+
const keys = Object.keys(state.decks)
|
|
403
|
+
if (keys.length === 1) return keys[0]
|
|
404
|
+
return undefined
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function currentDeckBlocker(state: DecksState): string {
|
|
408
|
+
const count = Object.keys(state.decks).length
|
|
409
|
+
if (count === 0) return `No current deck exists in ${DECKS_STATE_FILE}. Use revela-decks upsertDeck/upsertSlides/review before writing deck HTML.`
|
|
410
|
+
return `${DECKS_STATE_FILE} contains multiple deck records and no activeDeck. Select one current deck explicitly or move extra decks to separate workspaces.`
|
|
411
|
+
}
|
|
412
|
+
|
|
363
413
|
function computeDeckBlockers(deck: DeckSpec): string[] {
|
|
364
414
|
const blockers: string[] = []
|
|
365
415
|
if (!deck.goal.trim()) blockers.push("Deck goal is missing")
|
|
@@ -402,10 +452,6 @@ function normalizeSlides(slides: SlideSpec[]): SlideSpec[] {
|
|
|
402
452
|
.sort((a, b) => a.index - b.index)
|
|
403
453
|
}
|
|
404
454
|
|
|
405
|
-
function findDeckForTarget(state: DecksState, targetPath: string, targetSlug: string): DeckSpec | undefined {
|
|
406
|
-
return Object.values(state.decks).find((deck) => normalizeDeckPath(deck.outputPath) === targetPath) ?? state.decks[normalizeSlug(targetSlug)]
|
|
407
|
-
}
|
|
408
|
-
|
|
409
455
|
function hasSlideContent(slide: SlideSpec): boolean {
|
|
410
456
|
const content = slide.content ?? {}
|
|
411
457
|
return Boolean(
|
package/lib/design/designs.ts
CHANGED
|
@@ -250,6 +250,20 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
function hasDataAttribute(html: string, attr: string, value: string): boolean {
|
|
254
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
255
|
+
return new RegExp(`${attr}\\s*=\\s*(["'])${escaped}\\1`).test(html)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hasSlideRole(html: string, role: string): boolean {
|
|
259
|
+
const sectionRe = /<section\b[^>]*class\s*=\s*(["'])[^"']*\bslide\b[^"']*\1[^>]*>/gi
|
|
260
|
+
let match: RegExpExecArray | null
|
|
261
|
+
while ((match = sectionRe.exec(html)) !== null) {
|
|
262
|
+
if (hasDataAttribute(match[0], "data-slide-role", role)) return true
|
|
263
|
+
}
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
|
|
253
267
|
/** Validate a local design package for the minimum Revela design contract. */
|
|
254
268
|
export function validateDesignPackage(nameInput: string): ValidateDesignPackageResult {
|
|
255
269
|
let name = nameInput
|
|
@@ -298,6 +312,12 @@ export function validateDesignPackage(nameInput: string): ValidateDesignPackageR
|
|
|
298
312
|
if (!preview.includes('<section class="slide"')) errors.push("preview.html must include slide sections")
|
|
299
313
|
if (!preview.includes("slide-qa=")) errors.push("preview.html slides must include slide-qa attributes")
|
|
300
314
|
if (!preview.includes("slide-canvas")) errors.push("preview.html must include .slide-canvas")
|
|
315
|
+
if (!hasSlideRole(preview, "cover")) errors.push('preview.html must include a slide section with data-slide-role="cover"')
|
|
316
|
+
if (!hasSlideRole(preview, "closing")) errors.push('preview.html must include a slide section with data-slide-role="closing"')
|
|
317
|
+
const missingComponents = components.filter((component) => !hasDataAttribute(preview, "data-preview-component", component))
|
|
318
|
+
if (missingComponents.length > 0) {
|
|
319
|
+
errors.push(`preview.html must showcase every @component; missing: ${missingComponents.join(", ")}`)
|
|
320
|
+
}
|
|
301
321
|
}
|
|
302
322
|
|
|
303
323
|
return {
|
package/lib/edit/deck-state.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { activeDesign } from "../design/designs"
|
|
|
3
3
|
import { activeDomain } from "../domain/domains"
|
|
4
4
|
import {
|
|
5
5
|
defaultRequiredInputs,
|
|
6
|
+
DECKS_STATE_FILE,
|
|
7
|
+
normalizeWorkspaceDeckState,
|
|
6
8
|
readOrCreateDecksState,
|
|
7
9
|
reviewDeckState,
|
|
8
10
|
upsertDeck,
|
|
@@ -19,7 +21,11 @@ export interface EditDeckStatePreflightResult {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export function ensureEditableDeckState(workspaceRoot: string, deck: EditableDeck): EditDeckStatePreflightResult {
|
|
22
|
-
let state = readOrCreateDecksState(workspaceRoot)
|
|
24
|
+
let state = normalizeWorkspaceDeckState(readOrCreateDecksState(workspaceRoot), workspaceRoot)
|
|
25
|
+
const active = state.activeDeck ? state.decks[state.activeDeck] : undefined
|
|
26
|
+
if (active && active.slug !== deck.slug && active.outputPath !== deck.file) {
|
|
27
|
+
throw new Error(`${DECKS_STATE_FILE} already points to ${active.outputPath}. Revela 0.8 expects one deck per workspace; move extra decks to a separate workspace.`)
|
|
28
|
+
}
|
|
23
29
|
const existing = state.decks[deck.slug]
|
|
24
30
|
const existingReady = existing?.writeReadiness?.status === "ready" && existing.writeReadiness.blockers.length === 0
|
|
25
31
|
let changed = !existing || existing.outputPath !== deck.file
|
|
@@ -112,7 +118,7 @@ function safeActiveDesign(): string {
|
|
|
112
118
|
try {
|
|
113
119
|
return activeDesign()
|
|
114
120
|
} catch {
|
|
115
|
-
return "
|
|
121
|
+
return "aurora"
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
|
package/lib/edit/open.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface OpenEditableDeckResult {
|
|
|
14
14
|
source: string
|
|
15
15
|
stateNote: string
|
|
16
16
|
preflightChanged: boolean
|
|
17
|
+
reusedSession: boolean
|
|
18
|
+
liveSession: boolean
|
|
19
|
+
openedBrowser: boolean
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export interface OpenEditableDeckOptions {
|
|
@@ -21,6 +24,11 @@ export interface OpenEditableDeckOptions {
|
|
|
21
24
|
sessionID: string
|
|
22
25
|
workspaceRoot: string
|
|
23
26
|
openBrowser?: boolean
|
|
27
|
+
openUrl?: (url: string) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EnsureEditableDeckOpenResult extends OpenEditableDeckResult {
|
|
31
|
+
skippedReason?: "live-session"
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export function openUrl(url: string): void {
|
|
@@ -41,6 +49,21 @@ export function openUrl(url: string): void {
|
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
export function openEditableDeck(target: string, options: OpenEditableDeckOptions): OpenEditableDeckResult {
|
|
52
|
+
return openEditableDeckInternal(target, options, { skipLiveSession: false })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ensureEditableDeckOpenForChange(
|
|
56
|
+
target: string,
|
|
57
|
+
options: OpenEditableDeckOptions,
|
|
58
|
+
): EnsureEditableDeckOpenResult {
|
|
59
|
+
return openEditableDeckInternal(target, options, { skipLiveSession: true })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function openEditableDeckInternal(
|
|
63
|
+
target: string,
|
|
64
|
+
options: OpenEditableDeckOptions,
|
|
65
|
+
behavior: { skipLiveSession: boolean },
|
|
66
|
+
): EnsureEditableDeckOpenResult {
|
|
44
67
|
const deck = resolveEditableDeck(options.workspaceRoot, target)
|
|
45
68
|
const preflight = ensureEditableDeckState(options.workspaceRoot, deck)
|
|
46
69
|
if (!preflight.readiness.ready) {
|
|
@@ -55,13 +78,14 @@ export function openEditableDeck(target: string, options: OpenEditableDeckOption
|
|
|
55
78
|
}
|
|
56
79
|
|
|
57
80
|
const editServer = startEditServer()
|
|
58
|
-
const
|
|
81
|
+
const session = editServer.getOrCreateSession({
|
|
59
82
|
client: options.client,
|
|
60
83
|
sessionID: options.sessionID,
|
|
61
84
|
deck,
|
|
62
85
|
})
|
|
63
|
-
const url = `${editServer.baseUrl}/edit?token=${encodeURIComponent(token)}`
|
|
64
|
-
|
|
86
|
+
const url = `${editServer.baseUrl}/edit?token=${encodeURIComponent(session.token)}`
|
|
87
|
+
const shouldOpen = options.openBrowser !== false && !(behavior.skipLiveSession && session.live)
|
|
88
|
+
if (shouldOpen) (options.openUrl ?? openUrl)(url)
|
|
65
89
|
|
|
66
90
|
const source = deck.source === "decks-state" ? "DECKS.json" : deck.source === "file-path" ? "file path" : "fallback path"
|
|
67
91
|
const stateNote = preflight.changed ? "Deck state was prepared in DECKS.json before opening the editor." : "Deck state is ready in DECKS.json."
|
|
@@ -72,5 +96,9 @@ export function openEditableDeck(target: string, options: OpenEditableDeckOption
|
|
|
72
96
|
source,
|
|
73
97
|
stateNote,
|
|
74
98
|
preflightChanged: preflight.changed,
|
|
99
|
+
reusedSession: session.reused,
|
|
100
|
+
liveSession: session.live,
|
|
101
|
+
openedBrowser: shouldOpen,
|
|
102
|
+
skippedReason: behavior.skipLiveSession && session.live ? "live-session" : undefined,
|
|
75
103
|
}
|
|
76
104
|
}
|
package/lib/edit/resolve-deck.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync } from "fs"
|
|
2
|
-
import {
|
|
3
|
-
import { DECKS_STATE_FILE,
|
|
1
|
+
import { existsSync, readdirSync } from "fs"
|
|
2
|
+
import { relative, resolve, sep } from "path"
|
|
3
|
+
import { DECKS_STATE_FILE, isDeckHtmlPath, workspaceDeckSlug } from "../decks-state"
|
|
4
4
|
|
|
5
5
|
export interface EditableDeck {
|
|
6
6
|
slug: string
|
|
@@ -9,68 +9,29 @@ export interface EditableDeck {
|
|
|
9
9
|
source: "decks-state" | "fallback" | "file-path"
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function resolveEditableDeck(workspaceRoot: string, input
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const slug = normalizeSlug(requested)
|
|
17
|
-
|
|
18
|
-
if (hasDecksState(workspaceRoot)) {
|
|
19
|
-
const state = readDecksState(workspaceRoot)
|
|
20
|
-
const deck = state.decks[requested] ?? (slug ? state.decks[slug] : undefined)
|
|
21
|
-
if (deck) {
|
|
22
|
-
return resolveDeckFile(workspaceRoot, deck.slug, deck.outputPath, "decks-state")
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (looksLikePath(requested)) {
|
|
27
|
-
return resolvePathTarget(workspaceRoot, requested)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!slug) throw new Error("Deck target must be a deck slug or decks/*.html path.")
|
|
31
|
-
|
|
32
|
-
return resolveDeckFile(workspaceRoot, slug, `decks/${slug}.html`, "fallback")
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function resolveDefaultDeck(workspaceRoot: string): EditableDeck {
|
|
36
|
-
if (!hasDecksState(workspaceRoot)) {
|
|
37
|
-
throw new Error(`No ${DECKS_STATE_FILE} found. Use /revela edit <deck-slug|decks/file.html>.`)
|
|
12
|
+
export function resolveEditableDeck(workspaceRoot: string, input = ""): EditableDeck {
|
|
13
|
+
if (input.trim()) {
|
|
14
|
+
throw new Error("/revela edit no longer accepts a target. It opens the only HTML deck in decks/.")
|
|
38
15
|
}
|
|
39
16
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const deck = state.decks[activeSlug]
|
|
44
|
-
if (!deck) throw new Error(`Active deck ${activeSlug} does not exist in ${DECKS_STATE_FILE}. Use /revela edit <target>.`)
|
|
45
|
-
return resolveDeckFile(workspaceRoot, deck.slug, deck.outputPath, "decks-state")
|
|
17
|
+
const htmlFiles = listDeckHtmlFiles(workspaceRoot)
|
|
18
|
+
if (htmlFiles.length === 0) {
|
|
19
|
+
throw new Error("No deck HTML found in decks/. Generate a deck first.")
|
|
46
20
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (decks.length === 1) {
|
|
50
|
-
const deck = decks[0]
|
|
51
|
-
return resolveDeckFile(workspaceRoot, deck.slug, deck.outputPath, "decks-state")
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (decks.length === 0) {
|
|
55
|
-
throw new Error(`${DECKS_STATE_FILE} has no decks. Use /revela edit <deck-slug|decks/file.html>.`)
|
|
21
|
+
if (htmlFiles.length > 1) {
|
|
22
|
+
throw new Error("This workspace contains multiple deck HTML files. Revela 0.8 expects one deck per workspace. Move extra decks to separate workspaces.")
|
|
56
23
|
}
|
|
57
24
|
|
|
58
|
-
|
|
25
|
+
return resolveDeckFile(workspaceRoot, workspaceDeckSlug(workspaceRoot), htmlFiles[0], "file-path")
|
|
59
26
|
}
|
|
60
27
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
throw new Error("/revela edit file paths must point to decks/*.html.")
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const slug = normalizeSlug(basename(normalized, ".html"))
|
|
72
|
-
if (!slug) throw new Error("Deck target must be a deck slug or decks/*.html path.")
|
|
73
|
-
return resolveDeckFile(workspaceRoot, slug, normalized, "file-path")
|
|
28
|
+
function listDeckHtmlFiles(workspaceRoot: string): string[] {
|
|
29
|
+
const dir = resolve(workspaceRoot, "decks")
|
|
30
|
+
if (!existsSync(dir)) return []
|
|
31
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
32
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".html"))
|
|
33
|
+
.map((entry) => `decks/${entry.name}`)
|
|
34
|
+
.sort((a, b) => a.localeCompare(b))
|
|
74
35
|
}
|
|
75
36
|
|
|
76
37
|
function resolveDeckFile(
|
|
@@ -93,7 +54,7 @@ function resolveDeckFile(
|
|
|
93
54
|
}
|
|
94
55
|
|
|
95
56
|
return {
|
|
96
|
-
slug
|
|
57
|
+
slug,
|
|
97
58
|
file: workspaceRelative(root, absoluteFile),
|
|
98
59
|
absoluteFile,
|
|
99
60
|
source,
|
|
@@ -107,19 +68,3 @@ function isInside(root: string, target: string): boolean {
|
|
|
107
68
|
function workspaceRelative(root: string, target: string): string {
|
|
108
69
|
return relative(root, target).split(sep).join("/")
|
|
109
70
|
}
|
|
110
|
-
|
|
111
|
-
function looksLikePath(value: string): boolean {
|
|
112
|
-
return value.includes("/") || value.includes("\\") || value.endsWith(".html")
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function isAbsoluteLike(value: string): boolean {
|
|
116
|
-
return value.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(value)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function normalizePath(value: string): string {
|
|
120
|
-
return value.replace(/\\/g, "/")
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function normalizeSlug(value: string): string {
|
|
124
|
-
return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
|
|
125
|
-
}
|