@cyber-dash-tech/revela 0.19.8 → 0.19.9

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.
@@ -99,24 +99,24 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
99
99
  linear-gradient(135deg, #07111f, #101a2b 62%, #243a73);
100
100
  }
101
101
 
102
- .template-slide[data-design="starter"][data-template="cover"] .slide-canvas {
102
+ .template-slide[data-template="cover"] .slide-canvas {
103
103
  background:
104
104
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(7,17,31,0.42) 52%, rgba(7,17,31,0.24)),
105
105
  url("./assets/cover-background.jpg") center center / cover no-repeat;
106
106
  }
107
- .template-slide[data-design="starter"][data-template="agenda"] .slide-canvas {
107
+ .template-slide[data-template="agenda"] .slide-canvas {
108
108
  background:
109
109
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(7,17,31,0.58) 52%, rgba(7,17,31,0.32)),
110
110
  url("./assets/cover-background.jpg") center center / cover no-repeat;
111
111
  }
112
- .template-slide[data-design="starter"][data-template="section-divider"] .slide-canvas {
112
+ .template-slide[data-template="section-divider"] .slide-canvas {
113
113
  background:
114
114
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(16,26,43,0.62) 58%, rgba(36,58,115,0.36)),
115
115
  url("./assets/cover-background.jpg") center center / cover no-repeat;
116
116
  }
117
117
  .template-slide[data-template="closing"] .slide-canvas { background: linear-gradient(135deg, #07111f, #315eea 58%, #18a8d8); }
118
118
 
119
- .template-slide[data-design="starter"][data-template="closing"] .slide-canvas {
119
+ .template-slide[data-template="closing"] .slide-canvas {
120
120
  background:
121
121
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(49,94,234,0.42) 58%, rgba(24,168,216,0.24)),
122
122
  url("./assets/closing-background.jpg") center center / cover no-repeat;
@@ -162,10 +162,13 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
162
162
  .text-panel-formula-fallback { display: block; white-space: normal; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82em; line-height: 1.35; color: inherit; }
163
163
  .text-panel-formula-caption { margin: 0; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
164
164
  .template-chart-takeaway-list { display: grid; gap: 22px; width: 100%; }
165
- .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid rgba(255,255,255,0.24); }
165
+ .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid var(--line); }
166
+ .template-text-panel--color .template-chart-takeaway-item { border-top-color: rgba(255,255,255,0.24); }
166
167
  .template-chart-takeaway-item:first-child { padding-top: 0; border-top: 0; }
167
- .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: white; }
168
- .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: rgba(255,255,255,0.78); }
168
+ .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: var(--text-primary); }
169
+ .template-text-panel--color .template-chart-takeaway-item h3 { color: white; }
170
+ .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: var(--text-secondary); }
171
+ .template-text-panel--color .template-chart-takeaway-item p { color: rgba(255,255,255,0.78); }
169
172
  .template-bar { flex: 1; background: linear-gradient(180deg, var(--accent-primary), var(--accent-cyan)); min-height: 80px; }
170
173
  .template-table-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 34px; height: 100%; align-items: stretch; }
171
174
  .template-table-layout .template-side-panel { grid-column: 1; grid-row: 1; }
@@ -333,14 +336,14 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
333
336
  .template-table,
334
337
  .template-insight-panel,
335
338
  .template-catalog-panel { box-shadow: none; }
