@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.
- package/README.md +37 -54
- package/README.zh-CN.md +36 -53
- package/designs/lucent/design.css +10 -7
- package/designs/lucent-dark/design.css +16 -13
- package/designs/monet/design.css +38 -12
- package/designs/starter/design.css +14 -11
- package/designs/summit/design.css +14 -11
- package/lib/deck-html/foundation.ts +16 -2
- package/lib/design/designs.ts +13 -1
- package/lib/page-templates/render.ts +10 -7
- package/lib/runtime/index.ts +95 -3
- package/lib/runtime/open-deck.ts +190 -0
- package/package.json +1 -1
- package/plugins/revela/.codex-plugin/plugin.json +4 -3
- package/plugins/revela/mcp/revela-server.ts +17 -6
- package/plugins/revela/skills/revela/SKILL.md +1 -1
- package/plugins/revela/skills/revela-design/SKILL.md +5 -4
- package/plugins/revela/skills/revela-helper/SKILL.md +1 -1
- package/plugins/revela/skills/revela-review/SKILL.md +57 -0
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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:
|
|
168
|
-
.template-chart-takeaway-item
|
|
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-
|
|
337
|
-
.template-slide[data-
|
|
338
|
-
.template-slide[data-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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:
|
|
168
|
-
.template-chart-takeaway-item
|
|
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-
|
|
349
|
-
.template-slide[data-
|
|
350
|
-
.template-slide[data-
|
|
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-
|
|
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({
|
|
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")
|
package/lib/design/designs.ts
CHANGED
|
@@ -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",
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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:
|
|
597
|
-
.template-chart-takeaway-item
|
|
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; }
|
package/lib/runtime/index.ts
CHANGED
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "revela",
|
|
3
|
-
"version": "0.19.
|
|
4
|
-
|
|
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
|
|
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"
|