@cyber-dash-tech/revela 0.18.9 → 0.18.11
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 +4 -4
- package/README.zh-CN.md +4 -4
- package/bin/revela.ts +2 -0
- package/lib/design/archive.ts +167 -0
- package/lib/design/designs.ts +224 -4
- package/lib/runtime/index.ts +49 -0
- package/package.json +1 -1
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/mcp/revela-server.ts +46 -0
- package/plugins/revela/skills/revela-design/SKILL.md +21 -3
- package/plugins/revela/skills/revela-spec/SKILL.md +9 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**English** | [中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](tests/) [](https://opencode.ai) [](https://bun.sh)
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="assets/img/logo.png" alt="Revela" width="560" />
|
|
@@ -34,7 +34,7 @@ To install globally, add the same entry to `~/.config/opencode/opencode.json`.
|
|
|
34
34
|
Requirements:
|
|
35
35
|
|
|
36
36
|
- The Codex CLI must be installed and the `codex` command must be available in your shell.
|
|
37
|
-
- Your environment must be able to run `npx`; Revela uses `npx -y @cyber-dash-tech/revela@0.18.
|
|
37
|
+
- Your environment must be able to run `npx`; Revela uses `npx -y @cyber-dash-tech/revela@0.18.11 mcp` to start the MCP server.
|
|
38
38
|
- For interactive Review Apply actions, `codex exec` must also work because the Review UI uses it after saved comments are applied.
|
|
39
39
|
|
|
40
40
|
Optional preflight:
|
|
@@ -55,11 +55,11 @@ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
|
|
|
55
55
|
Install Revela through the Codex Git marketplace:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.
|
|
58
|
+
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.11
|
|
59
59
|
codex plugin add revela@revela
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
The Git marketplace install provides the Codex plugin shell, skills, hooks, and MCP configuration. When Codex starts the Revela MCP server for the first time, it runs `npx -y @cyber-dash-tech/revela@0.18.
|
|
62
|
+
The Git marketplace install provides the Codex plugin shell, skills, hooks, and MCP configuration. When Codex starts the Revela MCP server for the first time, it runs `npx -y @cyber-dash-tech/revela@0.18.11 mcp` so npm can fetch the published package and its dependencies.
|
|
63
63
|
|
|
64
64
|
You do not need to run `bun install` inside the Codex marketplace clone.
|
|
65
65
|
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | **中文**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](tests/) [](https://opencode.ai) [](https://bun.sh)
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="assets/img/logo.png" alt="Revela" width="560" />
|
|
@@ -34,7 +34,7 @@ Revela 可在 [OpenCode](https://opencode.ai) 和 Codex 中使用,把来源材
|
|
|
34
34
|
环境要求:
|
|
35
35
|
|
|
36
36
|
- 需要已安装 Codex CLI,并且 shell 中可以执行 `codex`。
|
|
37
|
-
- 环境中需要可以执行 `npx`;Revela 会用 `npx -y @cyber-dash-tech/revela@0.18.
|
|
37
|
+
- 环境中需要可以执行 `npx`;Revela 会用 `npx -y @cyber-dash-tech/revela@0.18.11 mcp` 启动 MCP server。
|
|
38
38
|
- 如果使用 Review UI 的 Apply,需要 `codex exec` 可用;评论会先保存,点击 Apply 后才执行修复。
|
|
39
39
|
|
|
40
40
|
可选的安装前检查:
|
|
@@ -55,11 +55,11 @@ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
|
|
|
55
55
|
通过 Codex Git marketplace 安装 Revela:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.
|
|
58
|
+
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.11
|
|
59
59
|
codex plugin add revela@revela
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。Codex 第一次启动 Revela MCP server 时,会运行 `npx -y @cyber-dash-tech/revela@0.18.
|
|
62
|
+
Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。Codex 第一次启动 Revela MCP server 时,会运行 `npx -y @cyber-dash-tech/revela@0.18.11 mcp`,由 npm 获取已发布 package 及其 dependencies。
|
|
63
63
|
|
|
64
64
|
不需要在 Codex marketplace clone 里运行 `bun install`。
|
|
65
65
|
|
package/bin/revela.ts
CHANGED
|
@@ -32,6 +32,8 @@ else {
|
|
|
32
32
|
else if (command === "design-use") result = runtime.designActivate(required(options, ["name"]))
|
|
33
33
|
else if (command === "design-create") result = runtime.designCreate(required(options, ["name", "designMd", "previewHtml"]))
|
|
34
34
|
else if (command === "design-validate") result = runtime.designValidate(required(options, ["name"]))
|
|
35
|
+
else if (command === "design-pack") result = runtime.designPack(required(options, ["name"]))
|
|
36
|
+
else if (command === "design-install-archive") result = runtime.designInstallArchive(required(options, ["archivePath"]))
|
|
35
37
|
else if (command === "domain-list") result = runtime.domainList()
|
|
36
38
|
else if (command === "domain-read") result = runtime.domainRead(options)
|
|
37
39
|
else if (command === "domain-use") result = runtime.domainActivate(required(options, ["name"]))
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs"
|
|
2
|
+
import { basename, dirname, join, relative, resolve, sep } from "path"
|
|
3
|
+
import { gunzipSync, gzipSync } from "zlib"
|
|
4
|
+
|
|
5
|
+
export interface TarEntry {
|
|
6
|
+
path: string
|
|
7
|
+
bytes: Buffer
|
|
8
|
+
mode?: number
|
|
9
|
+
mtime?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const BLOCK_SIZE = 512
|
|
13
|
+
|
|
14
|
+
export function writeTarArchive(entries: TarEntry[], targetPath: string, gzip: boolean): void {
|
|
15
|
+
const chunks: Buffer[] = []
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const normalized = safeArchivePath(entry.path)
|
|
18
|
+
const bytes = entry.bytes
|
|
19
|
+
const header = Buffer.alloc(BLOCK_SIZE, 0)
|
|
20
|
+
writeString(header, 0, 100, normalized)
|
|
21
|
+
writeOctal(header, 100, 8, entry.mode ?? 0o644)
|
|
22
|
+
writeOctal(header, 108, 8, 0)
|
|
23
|
+
writeOctal(header, 116, 8, 0)
|
|
24
|
+
writeOctal(header, 124, 12, bytes.byteLength)
|
|
25
|
+
writeOctal(header, 136, 12, Math.floor(entry.mtime ?? Date.now() / 1000))
|
|
26
|
+
header.fill(0x20, 148, 156)
|
|
27
|
+
header[156] = "0".charCodeAt(0)
|
|
28
|
+
writeString(header, 257, 6, "ustar")
|
|
29
|
+
writeString(header, 263, 2, "00")
|
|
30
|
+
const checksum = header.reduce((sum, value) => sum + value, 0)
|
|
31
|
+
writeOctal(header, 148, 8, checksum)
|
|
32
|
+
chunks.push(header, bytes)
|
|
33
|
+
const remainder = bytes.byteLength % BLOCK_SIZE
|
|
34
|
+
if (remainder !== 0) chunks.push(Buffer.alloc(BLOCK_SIZE - remainder, 0))
|
|
35
|
+
}
|
|
36
|
+
chunks.push(Buffer.alloc(BLOCK_SIZE * 2, 0))
|
|
37
|
+
const tar = Buffer.concat(chunks as any)
|
|
38
|
+
mkdirSync(dirname(targetPath), { recursive: true })
|
|
39
|
+
writeFileSync(targetPath, (gzip ? gzipSync(tar as any) : tar) as any)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readTarArchive(archivePath: string): TarEntry[] {
|
|
43
|
+
const raw = readFileSync(archivePath)
|
|
44
|
+
const bytes = (archivePath.endsWith(".gz") || archivePath.endsWith(".tgz") ? gunzipSync(raw as any) : raw) as Buffer
|
|
45
|
+
const entries: TarEntry[] = []
|
|
46
|
+
let offset = 0
|
|
47
|
+
while (offset + BLOCK_SIZE <= bytes.byteLength) {
|
|
48
|
+
const header = bytes.subarray(offset, offset + BLOCK_SIZE)
|
|
49
|
+
offset += BLOCK_SIZE
|
|
50
|
+
if (header.every((value) => value === 0)) break
|
|
51
|
+
const rawPath = readString(header, 0, 100)
|
|
52
|
+
const size = readOctal(header, 124, 12)
|
|
53
|
+
const type = String.fromCharCode(header[156] || 0)
|
|
54
|
+
if (type === "5") {
|
|
55
|
+
const dirPath = rawPath.replace(/\/+$/, "")
|
|
56
|
+
if (dirPath) safeArchivePath(dirPath)
|
|
57
|
+
offset += paddedSize(size)
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
const path = safeArchivePath(rawPath)
|
|
61
|
+
if (type === "2") throw new Error(`Archive symlinks are not supported: ${path}`)
|
|
62
|
+
if (type !== "0" && type !== "\0" && type !== "") {
|
|
63
|
+
offset += paddedSize(size)
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
if (offset + size > bytes.byteLength) throw new Error(`Archive entry is truncated: ${path}`)
|
|
67
|
+
entries.push({ path, bytes: bytes.subarray(offset, offset + size), mode: readOctal(header, 100, 8), mtime: readOctal(header, 136, 12) })
|
|
68
|
+
offset += paddedSize(size)
|
|
69
|
+
}
|
|
70
|
+
return entries
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function collectDirectoryEntries(sourceDir: string, prefix = ""): TarEntry[] {
|
|
74
|
+
const root = resolve(sourceDir)
|
|
75
|
+
const entries: TarEntry[] = []
|
|
76
|
+
walk(root)
|
|
77
|
+
return entries.sort((a, b) => a.path.localeCompare(b.path))
|
|
78
|
+
|
|
79
|
+
function walk(dir: string): void {
|
|
80
|
+
for (const entry of readdirSync(dir).sort()) {
|
|
81
|
+
if (entry === ".DS_Store" || entry.startsWith(".")) continue
|
|
82
|
+
const abs = join(dir, entry)
|
|
83
|
+
const stat = lstatSync(abs)
|
|
84
|
+
if (stat.isSymbolicLink()) throw new Error(`Design archives cannot include symlinks: ${abs}`)
|
|
85
|
+
if (stat.isDirectory()) {
|
|
86
|
+
walk(abs)
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if (!stat.isFile()) continue
|
|
90
|
+
const rel = relative(root, abs).split(sep).join("/")
|
|
91
|
+
entries.push({
|
|
92
|
+
path: safeArchivePath(prefix ? `${prefix}/${rel}` : rel),
|
|
93
|
+
bytes: readFileSync(abs),
|
|
94
|
+
mode: stat.mode & 0o777,
|
|
95
|
+
mtime: Math.floor(stat.mtimeMs / 1000),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function extractEntriesToDirectory(entries: TarEntry[], targetDir: string): string[] {
|
|
102
|
+
const root = resolve(targetDir)
|
|
103
|
+
if (existsSync(root)) rmSync(root, { recursive: true, force: true })
|
|
104
|
+
mkdirSync(root, { recursive: true })
|
|
105
|
+
const files: string[] = []
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const rel = safeArchivePath(entry.path)
|
|
108
|
+
const target = resolve(root, rel)
|
|
109
|
+
if (target !== root && !target.startsWith(root + sep)) throw new Error(`Archive path escapes target directory: ${entry.path}`)
|
|
110
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
111
|
+
writeFileSync(target, entry.bytes as any)
|
|
112
|
+
files.push(rel)
|
|
113
|
+
}
|
|
114
|
+
return files.sort()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function normalizePackageArchiveEntries(entries: TarEntry[]): TarEntry[] {
|
|
118
|
+
const files = entries.filter((entry) => entry.path && !entry.path.endsWith("/"))
|
|
119
|
+
if (files.some((entry) => basename(entry.path) === "DESIGN.md")) {
|
|
120
|
+
if (files.some((entry) => entry.path === "DESIGN.md")) return files
|
|
121
|
+
const top = commonTopLevel(files)
|
|
122
|
+
if (top && files.some((entry) => entry.path === `${top}/DESIGN.md`)) {
|
|
123
|
+
return files.map((entry) => ({ ...entry, path: entry.path.slice(top.length + 1) }))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
throw new Error("No DESIGN.md found in design archive")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function commonTopLevel(entries: TarEntry[]): string | null {
|
|
130
|
+
const first = entries[0]?.path.split("/")[0]
|
|
131
|
+
if (!first || first === entries[0]?.path) return null
|
|
132
|
+
return entries.every((entry) => entry.path.startsWith(`${first}/`)) ? first : null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function safeArchivePath(input: string): string {
|
|
136
|
+
const normalized = input.replace(/\\/g, "/").replace(/^\.\/+/, "")
|
|
137
|
+
if (!normalized || normalized.startsWith("/") || normalized.includes("\0")) throw new Error(`Invalid archive path: ${input}`)
|
|
138
|
+
const parts = normalized.split("/")
|
|
139
|
+
if (parts.some((part) => !part || part === "." || part === "..")) throw new Error(`Invalid archive path: ${input}`)
|
|
140
|
+
if (Buffer.byteLength(normalized) > 100) throw new Error(`Archive path is too long for v1 tar support: ${normalized}`)
|
|
141
|
+
return normalized
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function writeString(buffer: Buffer, offset: number, length: number, value: string): void {
|
|
145
|
+
buffer.write(value, offset, Math.min(length, Buffer.byteLength(value)), "utf8")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readString(buffer: Buffer, offset: number, length: number): string {
|
|
149
|
+
const slice = buffer.subarray(offset, offset + length)
|
|
150
|
+
const end = slice.indexOf(0)
|
|
151
|
+
return slice.subarray(0, end === -1 ? length : end).toString("utf8").trim()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function writeOctal(buffer: Buffer, offset: number, length: number, value: number): void {
|
|
155
|
+
const text = value.toString(8).padStart(length - 1, "0").slice(0, length - 1)
|
|
156
|
+
buffer.write(text, offset, length - 1, "ascii")
|
|
157
|
+
buffer[offset + length - 1] = 0
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readOctal(buffer: Buffer, offset: number, length: number): number {
|
|
161
|
+
const text = readString(buffer, offset, length).replace(/\0/g, "").trim()
|
|
162
|
+
return text ? parseInt(text, 8) : 0
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function paddedSize(size: number): number {
|
|
166
|
+
return Math.ceil(size / BLOCK_SIZE) * BLOCK_SIZE
|
|
167
|
+
}
|
package/lib/design/designs.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import {
|
|
10
10
|
cpSync,
|
|
11
11
|
existsSync,
|
|
12
|
+
lstatSync,
|
|
12
13
|
mkdirSync,
|
|
13
14
|
readdirSync,
|
|
14
15
|
readFileSync,
|
|
@@ -16,9 +17,10 @@ import {
|
|
|
16
17
|
statSync,
|
|
17
18
|
writeFileSync,
|
|
18
19
|
} from "fs"
|
|
19
|
-
import { dirname, join, resolve, basename } from "path"
|
|
20
|
+
import { dirname, join, resolve, basename, relative, sep } from "path"
|
|
20
21
|
import { tmpdir } from "os"
|
|
21
22
|
import { parseFrontmatter } from "../frontmatter"
|
|
23
|
+
import { collectDirectoryEntries, extractEntriesToDirectory, normalizePackageArchiveEntries, readTarArchive, writeTarArchive } from "./archive"
|
|
22
24
|
import {
|
|
23
25
|
DESIGNS_DIR,
|
|
24
26
|
DEFAULT_DESIGN,
|
|
@@ -50,6 +52,7 @@ export interface CreateDesignPackageArgs {
|
|
|
50
52
|
base?: string
|
|
51
53
|
designMd: string
|
|
52
54
|
previewHtml: string
|
|
55
|
+
assets?: DesignPackageAssetInput[]
|
|
53
56
|
overwrite?: boolean
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -62,6 +65,7 @@ export interface CreateDesignPackageResult {
|
|
|
62
65
|
name: string
|
|
63
66
|
path: string
|
|
64
67
|
files: string[]
|
|
68
|
+
assets: DesignPackageAssetInfo[]
|
|
65
69
|
base?: string
|
|
66
70
|
overwritten: boolean
|
|
67
71
|
}
|
|
@@ -86,9 +90,52 @@ export interface ValidateDesignPackageResult {
|
|
|
86
90
|
sections: string[]
|
|
87
91
|
layouts: string[]
|
|
88
92
|
components: string[]
|
|
93
|
+
assets: DesignPackageAssetInfo[]
|
|
89
94
|
errors: string[]
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
export interface DesignPackageAssetInput {
|
|
98
|
+
path: string
|
|
99
|
+
content?: string
|
|
100
|
+
contentBase64?: string
|
|
101
|
+
sourcePath?: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface DesignPackageAssetInfo {
|
|
105
|
+
path: string
|
|
106
|
+
kind: "cover-background" | "closing-background" | "background" | "logo" | "asset"
|
|
107
|
+
mimeType: string
|
|
108
|
+
bytes: number
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface PackDesignPackageArgs {
|
|
112
|
+
workspaceRoot?: string
|
|
113
|
+
name: string
|
|
114
|
+
source?: "draft" | "installed"
|
|
115
|
+
outputPath?: string
|
|
116
|
+
format?: "tar.gz" | "tar"
|
|
117
|
+
overwrite?: boolean
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface PackDesignPackageResult {
|
|
121
|
+
ok: true
|
|
122
|
+
name: string
|
|
123
|
+
archivePath: string
|
|
124
|
+
format: "tar.gz" | "tar"
|
|
125
|
+
files: string[]
|
|
126
|
+
assets: DesignPackageAssetInfo[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface InstallDesignArchiveArgs {
|
|
130
|
+
archivePath: string
|
|
131
|
+
name?: string
|
|
132
|
+
overwrite?: boolean
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface InstallDesignArchiveResult extends CreateDesignPackageResult {
|
|
136
|
+
archivePath: string
|
|
137
|
+
}
|
|
138
|
+
|
|
92
139
|
export interface DesignPreviewInfo {
|
|
93
140
|
name: string
|
|
94
141
|
designDir: string
|
|
@@ -283,6 +330,7 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
|
|
|
283
330
|
mkdirSync(target, { recursive: true })
|
|
284
331
|
writeFileSync(join(target, "DESIGN.md"), `${designMd}\n`, "utf-8")
|
|
285
332
|
writeFileSync(join(target, "preview.html"), `${previewHtml}\n`, "utf-8")
|
|
333
|
+
writeDesignAssets(target, args.assets)
|
|
286
334
|
|
|
287
335
|
const validation = validateDesignPackage(name)
|
|
288
336
|
if (!validation.ok) {
|
|
@@ -293,7 +341,8 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
|
|
|
293
341
|
ok: true,
|
|
294
342
|
name,
|
|
295
343
|
path: target,
|
|
296
|
-
files:
|
|
344
|
+
files: listDesignPackageFiles(target),
|
|
345
|
+
assets: listDesignAssetsInDir(target),
|
|
297
346
|
base: args.base,
|
|
298
347
|
overwritten: existed,
|
|
299
348
|
}
|
|
@@ -321,6 +370,7 @@ export function createDesignDraftPackage(args: CreateDesignDraftArgs): CreateDes
|
|
|
321
370
|
mkdirSync(target, { recursive: true })
|
|
322
371
|
writeFileSync(join(target, "DESIGN.md"), `${designMd}\n`, "utf-8")
|
|
323
372
|
writeFileSync(join(target, "preview.html"), `${previewHtml}\n`, "utf-8")
|
|
373
|
+
writeDesignAssets(target, args.assets)
|
|
324
374
|
|
|
325
375
|
const validation = validateDesignDraftPackage(args.workspaceRoot, name)
|
|
326
376
|
if (!validation.ok) {
|
|
@@ -331,7 +381,8 @@ export function createDesignDraftPackage(args: CreateDesignDraftArgs): CreateDes
|
|
|
331
381
|
ok: true,
|
|
332
382
|
name,
|
|
333
383
|
path: target,
|
|
334
|
-
files:
|
|
384
|
+
files: listDesignPackageFiles(target),
|
|
385
|
+
assets: listDesignAssetsInDir(target),
|
|
335
386
|
base: args.base,
|
|
336
387
|
overwritten: existed,
|
|
337
388
|
}
|
|
@@ -378,11 +429,74 @@ export function installDesignDraftPackage(args: InstallDesignDraftArgs): Install
|
|
|
378
429
|
name,
|
|
379
430
|
path: target,
|
|
380
431
|
sourcePath,
|
|
381
|
-
files:
|
|
432
|
+
files: listDesignPackageFiles(target),
|
|
433
|
+
assets: listDesignAssetsInDir(target),
|
|
382
434
|
overwritten: existed,
|
|
383
435
|
}
|
|
384
436
|
}
|
|
385
437
|
|
|
438
|
+
/** Package an installed design or workspace draft as a shareable .tar or .tar.gz archive. */
|
|
439
|
+
export function packDesignPackage(args: PackDesignPackageArgs): PackDesignPackageResult {
|
|
440
|
+
const name = normalizeDesignName(args.name)
|
|
441
|
+
const source = args.source ?? (args.workspaceRoot && designDraftExists(args.workspaceRoot, name) ? "draft" : "installed")
|
|
442
|
+
const sourceDir = source === "draft" ? designDraftDir(args.workspaceRoot || process.cwd(), name) : resolveDesignDir(name)
|
|
443
|
+
if (!sourceDir || !existsSync(sourceDir)) throw new Error(`Design ${source} '${name}' is not available`)
|
|
444
|
+
const validation = source === "draft" ? validateDesignDraftPackage(args.workspaceRoot || process.cwd(), name) : validateDesignPackage(name)
|
|
445
|
+
if (!validation.ok) throw new Error(`Design ${source} is invalid: ${validation.errors.join("; ")}`)
|
|
446
|
+
|
|
447
|
+
const format = args.format ?? "tar.gz"
|
|
448
|
+
const archivePath = resolve(args.outputPath || join(args.workspaceRoot || process.cwd(), ".revela", "design-archives", `${name}.${format}`))
|
|
449
|
+
if (existsSync(archivePath) && !args.overwrite) throw new Error(`Archive already exists: ${archivePath}`)
|
|
450
|
+
const entries = collectDirectoryEntries(sourceDir, name)
|
|
451
|
+
writeTarArchive(entries, archivePath, format === "tar.gz")
|
|
452
|
+
return {
|
|
453
|
+
ok: true,
|
|
454
|
+
name,
|
|
455
|
+
archivePath,
|
|
456
|
+
format,
|
|
457
|
+
files: listDesignPackageFiles(sourceDir),
|
|
458
|
+
assets: listDesignAssetsInDir(sourceDir),
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Install a .tar or .tar.gz design archive into the user-level design registry. */
|
|
463
|
+
export function installDesignArchive(args: InstallDesignArchiveArgs): InstallDesignArchiveResult {
|
|
464
|
+
const archivePath = resolve(args.archivePath)
|
|
465
|
+
if (!existsSync(archivePath)) throw new Error(`Archive does not exist: ${archivePath}`)
|
|
466
|
+
if (!archivePath.endsWith(".tar") && !archivePath.endsWith(".tar.gz") && !archivePath.endsWith(".tgz")) {
|
|
467
|
+
throw new Error("Design archive must be .tar, .tar.gz, or .tgz")
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const entries = normalizePackageArchiveEntries(readTarArchive(archivePath))
|
|
471
|
+
const tmp = join(tmpdir(), `revela-design-install-${Date.now()}`)
|
|
472
|
+
try {
|
|
473
|
+
extractEntriesToDirectory(entries, tmp)
|
|
474
|
+
const info = parseDesignFile(join(tmp, "DESIGN.md"))
|
|
475
|
+
const name = normalizeDesignName(args.name || info?.name || basename(archivePath).replace(/\.tar\.gz$|\.tgz$|\.tar$/i, ""))
|
|
476
|
+
const validation = validateDesignPackageAt(name, tmp)
|
|
477
|
+
if (!validation.ok) throw new Error(`Design archive is invalid: ${validation.errors.join("; ")}`)
|
|
478
|
+
|
|
479
|
+
const target = join(DESIGNS_DIR, name)
|
|
480
|
+
const existed = existsSync(target)
|
|
481
|
+
if (existed && !args.overwrite) throw new Error(`Design '${name}' already exists. Pass overwrite=true to replace it.`)
|
|
482
|
+
|
|
483
|
+
mkdirSync(DESIGNS_DIR, { recursive: true })
|
|
484
|
+
if (existed) rmSync(target, { recursive: true, force: true })
|
|
485
|
+
cpSync(tmp, target, { recursive: true })
|
|
486
|
+
return {
|
|
487
|
+
ok: true,
|
|
488
|
+
name,
|
|
489
|
+
path: target,
|
|
490
|
+
archivePath,
|
|
491
|
+
files: listDesignPackageFiles(target),
|
|
492
|
+
assets: listDesignAssetsInDir(target),
|
|
493
|
+
overwritten: existed,
|
|
494
|
+
}
|
|
495
|
+
} finally {
|
|
496
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
386
500
|
function hasDataAttribute(html: string, attr: string, value: string): boolean {
|
|
387
501
|
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
388
502
|
return new RegExp(`${attr}\\s*=\\s*(["'])${escaped}\\1`).test(html)
|
|
@@ -451,10 +565,12 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
451
565
|
let sections: string[] = []
|
|
452
566
|
let layouts: string[] = []
|
|
453
567
|
let components: string[] = []
|
|
568
|
+
let assets: DesignPackageAssetInfo[] = []
|
|
454
569
|
|
|
455
570
|
if (!existsSync(dir)) errors.push(`Design directory does not exist: ${dir}`)
|
|
456
571
|
if (!hasDesignMd) errors.push("DESIGN.md is missing")
|
|
457
572
|
if (!hasPreview) errors.push("preview.html is missing")
|
|
573
|
+
if (existsSync(dir)) assets = listDesignAssetsInDir(dir)
|
|
458
574
|
|
|
459
575
|
if (hasDesignMd) {
|
|
460
576
|
const info = parseDesignFile(mdPath)
|
|
@@ -501,6 +617,7 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
501
617
|
sections,
|
|
502
618
|
layouts,
|
|
503
619
|
components,
|
|
620
|
+
assets,
|
|
504
621
|
errors,
|
|
505
622
|
}
|
|
506
623
|
}
|
|
@@ -509,6 +626,107 @@ function designDraftDir(workspaceRoot: string, name: string): string {
|
|
|
509
626
|
return resolve(workspaceRoot, ".revela", "drafts", "designs", name)
|
|
510
627
|
}
|
|
511
628
|
|
|
629
|
+
function designDraftExists(workspaceRoot: string, name: string): boolean {
|
|
630
|
+
const dir = designDraftDir(workspaceRoot, name)
|
|
631
|
+
return existsSync(dir) && statSync(dir).isDirectory() && existsSync(join(dir, "DESIGN.md"))
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function writeDesignAssets(targetDir: string, assets?: DesignPackageAssetInput[]): void {
|
|
635
|
+
if (!assets || assets.length === 0) return
|
|
636
|
+
for (const asset of assets) {
|
|
637
|
+
const rel = normalizeAssetPath(asset.path)
|
|
638
|
+
const target = resolve(targetDir, rel)
|
|
639
|
+
if (target !== resolve(targetDir) && !target.startsWith(resolve(targetDir) + sep)) {
|
|
640
|
+
throw new Error(`Asset path escapes design package: ${asset.path}`)
|
|
641
|
+
}
|
|
642
|
+
let bytes: Buffer
|
|
643
|
+
if (asset.contentBase64 !== undefined) bytes = Buffer.from(asset.contentBase64, "base64")
|
|
644
|
+
else if (asset.content !== undefined) bytes = Buffer.from(asset.content, "utf-8")
|
|
645
|
+
else if (asset.sourcePath !== undefined) {
|
|
646
|
+
const source = resolve(asset.sourcePath)
|
|
647
|
+
if (!existsSync(source) || !statSync(source).isFile()) throw new Error(`Asset source file does not exist: ${asset.sourcePath}`)
|
|
648
|
+
bytes = readFileSync(source)
|
|
649
|
+
} else {
|
|
650
|
+
throw new Error(`Asset '${asset.path}' requires content, contentBase64, or sourcePath`)
|
|
651
|
+
}
|
|
652
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
653
|
+
writeFileSync(target, bytes as any)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function normalizeAssetPath(pathInput: string): string {
|
|
658
|
+
const normalized = pathInput.replace(/\\/g, "/").replace(/^\.\/+/, "")
|
|
659
|
+
if (!normalized.startsWith("assets/")) throw new Error(`Design asset path must be located under assets/: ${pathInput}`)
|
|
660
|
+
if (normalized.includes("\0") || normalized.startsWith("/") || normalized.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
661
|
+
throw new Error(`Design asset path must be located under assets/ and must not contain absolute paths, empty segments, '.', '..', or NUL characters: ${pathInput}`)
|
|
662
|
+
}
|
|
663
|
+
return normalized
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function listDesignPackageFiles(dir: string): string[] {
|
|
667
|
+
if (!existsSync(dir)) return []
|
|
668
|
+
const root = resolve(dir)
|
|
669
|
+
const files: string[] = []
|
|
670
|
+
walk(root)
|
|
671
|
+
return files.sort()
|
|
672
|
+
|
|
673
|
+
function walk(current: string): void {
|
|
674
|
+
for (const entry of readdirSync(current).sort()) {
|
|
675
|
+
if (entry === ".DS_Store" || entry.startsWith(".")) continue
|
|
676
|
+
const abs = join(current, entry)
|
|
677
|
+
const stat = lstatSync(abs)
|
|
678
|
+
if (stat.isSymbolicLink()) continue
|
|
679
|
+
if (stat.isDirectory()) {
|
|
680
|
+
walk(abs)
|
|
681
|
+
continue
|
|
682
|
+
}
|
|
683
|
+
if (!stat.isFile()) continue
|
|
684
|
+
files.push(relative(root, abs).split(sep).join("/"))
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function listDesignAssetsInDir(dir: string): DesignPackageAssetInfo[] {
|
|
690
|
+
const files = listDesignPackageFiles(dir).filter((file) => file.startsWith("assets/"))
|
|
691
|
+
return files.map((file) => {
|
|
692
|
+
const abs = join(dir, ...file.split("/"))
|
|
693
|
+
return {
|
|
694
|
+
path: file,
|
|
695
|
+
kind: inferAssetKind(file),
|
|
696
|
+
mimeType: mimeTypeForDesignAsset(file),
|
|
697
|
+
bytes: statSync(abs).size,
|
|
698
|
+
}
|
|
699
|
+
})
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export function listDesignAssets(nameInput?: string): DesignPackageAssetInfo[] {
|
|
703
|
+
const name = normalizeDesignName(nameInput || activeDesign())
|
|
704
|
+
const designDir = resolveDesignDir(name)
|
|
705
|
+
if (!designDir) throw new Error(`Design '${name}' is not installed`)
|
|
706
|
+
return listDesignAssetsInDir(designDir)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function inferAssetKind(path: string): DesignPackageAssetInfo["kind"] {
|
|
710
|
+
const lower = path.toLowerCase()
|
|
711
|
+
if (lower.includes("cover") && lower.includes("background")) return "cover-background"
|
|
712
|
+
if ((lower.includes("closing") || lower.includes("close")) && lower.includes("background")) return "closing-background"
|
|
713
|
+
if (lower.includes("background") || lower.includes("/bg-") || lower.includes("-bg.")) return "background"
|
|
714
|
+
if (lower.includes("logo")) return "logo"
|
|
715
|
+
return "asset"
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function mimeTypeForDesignAsset(path: string): string {
|
|
719
|
+
const lower = path.toLowerCase()
|
|
720
|
+
if (lower.endsWith(".png")) return "image/png"
|
|
721
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"
|
|
722
|
+
if (lower.endsWith(".webp")) return "image/webp"
|
|
723
|
+
if (lower.endsWith(".gif")) return "image/gif"
|
|
724
|
+
if (lower.endsWith(".svg")) return "image/svg+xml"
|
|
725
|
+
if (lower.endsWith(".css")) return "text/css"
|
|
726
|
+
if (lower.endsWith(".json")) return "application/json"
|
|
727
|
+
return "application/octet-stream"
|
|
728
|
+
}
|
|
729
|
+
|
|
512
730
|
// ---------------------------------------------------------------------------
|
|
513
731
|
// Marker-based section / component parsing
|
|
514
732
|
// ---------------------------------------------------------------------------
|
|
@@ -553,6 +771,7 @@ export interface DesignInventory {
|
|
|
553
771
|
sections: string[]
|
|
554
772
|
layouts: DesignInventoryLayout[]
|
|
555
773
|
components: DesignInventoryComponent[]
|
|
774
|
+
assets: DesignPackageAssetInfo[]
|
|
556
775
|
hasMarkers: boolean
|
|
557
776
|
}
|
|
558
777
|
|
|
@@ -690,6 +909,7 @@ export function getDesignInventory(designName?: string): DesignInventory {
|
|
|
690
909
|
description: designBlockDescription(content),
|
|
691
910
|
nesting: inferComponentNesting(componentName),
|
|
692
911
|
})),
|
|
912
|
+
assets: listDesignAssetsInDir(designDir),
|
|
693
913
|
hasMarkers,
|
|
694
914
|
}
|
|
695
915
|
}
|
package/lib/runtime/index.ts
CHANGED
|
@@ -11,11 +11,15 @@ import {
|
|
|
11
11
|
getDesignLayout,
|
|
12
12
|
getDesignSection,
|
|
13
13
|
getDesignSkillMd,
|
|
14
|
+
installDesignArchive,
|
|
14
15
|
installDesignDraftPackage,
|
|
16
|
+
listDesignAssets,
|
|
15
17
|
listDesigns,
|
|
18
|
+
packDesignPackage,
|
|
16
19
|
seedBuiltinDesigns,
|
|
17
20
|
validateDesignDraftPackage,
|
|
18
21
|
validateDesignPackage,
|
|
22
|
+
type DesignPackageAssetInput,
|
|
19
23
|
} from "../design/designs"
|
|
20
24
|
import { createDeckFoundation as createDeckFoundationShell } from "../deck-html/foundation"
|
|
21
25
|
import { activeDomain, activateDomain, createDomainDraftPackage, createDomainPackage, getDomainSkillMd, installDomainDraftPackage, listDomains, seedBuiltinDomains, validateDomainDraftPackage, validateDomainPackage } from "../domain/domains"
|
|
@@ -85,6 +89,7 @@ export interface RuntimeDesignCreateInput {
|
|
|
85
89
|
base?: string
|
|
86
90
|
designMd: string
|
|
87
91
|
previewHtml: string
|
|
92
|
+
assets?: DesignPackageAssetInput[]
|
|
88
93
|
overwrite?: boolean
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -103,6 +108,21 @@ export interface RuntimeDraftInstallInput extends RuntimeWorkspaceInput {
|
|
|
103
108
|
overwrite?: boolean
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
export interface RuntimeDesignPackInput extends RuntimeWorkspaceInput {
|
|
112
|
+
name: string
|
|
113
|
+
source?: "draft" | "installed"
|
|
114
|
+
outputPath?: string
|
|
115
|
+
format?: "tar.gz" | "tar"
|
|
116
|
+
overwrite?: boolean
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface RuntimeDesignArchiveInstallInput {
|
|
120
|
+
archivePath: string
|
|
121
|
+
name?: string
|
|
122
|
+
overwrite?: boolean
|
|
123
|
+
activate?: boolean
|
|
124
|
+
}
|
|
125
|
+
|
|
106
126
|
export interface RuntimeNameInput {
|
|
107
127
|
name: string
|
|
108
128
|
}
|
|
@@ -296,6 +316,7 @@ export function designRead(input: RuntimeDesignReadInput = {}) {
|
|
|
296
316
|
name,
|
|
297
317
|
section: input.section,
|
|
298
318
|
markdown,
|
|
319
|
+
assets: listDesignAssets(name),
|
|
299
320
|
}
|
|
300
321
|
if (input.section === "rules") recordDesignRulesRead(root(input.workspaceRoot), name, markdown)
|
|
301
322
|
return result
|
|
@@ -304,6 +325,7 @@ export function designRead(input: RuntimeDesignReadInput = {}) {
|
|
|
304
325
|
ok: true,
|
|
305
326
|
name,
|
|
306
327
|
markdown: getDesignSkillMd(name),
|
|
328
|
+
assets: listDesignAssets(name),
|
|
307
329
|
}
|
|
308
330
|
}
|
|
309
331
|
|
|
@@ -341,6 +363,7 @@ export function designCreate(input: RuntimeDesignCreateInput) {
|
|
|
341
363
|
base: input.base,
|
|
342
364
|
designMd: requiredString(input?.designMd, "designMd"),
|
|
343
365
|
previewHtml: requiredString(input?.previewHtml, "previewHtml"),
|
|
366
|
+
assets: input.assets,
|
|
344
367
|
overwrite: input.overwrite ?? false,
|
|
345
368
|
})
|
|
346
369
|
}
|
|
@@ -356,6 +379,7 @@ export function designDraftCreate(input: RuntimeDesignDraftCreateInput) {
|
|
|
356
379
|
base: input.base,
|
|
357
380
|
designMd: requiredString(input?.designMd, "designMd"),
|
|
358
381
|
previewHtml: requiredString(input?.previewHtml, "previewHtml"),
|
|
382
|
+
assets: input.assets,
|
|
359
383
|
overwrite: input.overwrite ?? false,
|
|
360
384
|
})
|
|
361
385
|
}
|
|
@@ -373,6 +397,31 @@ export function designDraftInstall(input: RuntimeDraftInstallInput) {
|
|
|
373
397
|
})
|
|
374
398
|
}
|
|
375
399
|
|
|
400
|
+
export function designPack(input: RuntimeDesignPackInput) {
|
|
401
|
+
return packDesignPackage({
|
|
402
|
+
workspaceRoot: root(input.workspaceRoot),
|
|
403
|
+
name: requiredName(input, "design"),
|
|
404
|
+
source: input.source,
|
|
405
|
+
outputPath: input.outputPath,
|
|
406
|
+
format: input.format,
|
|
407
|
+
overwrite: input.overwrite ?? false,
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function designInstallArchive(input: RuntimeDesignArchiveInstallInput) {
|
|
412
|
+
seedBuiltinDesigns()
|
|
413
|
+
const installed = installDesignArchive({
|
|
414
|
+
archivePath: requiredString(input?.archivePath, "archivePath"),
|
|
415
|
+
name: input.name,
|
|
416
|
+
overwrite: input.overwrite ?? false,
|
|
417
|
+
})
|
|
418
|
+
if (input.activate) {
|
|
419
|
+
activateDesign(installed.name)
|
|
420
|
+
return { ...installed, activated: true, activeDesign: installed.name }
|
|
421
|
+
}
|
|
422
|
+
return { ...installed, activated: false }
|
|
423
|
+
}
|
|
424
|
+
|
|
376
425
|
export interface DesignRulesReadinessResult {
|
|
377
426
|
ok: boolean
|
|
378
427
|
activeDesign: string
|
package/package.json
CHANGED
package/plugins/revela/.mcp.json
CHANGED
|
@@ -28,6 +28,8 @@ type RuntimeModule = {
|
|
|
28
28
|
designDraftCreate(input: any): any
|
|
29
29
|
designDraftValidate(input: any): any
|
|
30
30
|
designDraftInstall(input: any): any
|
|
31
|
+
designPack(input: any): any
|
|
32
|
+
designInstallArchive(input: any): any
|
|
31
33
|
domainList(): any
|
|
32
34
|
domainRead(input?: any): any
|
|
33
35
|
domainActivate(input: any): any
|
|
@@ -175,6 +177,7 @@ const tools = [
|
|
|
175
177
|
base: stringProp("Optional base design used as structural scaffold."),
|
|
176
178
|
designMd: requiredStringProp("Complete DESIGN.md content."),
|
|
177
179
|
previewHtml: requiredStringProp("Complete preview.html content."),
|
|
180
|
+
assets: designAssetsProp(),
|
|
178
181
|
overwrite: booleanProp("Whether to replace an existing local design package. Defaults to false."),
|
|
179
182
|
}, ["name", "designMd", "previewHtml"]),
|
|
180
183
|
},
|
|
@@ -192,6 +195,7 @@ const tools = [
|
|
|
192
195
|
base: stringProp("Optional base design used as structural scaffold."),
|
|
193
196
|
designMd: requiredStringProp("Complete DESIGN.md content."),
|
|
194
197
|
previewHtml: requiredStringProp("Complete preview.html content."),
|
|
198
|
+
assets: designAssetsProp(),
|
|
195
199
|
overwrite: booleanProp("Whether to replace an existing workspace draft. Defaults to false."),
|
|
196
200
|
}, ["name", "designMd", "previewHtml"]),
|
|
197
201
|
},
|
|
@@ -212,6 +216,28 @@ const tools = [
|
|
|
212
216
|
overwrite: booleanProp("Whether to replace an existing user-level design package. Defaults to false."),
|
|
213
217
|
}, ["name"]),
|
|
214
218
|
},
|
|
219
|
+
{
|
|
220
|
+
name: "revela_design_pack",
|
|
221
|
+
description: "Package an installed or workspace-draft Revela design as a shareable .tar or .tar.gz archive.",
|
|
222
|
+
inputSchema: objectSchema({
|
|
223
|
+
workspaceRoot: stringProp("Optional workspace root. Used for draft source lookup and default output path."),
|
|
224
|
+
name: requiredStringProp("Design name in kebab-case."),
|
|
225
|
+
source: enumProp(["draft", "installed"], "Package a workspace draft or installed design. Defaults to draft when a matching draft exists."),
|
|
226
|
+
outputPath: stringProp("Optional archive output path. Defaults to .revela/design-archives/{name}.tar.gz under the workspace."),
|
|
227
|
+
format: enumProp(["tar.gz", "tar"], "Archive format. Defaults to tar.gz."),
|
|
228
|
+
overwrite: booleanProp("Whether to replace an existing archive. Defaults to false."),
|
|
229
|
+
}, ["name"]),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "revela_design_install_archive",
|
|
233
|
+
description: "Install a local .tar or .tar.gz Revela design archive into the user-level design registry.",
|
|
234
|
+
inputSchema: objectSchema({
|
|
235
|
+
archivePath: requiredStringProp("Local path to a .tar, .tar.gz, or .tgz design archive."),
|
|
236
|
+
name: stringProp("Optional installed design name override."),
|
|
237
|
+
overwrite: booleanProp("Whether to replace an existing user-level design package. Defaults to false."),
|
|
238
|
+
activate: booleanProp("Whether to activate the design after installation. Defaults to false."),
|
|
239
|
+
}, ["archivePath"]),
|
|
240
|
+
},
|
|
215
241
|
{
|
|
216
242
|
name: "revela_domain_list",
|
|
217
243
|
description: "List installed Revela narrative domains and the active domain.",
|
|
@@ -410,6 +436,8 @@ async function callTool(name: string, args: any): Promise<any> {
|
|
|
410
436
|
if (name === "revela_design_draft_create") return r.designDraftCreate(args)
|
|
411
437
|
if (name === "revela_design_draft_validate") return r.designDraftValidate(args)
|
|
412
438
|
if (name === "revela_design_draft_install") return r.designDraftInstall(args)
|
|
439
|
+
if (name === "revela_design_pack") return r.designPack(args)
|
|
440
|
+
if (name === "revela_design_install_archive") return r.designInstallArchive(args)
|
|
413
441
|
if (name === "revela_domain_list") return r.domainList()
|
|
414
442
|
if (name === "revela_domain_read") return r.domainRead(args)
|
|
415
443
|
if (name === "revela_domain_activate") return r.domainActivate(args)
|
|
@@ -495,6 +523,24 @@ function arrayObjectProp(description: string) {
|
|
|
495
523
|
}
|
|
496
524
|
}
|
|
497
525
|
|
|
526
|
+
function designAssetsProp() {
|
|
527
|
+
return {
|
|
528
|
+
type: "array",
|
|
529
|
+
description: "Optional design-owned assets for user-uploaded or local materials to archive under assets/**. Each item must use path plus content, contentBase64, or sourcePath.",
|
|
530
|
+
items: {
|
|
531
|
+
type: "object",
|
|
532
|
+
properties: {
|
|
533
|
+
path: { type: "string", description: "Package-relative asset path for uploaded or local design material. Must start with assets/." },
|
|
534
|
+
content: { type: "string", description: "UTF-8 text asset content." },
|
|
535
|
+
contentBase64: { type: "string", description: "Base64-encoded binary asset content." },
|
|
536
|
+
sourcePath: { type: "string", description: "Local file path to copy into the design asset." },
|
|
537
|
+
},
|
|
538
|
+
required: ["path"],
|
|
539
|
+
additionalProperties: false,
|
|
540
|
+
},
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
498
544
|
function componentPlanArrayProp() {
|
|
499
545
|
const childSchema: any = {
|
|
500
546
|
type: "object",
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: revela-design
|
|
3
|
-
description: Create, edit, validate, install, activate, inspect, or list Revela design packages in Codex using design MCP tools.
|
|
3
|
+
description: Create, edit, validate, package, share, install, activate, inspect, or list Revela design packages in Codex using design MCP tools.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Revela Design
|
|
7
7
|
|
|
8
|
-
Use this skill when the user asks to create, customize, edit, validate, install, activate, inspect, list, or switch a Revela design.
|
|
8
|
+
Use this skill when the user asks to create, customize, edit, validate, package, share, install, activate, inspect, list, or switch a Revela design.
|
|
9
9
|
|
|
10
10
|
## Contract
|
|
11
11
|
|
|
12
12
|
- Designs define deck visual systems: rules, foundation, layouts, components, chart rules, and preview coverage.
|
|
13
|
+
- Designs may include package-owned `assets/**` such as cover or closing backgrounds; design tools surface these as design elements, not source evidence.
|
|
14
|
+
- When the user uploads or provides logo, cover, closing, background, texture, brand image, or similar design material, store it inside the design package with `revela_design_draft_create.assets`; use paths under `assets/**` only.
|
|
15
|
+
- Generated `preview.html` must actually reference uploaded design assets with package-relative `assets/...` paths rather than describing them only in text.
|
|
13
16
|
- Default authoring is workspace draft first, then validate, then install only when appropriate.
|
|
14
17
|
- Direct user-level creation is reserved for explicit create/install-now requests.
|
|
18
|
+
- Shareable design archives are `.tar` or `.tar.gz`; install archives only from trusted local paths.
|
|
15
19
|
- Do not use domain tools for visual design work.
|
|
16
20
|
- Do not generate deck HTML while authoring a design.
|
|
17
21
|
|
|
@@ -28,12 +32,20 @@ For new or edited designs:
|
|
|
28
32
|
1. Call `revela_design_list`.
|
|
29
33
|
2. Read the requested base design or active design with `revela_design_read`.
|
|
30
34
|
3. Draft complete `DESIGN.md` and complete `preview.html` content.
|
|
31
|
-
4. Call `revela_design_draft_create
|
|
35
|
+
4. Call `revela_design_draft_create`; when uploaded or local design material exists, pass `assets: [{ path: "assets/...", contentBase64|content|sourcePath }]` so the files are written into the draft package.
|
|
32
36
|
5. Call `revela_design_draft_validate`.
|
|
33
37
|
6. If validation fails, revise the draft content and repeat draft create/validate.
|
|
34
38
|
7. Call `revela_design_draft_install` only after the draft validates and the user intent is to install it.
|
|
35
39
|
8. Call `revela_design_activate` only when the user asks to make it active.
|
|
36
40
|
|
|
41
|
+
For sharing or installing design archives:
|
|
42
|
+
|
|
43
|
+
1. Call `revela_design_draft_validate` or `revela_design_validate` before packaging.
|
|
44
|
+
2. Call `revela_design_pack` to create a `.tar.gz` archive from a workspace draft or installed design.
|
|
45
|
+
3. Call `revela_design_install_archive` to install a local `.tar` or `.tar.gz` archive.
|
|
46
|
+
4. After archive installation, call `revela_design_inventory` or `revela_design_read` to confirm the design and assets are readable.
|
|
47
|
+
5. Call `revela_design_activate` only when the user asks to make the installed design active, or use `activate: true` on archive install when the request is explicit.
|
|
48
|
+
|
|
37
49
|
Use `revela_design_create` only when the user explicitly requests direct local creation outside the workspace draft workflow. Follow it with `revela_design_validate`.
|
|
38
50
|
|
|
39
51
|
## Design Package Requirements
|
|
@@ -41,13 +53,19 @@ Use `revela_design_create` only when the user explicitly requests direct local c
|
|
|
41
53
|
- Use a kebab-case design name.
|
|
42
54
|
- `DESIGN.md` must include valid frontmatter and complete design marker sections.
|
|
43
55
|
- Include design rules, foundation guidance, at least one layout, and at least one component.
|
|
56
|
+
- Optional assets must live under `assets/**`; reference them as package-relative paths like `assets/cover-background.png`.
|
|
57
|
+
- `DESIGN.md` may reference package assets in rules, layouts, or components with `assets/...`; do not reference workspace `assets/` media manifest entries for design-owned visuals.
|
|
44
58
|
- `preview.html` must use the fixed Revela preview canvas contract and visibly preview the design.
|
|
59
|
+
- If design assets are present, `preview.html` must visibly use the saved `assets/...` files, for example a cover hero background or logo image.
|
|
45
60
|
- Preview must include cover and closing examples and showcase every component.
|
|
46
61
|
- Preserve source inspiration and limitations explicitly; do not copy copyrighted design text or assets into the package.
|
|
47
62
|
|
|
48
63
|
## Outputs
|
|
49
64
|
|
|
50
65
|
- Design draft path/status or installed design name.
|
|
66
|
+
- Archive path/status when packaging or installing a shareable design.
|
|
67
|
+
- Asset metadata surfaced by read/inventory tools when `assets/**` exists.
|
|
68
|
+
- Saved asset paths and intended uses, for example `assets/cover-background.png -> cover hero background`.
|
|
51
69
|
- Validation result and any remaining diagnostics.
|
|
52
70
|
- Whether the design was activated.
|
|
53
71
|
- Next step, usually `revela-research` for planning with the design or `revela-make-deck` when a valid `deck-plan.md` already exists.
|
|
@@ -48,12 +48,21 @@ Use this skill to turn user intent and available workspace context into a concis
|
|
|
48
48
|
- `## Audience`
|
|
49
49
|
- `## Decision Or Action`
|
|
50
50
|
- `## Output`
|
|
51
|
+
- `## Language`
|
|
52
|
+
- `## Domain / Use Case`
|
|
53
|
+
- `## Design`
|
|
51
54
|
- `## Constraints`
|
|
52
55
|
- `## Available Materials`
|
|
53
56
|
- `## Known Gaps`
|
|
54
57
|
- `## Acceptance Criteria`
|
|
55
58
|
- `## Recommended Next Step`
|
|
56
59
|
|
|
60
|
+
Section expectations:
|
|
61
|
+
|
|
62
|
+
- `## Language`: output language, terminology preference, and localization notes.
|
|
63
|
+
- `## Domain / Use Case`: active or requested domain, business/use-case context, and decision context.
|
|
64
|
+
- `## Design`: active or requested design, visual direction, and brand/style constraints.
|
|
65
|
+
|
|
57
66
|
Use explicit `Unknown` or `Pending user input` entries instead of inventing requirements.
|
|
58
67
|
|
|
59
68
|
## Outputs
|