336
- .template-slide[data-design="starter"][data-template="cover"] .slide-canvas,
337
- .template-slide[data-design="starter"][data-template="agenda"] .slide-canvas,
338
- .template-slide[data-design="starter"][data-template="section-divider"] .slide-canvas {
339
+ .template-slide[data-template="cover"] .slide-canvas,
340
+ .template-slide[data-template="agenda"] .slide-canvas,
341
+ .template-slide[data-template="section-divider"] .slide-canvas {
339
342
  background:
340
343
  linear-gradient(90deg, rgba(16,19,22,0.82), rgba(16,19,22,0.42) 52%, rgba(16,19,22,0.18)),
341
344
  url("./assets/cover-background.jpg") center center / cover no-repeat;
342
345
  }
343
- .template-slide[data-design="starter"][data-template="closing"] .slide-canvas {
346
+ .template-slide[data-template="closing"] .slide-canvas {
344
347
  background:
345
348
  linear-gradient(90deg, rgba(16,19,22,0.72), rgba(47,115,218,0.34) 58%, rgba(43,159,195,0.22)),
346
349
  url("./assets/closing-background.jpg") center center / cover no-repeat;
@@ -99,24 +99,24 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
99
99
  linear-gradient(135deg, #07111f, #101a2b 62%, #243a73);
100
100
  }
101
101
 
102
- .template-slide[data-design="summit"][data-template="cover"] .slide-canvas {
102
+ .template-slide[data-template="cover"] .slide-canvas {
103
103
  background:
104
104
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(7,17,31,0.42) 52%, rgba(7,17,31,0.24)),
105
105
  url("./assets/cover-background.jpg") center center / cover no-repeat;
106
106
  }
107
- .template-slide[data-design="summit"][data-template="agenda"] .slide-canvas {
107
+ .template-slide[data-template="agenda"] .slide-canvas {
108
108
  background:
109
109
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(7,17,31,0.58) 52%, rgba(7,17,31,0.32)),
110
110
  url("./assets/cover-background.jpg") center center / cover no-repeat;
111
111
  }
112
- .template-slide[data-design="summit"][data-template="section-divider"] .slide-canvas {
112
+ .template-slide[data-template="section-divider"] .slide-canvas {
113
113
  background:
114
114
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(16,26,43,0.62) 58%, rgba(36,58,115,0.36)),
115
115
  url("./assets/cover-background.jpg") center center / cover no-repeat;
116
116
  }
117
117
  .template-slide[data-template="closing"] .slide-canvas { background: linear-gradient(135deg, #07111f, #315eea 58%, #18a8d8); }
118
118
 
119
- .template-slide[data-design="summit"][data-template="closing"] .slide-canvas {
119
+ .template-slide[data-template="closing"] .slide-canvas {
120
120
  background:
121
121
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(49,94,234,0.42) 58%, rgba(24,168,216,0.24)),
122
122
  url("./assets/closing-background.jpg") center center / cover no-repeat;
@@ -162,10 +162,13 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
162
162
  .text-panel-formula-fallback { display: block; white-space: normal; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82em; line-height: 1.35; color: inherit; }
163
163
  .text-panel-formula-caption { margin: 0; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
164
164
  .template-chart-takeaway-list { display: grid; gap: 22px; width: 100%; }
165
- .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid rgba(255,255,255,0.24); }
165
+ .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid var(--line); }
166
+ .template-text-panel--color .template-chart-takeaway-item { border-top-color: rgba(255,255,255,0.24); }
166
167
  .template-chart-takeaway-item:first-child { padding-top: 0; border-top: 0; }
167
- .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: white; }
168
- .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: rgba(255,255,255,0.78); }
168
+ .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: var(--text-primary); }
169
+ .template-text-panel--color .template-chart-takeaway-item h3 { color: white; }
170
+ .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: var(--text-secondary); }
171
+ .template-text-panel--color .template-chart-takeaway-item p { color: rgba(255,255,255,0.78); }
169
172
  .template-bar { flex: 1; background: linear-gradient(180deg, var(--accent-primary), var(--accent-cyan)); min-height: 80px; }
170
173
  .template-table-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 34px; height: 100%; align-items: stretch; }
171
174
  .template-table-layout .template-side-panel { grid-column: 1; grid-row: 1; }
