@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.
@@ -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, deck history, active deck specs, slide plans, and open questions for future sessions.
20
- - Do not treat initialization as permission to write a slide deck; each deck must pass a later readiness review.
21
- - ${DECKS_STATE_FILE} is the source of truth for Revela workspace state.
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. Record stable history, themes, filenames, and obvious reuse opportunities without treating them as authoritative source data.
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.`
@@ -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 ${deckTarget} is ready to be written to \`decks/*.html\`.
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 ${deckTarget}.
38
- 2. If no matching deck exists but the conversation contains enough deck context, call \`revela-decks\` action \`upsertDeck\` with slug, goal, outputPath, theme, requiredInputs, and researchPlan.
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\` for the slug. The tool computes and writes \`writeReadiness\`.
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.
@@ -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.activeDeck || "")
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 deck = findDeckForTarget(normalized, targetPath, targetSlug)
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: `No deck in ${DECKS_STATE_FILE} matches ${targetPath}. Use revela-decks upsertDeck/upsertSlides, then review before writing.`,
285
- blockers: [`No deck in ${DECKS_STATE_FILE} matches ${targetPath}.`],
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 active = state.activeDeck ? state.decks[state.activeDeck] : undefined
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: state.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 plans, and write readiness.\n- Do not edit ${DECKS_STATE_FILE} directly; use the revela-decks tool.\n- Before writing decks/*.html, the matching deck must have writeReadiness.status=ready and a complete slide spec.`
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(
@@ -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 {
@@ -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 "summit"
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 token = editServer.createSession({
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
- if (options.openBrowser !== false) openUrl(url)
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
  }
@@ -1,6 +1,6 @@
1
- import { existsSync } from "fs"
2
- import { basename, relative, resolve, sep } from "path"
3
- import { DECKS_STATE_FILE, hasDecksState, isDeckHtmlPath, readDecksState } from "../decks-state"
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: string): EditableDeck {
13
- const requested = input.trim()
14
- if (!requested) return resolveDefaultDeck(workspaceRoot)
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 state = readDecksState(workspaceRoot)
41
- const activeSlug = normalizeSlug(state.activeDeck || "")
42
- if (activeSlug) {
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
- const decks = Object.values(state.decks)
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
- throw new Error(`${DECKS_STATE_FILE} has multiple decks and no activeDeck. Use /revela edit <target>.`)
25
+ return resolveDeckFile(workspaceRoot, workspaceDeckSlug(workspaceRoot), htmlFiles[0], "file-path")
59
26
  }
60
27
 
61
- function resolvePathTarget(workspaceRoot: string, requested: string): EditableDeck {
62
- if (isAbsoluteLike(requested)) {
63
- throw new Error("/revela edit only accepts workspace-relative decks/*.html paths.")
64
- }
65
-
66
- const normalized = normalizePath(requested).replace(/^\.\//, "")
67
- if (!isDeckHtmlPath(normalized)) {
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: normalizeSlug(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
- }