@cyber-dash-tech/revela 0.17.20 → 0.17.22
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 +25 -3
- package/README.zh-CN.md +25 -3
- package/designs/monet/DESIGN.md +58 -38
- package/lib/design/designs.ts +113 -2
- package/lib/document-materials/extract.ts +189 -6
- package/lib/domain/domains.ts +221 -1
- package/lib/material-intake.ts +494 -0
- package/lib/runtime/index.ts +114 -1
- package/package.json +1 -1
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/hooks/hooks.json +10 -0
- package/plugins/revela/hooks/revela_guard.ts +19 -0
- package/plugins/revela/hooks/revela_material_notice.ts +58 -0
- package/plugins/revela/hooks/revela_post_write_notice.ts +37 -6
- package/plugins/revela/mcp/revela-server.ts +152 -0
- package/plugins/revela/skills/revela-design/SKILL.md +4 -2
- package/plugins/revela/skills/revela-domain/SKILL.md +13 -1
- package/plugins/revela/skills/revela-init/SKILL.md +18 -8
- package/plugins/revela/skills/revela-upgrade/SKILL.md +33 -0
|
@@ -58,6 +58,7 @@ export type DocumentMaterialsResult = {
|
|
|
58
58
|
cache_dir?: string
|
|
59
59
|
manifest_path?: string
|
|
60
60
|
text_path?: string
|
|
61
|
+
read_view_path?: string
|
|
61
62
|
images?: DocumentMaterial[]
|
|
62
63
|
skipped_assets?: SkippedAsset[]
|
|
63
64
|
slides?: PptxSlide[]
|
|
@@ -74,6 +75,7 @@ type CachedManifest = {
|
|
|
74
75
|
cache_dir: string
|
|
75
76
|
manifest_path: string
|
|
76
77
|
text_path: string
|
|
78
|
+
read_view_path?: string
|
|
77
79
|
images: DocumentMaterial[]
|
|
78
80
|
skipped_assets: SkippedAsset[]
|
|
79
81
|
slides: PptxSlide[]
|
|
@@ -157,6 +159,145 @@ function materialPath(cacheDir: string, workspaceDir: string, ...segments: strin
|
|
|
157
159
|
return workspaceRelative(join(cacheDir, ...segments), workspaceDir)
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
function buildReadView(input: {
|
|
163
|
+
source: string
|
|
164
|
+
type: SupportedType
|
|
165
|
+
fingerprint: string
|
|
166
|
+
text: string
|
|
167
|
+
manifestPath: string
|
|
168
|
+
textPath: string
|
|
169
|
+
images: DocumentMaterial[]
|
|
170
|
+
skippedAssets: SkippedAsset[]
|
|
171
|
+
tables: DocumentMaterial[]
|
|
172
|
+
slides: PptxSlide[] | undefined
|
|
173
|
+
}): string {
|
|
174
|
+
const lines = [
|
|
175
|
+
`# Extracted Material: ${basename(input.source)}`,
|
|
176
|
+
"",
|
|
177
|
+
"## Source",
|
|
178
|
+
"",
|
|
179
|
+
`- sourcePath: ${input.source}`,
|
|
180
|
+
`- type: ${input.type}`,
|
|
181
|
+
`- fingerprint: ${input.fingerprint}`,
|
|
182
|
+
`- manifestPath: ${input.manifestPath}`,
|
|
183
|
+
`- textPath: ${input.textPath}`,
|
|
184
|
+
"",
|
|
185
|
+
"## Text",
|
|
186
|
+
"",
|
|
187
|
+
input.text.trim() || "No text extracted.",
|
|
188
|
+
"",
|
|
189
|
+
"## Extracted Images",
|
|
190
|
+
"",
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
if (input.images.length === 0) lines.push("- None")
|
|
194
|
+
else {
|
|
195
|
+
for (const image of input.images) {
|
|
196
|
+
const parts = [
|
|
197
|
+
image.page_or_slide ? `page_or_slide: ${image.page_or_slide}` : null,
|
|
198
|
+
`source_ref: ${image.source_ref}`,
|
|
199
|
+
image.note ? `note: ${image.note}` : null,
|
|
200
|
+
].filter(Boolean).join("; ")
|
|
201
|
+
lines.push(`- ${image.path}${parts ? ` (${parts})` : ""}`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (input.skippedAssets.length > 0) {
|
|
206
|
+
lines.push("", "## Skipped Or Unmapped Assets", "")
|
|
207
|
+
for (const asset of input.skippedAssets) {
|
|
208
|
+
const parts = [
|
|
209
|
+
asset.page_or_slide ? `page_or_slide: ${asset.page_or_slide}` : null,
|
|
210
|
+
`reason: ${asset.reason}`,
|
|
211
|
+
asset.kind ? `kind: ${asset.kind}` : null,
|
|
212
|
+
].filter(Boolean).join("; ")
|
|
213
|
+
lines.push(`- ${asset.source_ref}${parts ? ` (${parts})` : ""}`)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (input.tables.length > 0) {
|
|
218
|
+
lines.push("", "## Extracted Tables", "")
|
|
219
|
+
for (const table of input.tables) lines.push(`- ${table.path} (${table.note ?? table.source_ref})`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (input.slides?.length) {
|
|
223
|
+
lines.push("", "## Slide Structure", "")
|
|
224
|
+
for (const slide of input.slides) {
|
|
225
|
+
const textCount = slide.elements.filter((element) => element.kind === "text").length
|
|
226
|
+
const imageCount = slide.elements.filter((element) => element.kind === "image").length
|
|
227
|
+
const shapeCount = slide.elements.filter((element) => element.kind === "shape").length
|
|
228
|
+
lines.push(`- ${slide.slide}: ${textCount} text, ${imageCount} image, ${shapeCount} shape`)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
lines.push(
|
|
233
|
+
"",
|
|
234
|
+
"## Intake Rules",
|
|
235
|
+
"",
|
|
236
|
+
"- Treat this extracted material as source context until a material review records what was considered.",
|
|
237
|
+
"- Do not treat extracted images as interpreted evidence unless an explicit image review or user-provided meaning exists.",
|
|
238
|
+
"- Canonical evidence still requires source trace, quote/snippet, support scope, unsupported scope, caveat, strength, and relations in `revela-narrative/`.",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return lines.join("\n")
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function writeReadView(input: {
|
|
245
|
+
cacheDir: string
|
|
246
|
+
workspaceDir: string
|
|
247
|
+
source: string
|
|
248
|
+
type: SupportedType
|
|
249
|
+
fingerprint: string
|
|
250
|
+
text: string
|
|
251
|
+
manifestPath: string
|
|
252
|
+
textPath: string
|
|
253
|
+
images: DocumentMaterial[]
|
|
254
|
+
skippedAssets: SkippedAsset[]
|
|
255
|
+
tables: DocumentMaterial[]
|
|
256
|
+
slides?: PptxSlide[]
|
|
257
|
+
}): string {
|
|
258
|
+
const readViewPath = join(input.cacheDir, "read.md")
|
|
259
|
+
writeFileSync(readViewPath, buildReadView({
|
|
260
|
+
source: input.source,
|
|
261
|
+
type: input.type,
|
|
262
|
+
fingerprint: input.fingerprint,
|
|
263
|
+
text: input.text,
|
|
264
|
+
manifestPath: input.manifestPath,
|
|
265
|
+
textPath: input.textPath,
|
|
266
|
+
images: input.images,
|
|
267
|
+
skippedAssets: input.skippedAssets,
|
|
268
|
+
tables: input.tables,
|
|
269
|
+
slides: input.slides,
|
|
270
|
+
}), "utf-8")
|
|
271
|
+
return workspaceRelative(readViewPath, input.workspaceDir)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function ensureCachedReadView(
|
|
275
|
+
manifest: CachedManifest,
|
|
276
|
+
cacheDir: string,
|
|
277
|
+
workspaceDir: string,
|
|
278
|
+
): string {
|
|
279
|
+
const existing = manifest.read_view_path
|
|
280
|
+
if (existing && existsSync(join(workspaceDir, existing))) return existing
|
|
281
|
+
|
|
282
|
+
const text = existsSync(join(workspaceDir, manifest.text_path))
|
|
283
|
+
? readFileSync(join(workspaceDir, manifest.text_path), "utf-8").replace(/^\[Extracted from: .*?\]\n\n/, "")
|
|
284
|
+
: ""
|
|
285
|
+
return writeReadView({
|
|
286
|
+
cacheDir,
|
|
287
|
+
workspaceDir,
|
|
288
|
+
source: manifest.source,
|
|
289
|
+
type: manifest.type,
|
|
290
|
+
fingerprint: manifest.fingerprint,
|
|
291
|
+
text,
|
|
292
|
+
manifestPath: manifest.manifest_path,
|
|
293
|
+
textPath: manifest.text_path,
|
|
294
|
+
images: manifest.images,
|
|
295
|
+
skippedAssets: manifest.skipped_assets,
|
|
296
|
+
tables: manifest.tables,
|
|
297
|
+
slides: manifest.slides,
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
160
301
|
function updateDecksSourceMaterialIndex(
|
|
161
302
|
workspaceDir: string,
|
|
162
303
|
filePath: string,
|
|
@@ -716,6 +857,7 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
|
|
|
716
857
|
|
|
717
858
|
if (existsSync(manifestPath)) {
|
|
718
859
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as CachedManifest
|
|
860
|
+
const readViewPath = ensureCachedReadView(manifest, cacheDir, workspaceDir)
|
|
719
861
|
return {
|
|
720
862
|
status: "processed",
|
|
721
863
|
cache_status: "hit",
|
|
@@ -724,6 +866,7 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
|
|
|
724
866
|
cache_dir: manifest.cache_dir,
|
|
725
867
|
manifest_path: manifest.manifest_path,
|
|
726
868
|
text_path: manifest.text_path,
|
|
869
|
+
read_view_path: readViewPath,
|
|
727
870
|
images: manifest.images,
|
|
728
871
|
skipped_assets: manifest.skipped_assets,
|
|
729
872
|
slides: manifest.slides,
|
|
@@ -740,6 +883,22 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
|
|
|
740
883
|
writeFileSync(textPath, `[Extracted from: ${basename(filePath)}]\n\n${text}`, "utf-8")
|
|
741
884
|
|
|
742
885
|
const images = await extractPdfImages(buf, cacheDir, workspaceDir)
|
|
886
|
+
const relativeManifestPath = workspaceRelative(manifestPath, workspaceDir)
|
|
887
|
+
const relativeTextPath = workspaceRelative(textPath, workspaceDir)
|
|
888
|
+
const readViewPath = writeReadView({
|
|
889
|
+
cacheDir,
|
|
890
|
+
workspaceDir,
|
|
891
|
+
source: relativeSource,
|
|
892
|
+
type: "pdf",
|
|
893
|
+
fingerprint,
|
|
894
|
+
text,
|
|
895
|
+
manifestPath: relativeManifestPath,
|
|
896
|
+
textPath: relativeTextPath,
|
|
897
|
+
images,
|
|
898
|
+
skippedAssets: [],
|
|
899
|
+
tables: [],
|
|
900
|
+
slides: [],
|
|
901
|
+
})
|
|
743
902
|
|
|
744
903
|
const result: DocumentMaterialsResult = {
|
|
745
904
|
status: "processed",
|
|
@@ -747,8 +906,9 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
|
|
|
747
906
|
source: relativeSource,
|
|
748
907
|
type: "pdf",
|
|
749
908
|
cache_dir: workspaceRelative(cacheDir, workspaceDir),
|
|
750
|
-
manifest_path:
|
|
751
|
-
text_path:
|
|
909
|
+
manifest_path: relativeManifestPath,
|
|
910
|
+
text_path: relativeTextPath,
|
|
911
|
+
read_view_path: readViewPath,
|
|
752
912
|
images,
|
|
753
913
|
skipped_assets: [],
|
|
754
914
|
slides: [],
|
|
@@ -762,6 +922,7 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
|
|
|
762
922
|
cache_dir: result.cache_dir!,
|
|
763
923
|
manifest_path: result.manifest_path!,
|
|
764
924
|
text_path: result.text_path!,
|
|
925
|
+
read_view_path: result.read_view_path,
|
|
765
926
|
images: result.images ?? [],
|
|
766
927
|
skipped_assets: [],
|
|
767
928
|
slides: [],
|
|
@@ -780,6 +941,7 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
780
941
|
|
|
781
942
|
if (existsSync(manifestPath)) {
|
|
782
943
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as CachedManifest
|
|
944
|
+
const readViewPath = ensureCachedReadView(manifest, cacheDir, workspaceDir)
|
|
783
945
|
return {
|
|
784
946
|
status: "processed",
|
|
785
947
|
cache_status: "hit",
|
|
@@ -788,6 +950,7 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
788
950
|
cache_dir: manifest.cache_dir,
|
|
789
951
|
manifest_path: manifest.manifest_path,
|
|
790
952
|
text_path: manifest.text_path,
|
|
953
|
+
read_view_path: readViewPath,
|
|
791
954
|
images: manifest.images,
|
|
792
955
|
skipped_assets: manifest.skipped_assets,
|
|
793
956
|
slides: manifest.slides,
|
|
@@ -821,6 +984,24 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
821
984
|
const slides = type === "pptx"
|
|
822
985
|
? extractPptxSlides(files, images, pptxAssets!.skipped_assets)
|
|
823
986
|
: undefined
|
|
987
|
+
const relativeManifestPath = workspaceRelative(manifestPath, workspaceDir)
|
|
988
|
+
const relativeTextPath = workspaceRelative(textPath, workspaceDir)
|
|
989
|
+
const tables = extractTables(type, relativeTextPath)
|
|
990
|
+
const skippedAssets = pptxAssets?.skipped_assets ?? []
|
|
991
|
+
const readViewPath = writeReadView({
|
|
992
|
+
cacheDir,
|
|
993
|
+
workspaceDir,
|
|
994
|
+
source: relativeSource,
|
|
995
|
+
type,
|
|
996
|
+
fingerprint,
|
|
997
|
+
text,
|
|
998
|
+
manifestPath: relativeManifestPath,
|
|
999
|
+
textPath: relativeTextPath,
|
|
1000
|
+
images,
|
|
1001
|
+
skippedAssets,
|
|
1002
|
+
tables,
|
|
1003
|
+
slides,
|
|
1004
|
+
})
|
|
824
1005
|
|
|
825
1006
|
const result: DocumentMaterialsResult = {
|
|
826
1007
|
status: "processed",
|
|
@@ -828,12 +1009,13 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
828
1009
|
source: relativeSource,
|
|
829
1010
|
type,
|
|
830
1011
|
cache_dir: workspaceRelative(cacheDir, workspaceDir),
|
|
831
|
-
manifest_path:
|
|
832
|
-
text_path:
|
|
1012
|
+
manifest_path: relativeManifestPath,
|
|
1013
|
+
text_path: relativeTextPath,
|
|
1014
|
+
read_view_path: readViewPath,
|
|
833
1015
|
images,
|
|
834
|
-
skipped_assets:
|
|
1016
|
+
skipped_assets: skippedAssets,
|
|
835
1017
|
slides,
|
|
836
|
-
tables
|
|
1018
|
+
tables,
|
|
837
1019
|
}
|
|
838
1020
|
|
|
839
1021
|
const manifest: CachedManifest = {
|
|
@@ -843,6 +1025,7 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
843
1025
|
cache_dir: result.cache_dir!,
|
|
844
1026
|
manifest_path: result.manifest_path!,
|
|
845
1027
|
text_path: result.text_path!,
|
|
1028
|
+
read_view_path: result.read_view_path,
|
|
846
1029
|
images: result.images ?? [],
|
|
847
1030
|
skipped_assets: result.skipped_assets ?? [],
|
|
848
1031
|
slides: result.slides ?? [],
|
package/lib/domain/domains.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
statSync,
|
|
23
23
|
writeFileSync,
|
|
24
24
|
} from "fs"
|
|
25
|
-
import { join, resolve, basename } from "path"
|
|
25
|
+
import { dirname, join, resolve, basename } from "path"
|
|
26
26
|
import { tmpdir } from "os"
|
|
27
27
|
import { parseFrontmatter } from "../frontmatter"
|
|
28
28
|
import {
|
|
@@ -49,6 +49,44 @@ export interface DomainInfo {
|
|
|
49
49
|
skillText: string
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export interface CreateDomainPackageArgs {
|
|
53
|
+
name: string
|
|
54
|
+
domainMd: string
|
|
55
|
+
overwrite?: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CreateDomainDraftArgs extends CreateDomainPackageArgs {
|
|
59
|
+
workspaceRoot: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CreateDomainPackageResult {
|
|
63
|
+
ok: true
|
|
64
|
+
name: string
|
|
65
|
+
path: string
|
|
66
|
+
files: string[]
|
|
67
|
+
overwritten: boolean
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface InstallDomainDraftArgs {
|
|
71
|
+
workspaceRoot: string
|
|
72
|
+
name: string
|
|
73
|
+
overwrite?: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface InstallDomainDraftResult extends CreateDomainPackageResult {
|
|
77
|
+
sourcePath: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ValidateDomainPackageResult {
|
|
81
|
+
ok: boolean
|
|
82
|
+
name: string
|
|
83
|
+
path: string
|
|
84
|
+
hasIndustryMd: boolean
|
|
85
|
+
hasRequiredFrontmatter: boolean
|
|
86
|
+
hasBody: boolean
|
|
87
|
+
errors: string[]
|
|
88
|
+
}
|
|
89
|
+
|
|
52
90
|
// ---------------------------------------------------------------------------
|
|
53
91
|
// Seed
|
|
54
92
|
// ---------------------------------------------------------------------------
|
|
@@ -148,6 +186,188 @@ export function getDomainSkillMd(name?: string): string {
|
|
|
148
186
|
return info.skillText
|
|
149
187
|
}
|
|
150
188
|
|
|
189
|
+
/** Normalize and validate a domain package name. */
|
|
190
|
+
export function normalizeDomainName(name: string): string {
|
|
191
|
+
const normalized = name.trim().toLowerCase()
|
|
192
|
+
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(normalized)) {
|
|
193
|
+
throw new Error("Domain name must be kebab-case using lowercase letters, numbers, and hyphens")
|
|
194
|
+
}
|
|
195
|
+
return normalized
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Create a local domain package in ~/.config/revela/domains/<name>/. */
|
|
199
|
+
export function createDomainPackage(args: CreateDomainPackageArgs): CreateDomainPackageResult {
|
|
200
|
+
const name = normalizeDomainName(args.name)
|
|
201
|
+
const domainMd = args.domainMd?.trim()
|
|
202
|
+
|
|
203
|
+
if (!domainMd) throw new Error("domainMd is required")
|
|
204
|
+
|
|
205
|
+
const target = join(DOMAINS_DIR, name)
|
|
206
|
+
const existed = existsSync(target)
|
|
207
|
+
if (existed && !args.overwrite) {
|
|
208
|
+
throw new Error(`Domain '${name}' already exists. Pass overwrite=true to replace it.`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
mkdirSync(DOMAINS_DIR, { recursive: true })
|
|
212
|
+
if (existed) {
|
|
213
|
+
rmSync(target, { recursive: true, force: true })
|
|
214
|
+
}
|
|
215
|
+
mkdirSync(target, { recursive: true })
|
|
216
|
+
writeFileSync(join(target, DOMAIN_FILE), `${domainMd}\n`, "utf-8")
|
|
217
|
+
|
|
218
|
+
const validation = validateDomainPackage(name)
|
|
219
|
+
if (!validation.ok) {
|
|
220
|
+
throw new Error(`Created domain package is invalid: ${validation.errors.join("; ")}`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
ok: true,
|
|
225
|
+
name,
|
|
226
|
+
path: target,
|
|
227
|
+
files: [DOMAIN_FILE],
|
|
228
|
+
overwritten: existed,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Create a project-local domain draft under .revela/drafts/domains/<name>/. */
|
|
233
|
+
export function createDomainDraftPackage(args: CreateDomainDraftArgs): CreateDomainPackageResult {
|
|
234
|
+
const name = normalizeDomainName(args.name)
|
|
235
|
+
const domainMd = args.domainMd?.trim()
|
|
236
|
+
|
|
237
|
+
if (!domainMd) throw new Error("domainMd is required")
|
|
238
|
+
|
|
239
|
+
const target = domainDraftDir(args.workspaceRoot, name)
|
|
240
|
+
const existed = existsSync(target)
|
|
241
|
+
if (existed && !args.overwrite) {
|
|
242
|
+
throw new Error(`Domain draft '${name}' already exists. Pass overwrite=true to replace it.`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
246
|
+
if (existed) {
|
|
247
|
+
rmSync(target, { recursive: true, force: true })
|
|
248
|
+
}
|
|
249
|
+
mkdirSync(target, { recursive: true })
|
|
250
|
+
writeFileSync(join(target, DOMAIN_FILE), `${domainMd}\n`, "utf-8")
|
|
251
|
+
|
|
252
|
+
const validation = validateDomainDraftPackage(args.workspaceRoot, name)
|
|
253
|
+
if (!validation.ok) {
|
|
254
|
+
throw new Error(`Created domain draft is invalid: ${validation.errors.join("; ")}`)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
name,
|
|
260
|
+
path: target,
|
|
261
|
+
files: [DOMAIN_FILE],
|
|
262
|
+
overwritten: existed,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Validate a project-local domain draft. */
|
|
267
|
+
export function validateDomainDraftPackage(workspaceRoot: string, nameInput: string): ValidateDomainPackageResult {
|
|
268
|
+
let name = nameInput
|
|
269
|
+
try {
|
|
270
|
+
name = normalizeDomainName(nameInput)
|
|
271
|
+
} catch {
|
|
272
|
+
// validateDomainPackageAt records the invalid-name error.
|
|
273
|
+
}
|
|
274
|
+
return validateDomainPackageAt(nameInput, domainDraftDir(workspaceRoot, name))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Install a validated project-local domain draft into the user-level domain registry. */
|
|
278
|
+
export function installDomainDraftPackage(args: InstallDomainDraftArgs): InstallDomainDraftResult {
|
|
279
|
+
const name = normalizeDomainName(args.name)
|
|
280
|
+
const sourcePath = domainDraftDir(args.workspaceRoot, name)
|
|
281
|
+
const validation = validateDomainDraftPackage(args.workspaceRoot, name)
|
|
282
|
+
if (!validation.ok) {
|
|
283
|
+
throw new Error(`Domain draft is invalid: ${validation.errors.join("; ")}`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const target = join(DOMAINS_DIR, name)
|
|
287
|
+
const existed = existsSync(target)
|
|
288
|
+
if (existed && !args.overwrite) {
|
|
289
|
+
throw new Error(`Domain '${name}' already exists. Pass overwrite=true to replace it.`)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
mkdirSync(DOMAINS_DIR, { recursive: true })
|
|
294
|
+
if (existed) {
|
|
295
|
+
rmSync(target, { recursive: true, force: true })
|
|
296
|
+
}
|
|
297
|
+
cpSync(sourcePath, target, { recursive: true })
|
|
298
|
+
} catch (e) {
|
|
299
|
+
throw new Error(`Installing domain draft requires write access to Revela user config at ${DOMAINS_DIR}: ${e instanceof Error ? e.message : String(e)}`)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
ok: true,
|
|
304
|
+
name,
|
|
305
|
+
path: target,
|
|
306
|
+
sourcePath,
|
|
307
|
+
files: [DOMAIN_FILE],
|
|
308
|
+
overwritten: existed,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Validate a local domain package for the minimum Revela domain contract. */
|
|
313
|
+
export function validateDomainPackage(nameInput: string): ValidateDomainPackageResult {
|
|
314
|
+
let name = nameInput
|
|
315
|
+
try {
|
|
316
|
+
name = normalizeDomainName(nameInput)
|
|
317
|
+
} catch {
|
|
318
|
+
// validateDomainPackageAt records the invalid-name error.
|
|
319
|
+
}
|
|
320
|
+
return validateDomainPackageAt(nameInput, join(DOMAINS_DIR, name))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function validateDomainPackageAt(nameInput: string, dir: string): ValidateDomainPackageResult {
|
|
324
|
+
let name = nameInput
|
|
325
|
+
const errors: string[] = []
|
|
326
|
+
try {
|
|
327
|
+
name = normalizeDomainName(nameInput)
|
|
328
|
+
} catch (e) {
|
|
329
|
+
errors.push(e instanceof Error ? e.message : String(e))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const mdPath = join(dir, DOMAIN_FILE)
|
|
333
|
+
const hasIndustryMd = existsSync(mdPath)
|
|
334
|
+
let hasRequiredFrontmatter = false
|
|
335
|
+
let hasBody = false
|
|
336
|
+
|
|
337
|
+
if (!existsSync(dir)) errors.push(`Domain directory does not exist: ${dir}`)
|
|
338
|
+
if (!hasIndustryMd) errors.push(`${DOMAIN_FILE} is missing`)
|
|
339
|
+
|
|
340
|
+
if (hasIndustryMd) {
|
|
341
|
+
try {
|
|
342
|
+
const text = readFileSync(mdPath, "utf-8")
|
|
343
|
+
const { meta, body } = parseFrontmatter(text)
|
|
344
|
+
const required = ["name", "description", "author", "version"]
|
|
345
|
+
const missing = required.filter((field) => !meta[field]?.trim())
|
|
346
|
+
hasRequiredFrontmatter = missing.length === 0
|
|
347
|
+
hasBody = body.trim().length > 0
|
|
348
|
+
if (!hasRequiredFrontmatter) errors.push(`INDUSTRY.md frontmatter is missing required field(s): ${missing.join(", ")}`)
|
|
349
|
+
if (!hasBody) errors.push("INDUSTRY.md body/guidance is empty")
|
|
350
|
+
if (meta.name && meta.name.trim() !== name) errors.push(`INDUSTRY.md frontmatter name '${meta.name.trim()}' does not match package name '${name}'`)
|
|
351
|
+
} catch (e) {
|
|
352
|
+
errors.push(`INDUSTRY.md could not be parsed: ${e instanceof Error ? e.message : String(e)}`)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
ok: errors.length === 0,
|
|
358
|
+
name,
|
|
359
|
+
path: dir,
|
|
360
|
+
hasIndustryMd,
|
|
361
|
+
hasRequiredFrontmatter,
|
|
362
|
+
hasBody,
|
|
363
|
+
errors,
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function domainDraftDir(workspaceRoot: string, name: string): string {
|
|
368
|
+
return resolve(workspaceRoot, ".revela", "drafts", "domains", name)
|
|
369
|
+
}
|
|
370
|
+
|
|
151
371
|
/** Remove an installed domain. Throws if not found or is the protected default. */
|
|
152
372
|
export function removeDomain(name: string): void {
|
|
153
373
|
if (name === DEFAULT_DOMAIN) {
|