@@ -345,14 +348,14 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
345
348
  .template-timeline-layout .template-text-panel--color {
346
349
  background: linear-gradient(135deg, #176c63 0%, #2a8f84 54%, #b0822e 125%);
347
350
  }
348
- .template-slide[data-design="summit"][data-template="cover"] .slide-canvas,
349
- .template-slide[data-design="summit"][data-template="agenda"] .slide-canvas,
350
- .template-slide[data-design="summit"][data-template="section-divider"] .slide-canvas {
351
+ .template-slide[data-template="cover"] .slide-canvas,
352
+ .template-slide[data-template="agenda"] .slide-canvas,
353
+ .template-slide[data-template="section-divider"] .slide-canvas {
351
354
  background:
352
355
  linear-gradient(90deg, rgba(10,16,16,0.84), rgba(10,16,16,0.46) 52%, rgba(10,16,16,0.18)),
353
356
  url("./assets/cover-background.jpg") center center / cover no-repeat;
354
357
  }
355
- .template-slide[data-design="summit"][data-template="closing"] .slide-canvas {
358
+ .template-slide[data-template="closing"] .slide-canvas {
356
359
  background:
357
360
  linear-gradient(90deg, rgba(10,16,16,0.78), rgba(23,108,99,0.4) 58%, rgba(176,130,46,0.22)),
358
361
  url("./assets/closing-background.jpg") center center / cover no-repeat;
@@ -1,5 +1,5 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "fs"
2
- import { dirname, isAbsolute, normalize, resolve } from "path"
2
+ import { basename, dirname, isAbsolute, normalize, resolve } from "path"
3
3
  import { activeDesign, getDesignSection, materializeDesignCssSnapshot } from "../design/designs"
4
4
 
5
5
  export type DeckFoundationMode = "create" | "repair"
@@ -48,7 +48,12 @@ export function createDeckFoundation(input: CreateDeckFoundationInput): CreateDe
48
48
  const foundation = getDesignSection("foundation", design)
49
49
  const parts = parseFoundationParts(foundation)
50
50
  if (parts.scriptBlocks.length === 0) throw new Error(`Design '${design}' foundation does not include a SlidePresentation JavaScript code block.`)
51
- const snapshot = materializeDesignCssSnapshot({ workspaceRoot: input.workspaceRoot, outputPath, designName: design })
51
+ const snapshot = materializeDesignCssSnapshot({
52
+ workspaceRoot: input.workspaceRoot,
53
+ outputPath,
54
+ designName: design,
55
+ snapshotName: activeDesignSnapshotName(outputPath),
56
+ })
52
57
 
53
58
  const html = renderFoundationHtml({
54
59
  language: input.language || "en",
@@ -69,6 +74,7 @@ export function createDeckFoundation(input: CreateDeckFoundationInput): CreateDe
69
74
  "design:foundation",
70
75
  parts.fontLinks.length > 0 ? "foundation:font-links" : "foundation:font-links:none",
71
76
  snapshot.generatedFallback ? "design-css:fallback" : "design-css:snapshot",
77
+ `design-css:active-snapshot:${snapshot.snapshotName}`,
72
78
  snapshot.assetCount > 0 ? "design-assets:snapshot" : "design-assets:none",
73
79
  "foundation:SlidePresentation",
74
80
  ],
@@ -81,6 +87,14 @@ export function createDeckFoundation(input: CreateDeckFoundationInput): CreateDe
81
87
  }
82
88
  }
83
89
 
90
+ export function activeDesignSnapshotName(outputPath: string): string {
91
+ const stem = basename(normalizeOutputPath(outputPath), ".html")
92
+ .toLowerCase()
93
+ .replace(/[^a-z0-9-]+/g, "-")
94
+ .replace(/^-+|-+$/g, "")
95
+ return `${stem || "deck"}-active`
96
+ }
97
+
84
98
  export function normalizeOutputPath(outputPath: string): string {
85
99
  const trimmed = outputPath.trim()
86
100
  if (!trimmed) throw new Error("outputPath is required")
@@ -180,6 +180,7 @@ export interface MaterializeDesignPreviewResult {
180
180
  export interface DesignCssSnapshotResult {
181
181
  ok: true
182
182
  design: string
183
+ snapshotName: string
183
184
  sourcePath: string
184
185
  snapshotDir: string
185
186
  cssPath: string
@@ -425,11 +426,13 @@ export function materializeDesignCssSnapshot(input: {
425
426
  workspaceRoot: string
426
427
  outputPath: string
427
428
  designName?: string
429
+ snapshotName?: string
428
430
  }): DesignCssSnapshotResult {
429
431
  const designName = normalizeDesignName(input.designName || activeDesign())
432
+ const snapshotName = normalizeDesignSnapshotName(input.snapshotName || designName)
430
433
  const designDir = resolveDesignPackageDir(designName)
431
434
  const outputDir = dirname(normalize(input.outputPath))
432
- const snapshotRelDir = normalize(join(outputDir, "_revela-design", designName)).replace(/\\/g, "/")
435
+ const snapshotRelDir = normalize(join(outputDir, "_revela-design", snapshotName)).replace(/\\/g, "/")
433
436
  const snapshotDir = resolve(input.workspaceRoot, snapshotRelDir)
434
437
  const cssPath = join(snapshotDir, "design.css")
435
438
  const cssRead = readDesignCss(designName)
@@ -450,6 +453,7 @@ export function materializeDesignCssSnapshot(input: {
450
453
  return {
451
454
  ok: true,
452
455
  design: designName,
456
+ snapshotName,
453
457
  sourcePath: cssRead.path || join(designDir, "DESIGN.md"),
454
458
  snapshotDir,
455
459
  cssPath,
@@ -460,6 +464,14 @@ export function materializeDesignCssSnapshot(input: {
460
464
  }
461
465
  }
462
466
 
467
+ function normalizeDesignSnapshotName(name: string): string {
468
+ const normalized = name.trim().toLowerCase()
469
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(normalized)) {
470
+ throw new Error("Design snapshot name must be kebab-case using lowercase letters, numbers, and hyphens")
471
+ }
472
+ return normalized
473
+ }
474
+
463
475
  /** Normalize and validate a design package name. */
464
476
  export function normalizeDesignName(name: string): string {
465
477
  const normalized = name.trim().toLowerCase()
@@ -475,23 +475,23 @@ export function templateDeckCss(input: { designName?: string; designAssetBasePat
475
475
  const lucentCoverBackground = designName === "lucent" && assetBasePath ? cssUrl(`${assetBasePath}/cover-background.jpg`) : ""
476
476
  const lucentClosingBackground = designName === "lucent" && assetBasePath ? cssUrl(`${assetBasePath}/closing-background.jpg`) : ""
477
477
  const lucentCoverBackgroundCss = lucentCoverBackground ? `
478
- .template-slide[data-design="lucent"][data-template="cover"] .slide-canvas {
478
+ .template-slide[data-template="cover"] .slide-canvas {
479
479
  background:
480
480
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(7,17,31,0.42) 52%, rgba(7,17,31,0.24)),
481
481
  url("${lucentCoverBackground}") center center / cover no-repeat;
482
482
  }
483
- .template-slide[data-design="lucent"][data-template="agenda"] .slide-canvas {
483
+ .template-slide[data-template="agenda"] .slide-canvas {
484
484
  background:
485
485
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(7,17,31,0.58) 52%, rgba(7,17,31,0.32)),
486
486
  url("${lucentCoverBackground}") center center / cover no-repeat;
487
487
  }
488
- .template-slide[data-design="lucent"][data-template="section-divider"] .slide-canvas {
488
+ .template-slide[data-template="section-divider"] .slide-canvas {
489
489
  background:
490
490
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(16,26,43,0.62) 58%, rgba(36,58,115,0.36)),
491
491
  url("${lucentCoverBackground}") center center / cover no-repeat;
492
492
  }` : ""
493
493
  const lucentClosingBackgroundCss = lucentClosingBackground ? `
494
- .template-slide[data-design="lucent"][data-template="closing"] .slide-canvas {
494
+ .template-slide[data-template="closing"] .slide-canvas {
495
495
  background:
496
496
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(49,94,234,0.42) 58%, rgba(24,168,216,0.24)),
497
497
  url("${lucentClosingBackground}") center center / cover no-repeat;
@@ -591,10 +591,13 @@ ${lucentClosingBackgroundCss}
591
591
  .template-text-panel-formula-caption { margin: 0; font-size: 14px; line-height: 1.35; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
592
592
  .template-text-panel--color .template-text-panel-formula-caption { color: rgba(255,255,255,0.72); }
593
593
  .template-chart-takeaway-list { display: grid; gap: 22px; width: 100%; }
594
- .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid rgba(255,255,255,0.24); }
594
+ .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid var(--line); }
595
+ .template-text-panel--color .template-chart-takeaway-item { border-top-color: rgba(255,255,255,0.24); }
595
596
  .template-chart-takeaway-item:first-child { padding-top: 0; border-top: 0; }
596
- .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: white; }
597
- .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: rgba(255,255,255,0.78); }
597
+ .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: var(--text-primary); }
598
+ .template-text-panel--color .template-chart-takeaway-item h3 { color: white; }
599
+ .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: var(--text-secondary); }
600
+ .template-text-panel--color .template-chart-takeaway-item p { color: rgba(255,255,255,0.78); }
598
601
  .template-bar { flex: 1; background: linear-gradient(180deg, var(--accent-primary), var(--accent-cyan)); min-height: 80px; }
599
602
  .template-table-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 34px; height: 100%; align-items: stretch; }
600
603
  .template-table-layout .template-side-panel { grid-column: 1; grid-row: 1; }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "crypto"
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
3
- import { dirname, resolve } from "path"
2
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs"
3
+ import { dirname, isAbsolute, resolve, sep } from "path"
4
4
  import {
5
5
  activeDesign,
6
6
  activateDesign,
@@ -16,13 +16,14 @@ import {
16
16
  listDesignAssets,
17
17
  listDesigns,
18
18
  materializeDesignPreview,
19
+ materializeDesignCssSnapshot,
19
20
  packDesignPackage,
20
21
  seedBuiltinDesigns,
21
22
  validateDesignDraftPackage,
22
23
  validateDesignPackage,
23
24
  type DesignPackageAssetInput,
24
25
  } from "../design/designs"
25
- import { createDeckFoundation as createDeckFoundationShell } from "../deck-html/foundation"
26
+ import { activeDesignSnapshotName, createDeckFoundation as createDeckFoundationShell } from "../deck-html/foundation"
26
27
  import { activeDomain, activateDomain, createDomainDraftPackage, createDomainPackage, getDomainSkillMd, installDomainDraftPackage, listDomains, seedBuiltinDomains, validateDomainDraftPackage, validateDomainPackage } from "../domain/domains"
27
28
  import { compileNarrativeVault } from "../narrative-vault/compile"
28
29
  import { autoCompileNarrativeVault } from "../narrative-vault/auto-compile"
@@ -36,6 +37,8 @@ import { recordRenderedArtifact, workspaceRelative } from "../workspace-state/re
36
37
  import { existingWorkspaceMetaPath, workspaceMetaPath } from "../workspace-meta"
37
38
  import { checkMaterialIntake, extractMaterial, materialIntakeNoticeForCommand, prepareLocalMaterials, recordMaterialReview } from "../material-intake"
38
39
  import type { ReviewDeckOpenInput, ReviewDeckReadInput } from "./review"
40
+ export type { OpenDeckInput } from "./open-deck"
41
+ import { openDeck as openDeckDirect } from "./open-deck"
39
42
  import pkg from "../../package.json"
40
43
  export { bindResearchFindings, evaluateResearchFindings, researchSave, researchTargets } from "./research"
41
44
  export { storyRead } from "./story"
@@ -72,6 +75,12 @@ export interface RuntimeDesignInventoryInput {
72
75
  name?: string
73
76
  }
74
77
 
78
+ export interface RuntimeDeckDesignSwitchInput extends RuntimeWorkspaceInput {
79
+ file: string
80
+ name: string
81
+ openBrowser?: boolean
82
+ }
83
+
75
84
  export interface RuntimeDesignLayoutReadInput {
76
85
  name?: string
77
86
  layout: string | string[]
@@ -380,6 +389,16 @@ export async function reviewDeckOpen(input: ReviewDeckOpenInput) {
380
389
  return review.reviewDeckOpen(input)
381
390
  }
382
391
 
392
+ export async function openDeck(input: RuntimeFileInput & { openBrowser?: boolean; openUrl?: (url: string) => void }) {
393
+ const direct = await import("./open-deck")
394
+ return direct.openDeck(input)
395
+ }
396
+
397
+ export async function stopOpenDeckServers() {
398
+ const direct = await import("./open-deck")
399
+ return direct.stopOpenDeckServers()
400
+ }
401
+
383
402
  export function designList() {
384
403
  return {
385
404
  ok: true,
@@ -586,6 +605,50 @@ export function designActivate(input: RuntimeNameInput) {
586
605
  }
587
606
  }
588
607
 
608
+ export function switchDeckDesign(input: RuntimeDeckDesignSwitchInput) {
609
+ seedBuiltinDesigns()
610
+ const workspaceRoot = root(input.workspaceRoot)
611
+ const file = normalizeDeckFile(workspaceRoot, requiredString(input.file, "file"))
612
+ const design = requiredName({ name: input.name }, "design")
613
+ activateDesign(design)
614
+
615
+ const snapshot = materializeDesignCssSnapshot({
616
+ workspaceRoot,
617
+ outputPath: file,
618
+ designName: design,
619
+ snapshotName: activeDesignSnapshotName(file),
620
+ })
621
+
622
+ const htmlPath = resolve(workspaceRoot, file)
623
+ const html = readFileSync(htmlPath, "utf-8")
624
+ const migratedHtml = migrateDeckDesignLink(html, snapshot.href)
625
+ const migratedLink = migratedHtml !== html
626
+ if (migratedLink) writeFileSync(htmlPath, migratedHtml, "utf-8")
627
+
628
+ const opened = input.openBrowser === false
629
+ ? undefined
630
+ : openDeckDirect({ workspaceRoot, file, openBrowser: input.openBrowser })
631
+
632
+ return {
633
+ ok: true,
634
+ file,
635
+ activeDesign: activeDesign(),
636
+ design,
637
+ snapshotHref: snapshot.href,
638
+ snapshotDir: workspaceRelative(workspaceRoot, snapshot.snapshotDir),
639
+ assetCount: snapshot.assetCount,
640
+ migratedLink,
641
+ opened: opened ? {
642
+ ok: opened.ok,
643
+ url: opened.url,
644
+ openedBrowser: opened.openedBrowser,
645
+ mode: opened.mode,
646
+ readOnly: opened.readOnly,
647
+ } : undefined,
648
+ warnings: snapshot.warnings,
649
+ }
650
+ }
651
+
589
652
  export function domainList() {
590
653
  seedBuiltinDomains()
591
654
  return {
@@ -659,6 +722,35 @@ function root(workspaceRoot: string | undefined): string {
659
722
  return resolve(workspaceRoot || process.cwd())
660
723
  }
661
724
 
725
+ function normalizeDeckFile(workspaceRoot: string, fileInput: string): string {
726
+ const requested = fileInput.trim()
727
+ const absolute = isAbsolute(requested) ? resolve(requested) : resolve(workspaceRoot, requested)
728
+ if (!isInside(workspaceRoot, absolute)) throw new Error(`Deck HTML file is outside the workspace: ${requested}`)
729
+ if (!existsSync(absolute) || !statSync(absolute).isFile()) throw new Error(`Deck HTML file not found: ${requested}`)
730
+ const file = workspaceRelative(workspaceRoot, absolute)
731
+ if (!file.startsWith("decks/") || !file.endsWith(".html")) throw new Error(`Deck HTML file must be under decks/*.html: ${file}`)
732
+ return file
733
+ }
734
+
735
+ function migrateDeckDesignLink(html: string, activeHref: string): string {
736
+ const escapedHref = activeHref.replace(/"/g, """)
737
+ if (html.includes(`href="${escapedHref}"`) || html.includes(`href='${escapedHref}'`)) return html
738
+ const linkRe = /<link\b(?=[^>]*\brel=(["'])stylesheet\1)(?=[^>]*\bhref=(["'])\.\/_revela-design\/[^"']+\/design\.css\2)[^>]*>/i
739
+ if (linkRe.test(html)) {
740
+ return html.replace(linkRe, `<link rel="stylesheet" href="${escapedHref}" data-revela-design-link="active">`)
741
+ }
742
+ if (/<\/head>/i.test(html)) {
743
+ return html.replace(/<\/head>/i, ` <link rel="stylesheet" href="${escapedHref}" data-revela-design-link="active">\n</head>`)
744
+ }
745
+ return html
746
+ }
747
+
748
+ function isInside(rootPath: string, candidate: string): boolean {
749
+ const normalizedRoot = resolve(rootPath)
750
+ const normalizedCandidate = resolve(candidate)
751
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : normalizedRoot + sep)
752
+ }
753
+
662
754
  function safe<T>(fn: () => T): T | undefined {
663
755
  try {
664
756
  return fn()
@@ -0,0 +1,190 @@
1
+ import { existsSync, statSync } from "fs"
2
+ import { extname, resolve, sep } from "path"
3
+ import { openUrl as systemOpenUrl } from "../edit/open"
4
+ import { workspaceRelative } from "../workspace-state/rendered-artifacts"
5
+
6
+ export interface OpenDeckInput {
7
+ workspaceRoot?: string
8
+ file: string
9
+ openBrowser?: boolean
10
+ openUrl?: (url: string) => void
11
+ }
12
+
13
+ interface DirectDeckServer {
14
+ server: ReturnType<typeof Bun.serve>
15
+ baseUrl: string
16
+ workspaceRoot: string
17
+ idleTimer?: Timer
18
+ }
19
+
20
+ const servers = new Map<string, DirectDeckServer>()
21
+ const IDLE_STOP_MS = 30 * 60 * 1000
22
+ const FALLBACK_PORT_START = 8765
23
+ const FALLBACK_PORT_END = 8899
24
+
25
+ export function openDeck(input: OpenDeckInput): any {
26
+ const workspaceRoot = resolve(input.workspaceRoot || process.cwd())
27
+ const requestedFile = input.file?.trim()
28
+ if (!requestedFile) {
29
+ return {
30
+ ok: false,
31
+ file: "",
32
+ error: "Missing required file.",
33
+ diagnostics: [{ severity: "error", code: "missing_file", message: "Provide a workspace-relative or absolute deck HTML file." }],
34
+ }
35
+ }
36
+
37
+ const absoluteFile = resolve(workspaceRoot, requestedFile)
38
+ const file = workspaceRelative(workspaceRoot, absoluteFile)
39
+ if (!isInside(workspaceRoot, absoluteFile)) {
40
+ return {
41
+ ok: false,
42
+ file,
43
+ error: `Deck HTML file is outside the workspace: ${file}`,
44
+ diagnostics: [{ severity: "error", code: "file_outside_workspace", message: `Deck HTML file is outside the workspace: ${file}` }],
45
+ }
46
+ }
47
+ if (!existsSync(absoluteFile) || !statSync(absoluteFile).isFile()) {
48
+ return {
49
+ ok: false,
50
+ file,
51
+ error: `Deck HTML file not found: ${file}`,
52
+ diagnostics: [{ severity: "error", code: "file_not_found", message: `Deck HTML file not found: ${file}` }],
53
+ }
54
+ }
55
+ if (!file.startsWith("decks/") || !file.endsWith(".html")) {
56
+ return {
57
+ ok: false,
58
+ file,
59
+ error: `Deck HTML file must be under decks/*.html: ${file}`,
60
+ diagnostics: [{ severity: "error", code: "invalid_deck_path", message: `Deck HTML file must be under decks/*.html: ${file}` }],
61
+ }
62
+ }
63
+
64
+ try {
65
+ const deckServer = startDeckStaticServer(workspaceRoot)
66
+ const url = `${deckServer.baseUrl}/${file.split("/").map(encodeURIComponent).join("/")}`
67
+ const openedBrowser = input.openBrowser !== false
68
+ if (openedBrowser) (input.openUrl ?? systemOpenUrl)(url)
69
+ return {
70
+ ok: true,
71
+ file,
72
+ url,
73
+ serveRoot: workspaceRoot,
74
+ openedBrowser,
75
+ mode: "direct",
76
+ readOnly: true,
77
+ }
78
+ } catch (e) {
79
+ const message = e instanceof Error ? e.message : String(e)
80
+ return {
81
+ ok: false,
82
+ file,
83
+ error: message,
84
+ diagnostics: [{ severity: "error", code: "open_deck_failed", message }],
85
+ }
86
+ }
87
+ }
88
+
89
+ export function stopOpenDeckServers(): void {
90
+ for (const item of servers.values()) {
91
+ if (item.idleTimer) clearTimeout(item.idleTimer)
92
+ item.server.stop()
93
+ }
94
+ servers.clear()
95
+ }
96
+
97
+ function startDeckStaticServer(workspaceRoot: string): DirectDeckServer {
98
+ const existing = servers.get(workspaceRoot)
99
+ if (existing) {
100
+ scheduleIdleStop(existing)
101
+ return existing
102
+ }
103
+
104
+ const server = serveWithFallback(workspaceRoot)
105
+ const item: DirectDeckServer = {
106
+ server,
107
+ baseUrl: `http://127.0.0.1:${server.port}`,
108
+ workspaceRoot,
109
+ }
110
+ ;(server as any).unref?.()
111
+ servers.set(workspaceRoot, item)
112
+ scheduleIdleStop(item)
113
+ return item
114
+ }
115
+
116
+ function serveWithFallback(workspaceRoot: string): ReturnType<typeof Bun.serve> {
117
+ const ports = [0, ...Array.from({ length: FALLBACK_PORT_END - FALLBACK_PORT_START + 1 }, (_, index) => FALLBACK_PORT_START + index)]
118
+ const failures: string[] = []
119
+ for (const port of ports) {
120
+ try {
121
+ return Bun.serve({
122
+ hostname: "127.0.0.1",
123
+ port,
124
+ fetch: (req) => handleStaticRequest(workspaceRoot, req),
125
+ })
126
+ } catch (e) {
127
+ failures.push(`port ${port}: ${e instanceof Error ? e.message : String(e)}`)
128
+ }
129
+ }
130
+ throw new Error(`Failed to start direct deck server. ${failures.slice(0, 3).join(" ")}`)
131
+ }
132
+
133
+ async function handleStaticRequest(workspaceRoot: string, req: Request): Promise<Response> {
134
+ const url = new URL(req.url)
135
+ if (req.method !== "GET" && req.method !== "HEAD") return new Response("Method not allowed", { status: 405 })
136
+ if (url.pathname === "/health") return new Response("ok", { status: 200 })
137
+ const pathPart = decodeURIComponent(url.pathname.replace(/^\/+/, ""))
138
+ if (!pathPart) return new Response("Not found", { status: 404 })
139
+ const absolutePath = resolve(workspaceRoot, pathPart)
140
+ if (!isInside(workspaceRoot, absolutePath)) return new Response("Forbidden", { status: 403 })
141
+ if (!existsSync(absolutePath) || !statSync(absolutePath).isFile()) return new Response("Not found", { status: 404 })
142
+ const file = Bun.file(absolutePath)
143
+ const headers = new Headers({ "content-type": contentType(absolutePath) })
144
+ if (req.method === "HEAD") return new Response(null, { status: 200, headers })
145
+ return new Response(file, { headers })
146
+ }
147
+
148
+ function scheduleIdleStop(item: DirectDeckServer): void {
149
+ if (item.idleTimer) clearTimeout(item.idleTimer)
150
+ item.idleTimer = setTimeout(() => {
151
+ item.server.stop()
152
+ servers.delete(item.workspaceRoot)
153
+ }, IDLE_STOP_MS)
154
+ }
155
+
156
+ function isInside(root: string, candidate: string): boolean {
157
+ const normalizedRoot = resolve(root)
158
+ const normalizedCandidate = resolve(candidate)
159
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : normalizedRoot + sep)
160
+ }
161
+
162
+ function contentType(filePath: string): string {
163
+ switch (extname(filePath).toLowerCase()) {
164
+ case ".html":
165
+ return "text/html; charset=utf-8"
166
+ case ".css":
167
+ return "text/css; charset=utf-8"
168
+ case ".js":
169
+ return "application/javascript; charset=utf-8"
170
+ case ".json":
171
+ return "application/json; charset=utf-8"
172
+ case ".svg":
173
+ return "image/svg+xml"
174
+ case ".png":
175
+ return "image/png"
176
+ case ".jpg":
177
+ case ".jpeg":
178
+ return "image/jpeg"
179
+ case ".webp":
180
+ return "image/webp"
181
+ case ".gif":
182
+ return "image/gif"
183
+ case ".woff":
184
+ return "font/woff"
185
+ case ".woff2":
186
+ return "font/woff2"
187
+ default:
188
+ return "application/octet-stream"
189
+ }
190
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.19.8",
3
+ "version": "0.19.9",
4
4
  "description": "Codex-first CLI/MCP workspace for trusted narrative artifacts from local sources, research, and evidence",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "revela",
3
- "version": "0.19.7",
4
- "description": "Use Revela in Codex to specify, research, plan, make, and export trusted narrative decision artifacts.",
3
+ "version": "0.19.9",
4
+ "description": "Use Revela in Codex to specify, research, plan, make, review, and export trusted narrative decision artifacts.",
5
5
  "author": {
6
6
  "name": "cyber-dash-tech",
7
7
  "url": "https://github.com/cyber-dash-tech"
@@ -20,7 +20,7 @@
20
20
  "interface": {
21
21
  "displayName": "Revela",
22
22
  "shortDescription": "Trusted narrative artifacts from local sources and research.",
23
- "longDescription": "Revela helps Codex route workspace workflows, capture requirements in spec.md, ingest local materials, save research findings, plan decks, generate HTML deck artifacts, open them in Codex Browser for annotation, and export PDF/PPTX/PNG outputs while preserving source traceability.",
23
+ "longDescription": "Revela helps Codex route workspace workflows, capture requirements in spec.md, ingest local materials, save research findings, plan decks, generate HTML deck artifacts, open them directly in Codex Browser, and export PDF/PPTX/PNG outputs while preserving source traceability.",
24
24
  "developerName": "cyber-dash-tech",
25
25
  "category": "Productivity",
26
26
  "capabilities": [
@@ -31,6 +31,7 @@
31
31
  "Use Revela to route the next workflow step for this workspace.",
32
32
  "Use Revela to write a spec.md for this deck objective.",
33
33
  "Use Revela to make a deck from the deck plan.",
34
+ "Use Revela to review this deck in Codex Browser.",
34
35
  "Use Revela to export this deck artifact."
35
36
  ],
36
37
  "brandColor": "#2563EB"