@datashow/super-visual-cli 0.0.0-stage → 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/supervisual.mjs +9 -0
- package/package.json +45 -4
- package/src/fabric-client.mjs +101 -0
- package/src/main.mjs +729 -0
- package/src/project-target.mjs +80 -0
- package/src/project.mjs +307 -0
- package/src/semantic-preview.mjs +528 -0
- package/templates/blank/README.md +26 -0
- package/templates/blank/contracts/page-set.yaml +78 -0
- package/templates/blank/contracts/report-source.json +30 -0
- package/templates/blank/contracts/semantic-model.yaml +39 -0
- package/templates/blank/contracts/widget-broker.json +33 -0
- package/templates/blank/deployment/CERTIFICATION.md +18 -0
- package/templates/blank/package.json +15 -0
- package/templates/blank/source/report-design.less +20 -0
- package/templates/blank/source/reportApplication.tsx +29 -0
- package/templates/blank/supervisual.yaml +24 -0
- package/templates/hello-world/README.md +29 -0
- package/templates/hello-world/contracts/page-set.yaml +74 -0
- package/templates/hello-world/contracts/report-source.json +45 -0
- package/templates/hello-world/contracts/semantic-model.yaml +32 -0
- package/templates/hello-world/contracts/widget-broker.json +37 -0
- package/templates/hello-world/deployment/CERTIFICATION.md +17 -0
- package/templates/hello-world/package.json +15 -0
- package/templates/hello-world/source/report-design.less +35 -0
- package/templates/hello-world/source/reportApplication.tsx +69 -0
- package/templates/hello-world/supervisual.yaml +24 -0
- package/templates/semantic-preview/index.html +14 -0
- package/templates/semantic-preview/src/main.tsx +172 -0
- package/README.md +0 -3
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function projectTargetPath(root) {
|
|
5
|
+
return path.join(path.resolve(root), ".supervisual", "target.json");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function readProjectTarget(root) {
|
|
9
|
+
const file = projectTargetPath(root);
|
|
10
|
+
try {
|
|
11
|
+
const value = JSON.parse(await readFile(file, "utf8"));
|
|
12
|
+
if (![1, 2].includes(value?.contractVersion) || value?.kind !== "supervisual-project-target") throw new Error(`Invalid project target: ${file}`);
|
|
13
|
+
if (value.contractVersion === 1) {
|
|
14
|
+
return {
|
|
15
|
+
...value,
|
|
16
|
+
contractVersion: 2,
|
|
17
|
+
managedSemanticModel: value.semanticModel,
|
|
18
|
+
semanticModel: undefined,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error?.code === "ENOENT") return null;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function writeProjectTarget(root, update) {
|
|
29
|
+
const current = await readProjectTarget(root);
|
|
30
|
+
const value = {
|
|
31
|
+
contractVersion: 2,
|
|
32
|
+
kind: "supervisual-project-target",
|
|
33
|
+
...(current ?? {}),
|
|
34
|
+
...update,
|
|
35
|
+
workspace: { ...(current?.workspace ?? {}), ...(update.workspace ?? {}) },
|
|
36
|
+
semanticModel: { ...(current?.semanticModel ?? {}), ...(update.semanticModel ?? {}) },
|
|
37
|
+
managedSemanticModel: { ...(current?.managedSemanticModel ?? {}), ...(update.managedSemanticModel ?? {}) },
|
|
38
|
+
report: { ...(current?.report ?? {}), ...(update.report ?? {}) },
|
|
39
|
+
connection: { ...(current?.connection ?? {}), ...(update.connection ?? {}) },
|
|
40
|
+
};
|
|
41
|
+
for (const key of ["workspace", "semanticModel", "managedSemanticModel", "report", "connection"]) {
|
|
42
|
+
value[key] = Object.fromEntries(Object.entries(value[key]).filter(([, item]) => item !== undefined && item !== null && item !== ""));
|
|
43
|
+
if (!Object.keys(value[key]).length) delete value[key];
|
|
44
|
+
}
|
|
45
|
+
const file = projectTargetPath(root);
|
|
46
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
47
|
+
const temporary = `${file}.tmp`;
|
|
48
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`);
|
|
49
|
+
await rename(temporary, file);
|
|
50
|
+
return { file, value };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function resolveProjectTarget(root, options = {}, { requireSemanticModel = false } = {}) {
|
|
54
|
+
const saved = await readProjectTarget(root);
|
|
55
|
+
const target = {
|
|
56
|
+
workspaceId: options.workspaceId ?? saved?.workspace?.id,
|
|
57
|
+
workspaceName: options.workspaceName ?? saved?.workspace?.name,
|
|
58
|
+
semanticModelId: options.semanticModelId ?? saved?.semanticModel?.id,
|
|
59
|
+
semanticModelName: options.semanticModelName ?? saved?.semanticModel?.name,
|
|
60
|
+
managedSemanticModelId: saved?.managedSemanticModel?.id,
|
|
61
|
+
managedSemanticModelName: saved?.managedSemanticModel?.name,
|
|
62
|
+
reportId: options.reportId ?? saved?.report?.id,
|
|
63
|
+
reportName: options.reportName ?? saved?.report?.name,
|
|
64
|
+
connectionId: options.connectionId ?? saved?.connection?.id,
|
|
65
|
+
connectionMode: options.connectionMode ?? saved?.connection?.mode ?? "ShareableCloud",
|
|
66
|
+
};
|
|
67
|
+
if (!target.workspaceId) throw new Error("No Fabric workspace is bound. Pass --workspace-id during init or on this command.");
|
|
68
|
+
if (requireSemanticModel && !target.semanticModelId && !target.managedSemanticModelId) throw new Error("No Power BI semantic model is bound. Pass --semantic-model-id during init or on this command.");
|
|
69
|
+
return target;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function targetUpdate(options = {}) {
|
|
73
|
+
return {
|
|
74
|
+
workspace: { id: options.workspaceId, name: options.workspaceName },
|
|
75
|
+
semanticModel: { id: options.semanticModelId, name: options.semanticModelName },
|
|
76
|
+
managedSemanticModel: { id: options.managedSemanticModelId, name: options.managedSemanticModelName },
|
|
77
|
+
report: { id: options.reportId, name: options.reportName },
|
|
78
|
+
connection: { id: options.connectionId, mode: options.connectionMode },
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/project.mjs
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { access, cp, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { loadProject, validateCompilation, verifySolutionManifest } from "@datashow/super-visual-compiler";
|
|
6
|
+
import { inspectClaudeDashboard } from "@datashow/super-visual-adapter-claude-dashboard";
|
|
7
|
+
|
|
8
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const templatesRoot = path.join(packageRoot, "templates");
|
|
10
|
+
const initializationChecks = [
|
|
11
|
+
"project-contract",
|
|
12
|
+
"widget-broker",
|
|
13
|
+
"page-set-primitives",
|
|
14
|
+
"report-graph-bindings",
|
|
15
|
+
"report-source-policy",
|
|
16
|
+
"semantic-references",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
async function exists(file) {
|
|
20
|
+
try { await access(file); return true; } catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readOptional(file) {
|
|
24
|
+
try { return await readFile(file, "utf8"); }
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error?.code === "ENOENT") return null;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function slug(value) {
|
|
32
|
+
return String(value)
|
|
33
|
+
.trim()
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
36
|
+
.replace(/^-+|-+$/g, "")
|
|
37
|
+
.replace(/^[^a-z]+/, "") || "super-visual-project";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function displayTitle(value) {
|
|
41
|
+
return value.split("-").map((part) => part ? `${part[0].toUpperCase()}${part.slice(1)}` : part).join(" ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function supportedTemplates() {
|
|
45
|
+
return [
|
|
46
|
+
{ id: "blank", description: "A clean full-page React canvas with the minimum governed model binding.", status: "supported" },
|
|
47
|
+
{ id: "hello-world", description: "A searchable semantic-model catalog that proves the governed data connection.", status: "supported" },
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function supportedAdapters() {
|
|
52
|
+
return [
|
|
53
|
+
{ id: "react-source-bundle", input: "report-source-bundle.v1", status: "supported" },
|
|
54
|
+
{ id: "claude-dashboard", input: "claude-dashboard-bundle.v1", status: "supported", note: "Super Visual's documented intake contract; not an Anthropic export standard." },
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function classifyFile(file) {
|
|
59
|
+
const extension = path.extname(file).toLowerCase();
|
|
60
|
+
if (path.basename(file) === "supervisual.yaml") return { type: "supervisual-project", supported: true, projectFile: file };
|
|
61
|
+
if (path.basename(file) === "claude-dashboard.json") return { type: "claude-dashboard-bundle", supported: true, adapter: "claude-dashboard", manifestPath: file };
|
|
62
|
+
if ([".html", ".htm", ".js", ".zip"].includes(extension)) return { type: "dashboard-source", supported: false, adapter: "claude-dashboard", reason: "Use a directory containing the documented claude-dashboard.json manifest" };
|
|
63
|
+
return { type: "unknown", supported: false };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function discoverInput(input = ".") {
|
|
67
|
+
const resolved = path.resolve(input);
|
|
68
|
+
let metadata;
|
|
69
|
+
try { metadata = await stat(resolved); } catch { return { ok: false, input: resolved, type: "missing", supported: false, diagnostics: [`Input does not exist: ${resolved}`] }; }
|
|
70
|
+
let result;
|
|
71
|
+
if (metadata.isFile()) result = await classifyFile(resolved);
|
|
72
|
+
else if (await exists(path.join(resolved, "supervisual.yaml"))) result = { type: "supervisual-project", supported: true, projectFile: path.join(resolved, "supervisual.yaml") };
|
|
73
|
+
else {
|
|
74
|
+
const names = await readdir(resolved);
|
|
75
|
+
const sourceSignals = names.filter((name) => /^(?:claude-dashboard\.json|index\.html|application\.js|dashboard\.js|dashboard\.json|package\.json)$/i.test(name));
|
|
76
|
+
result = names.includes("claude-dashboard.json")
|
|
77
|
+
? { type: "claude-dashboard-bundle", supported: true, adapter: "claude-dashboard", manifestPath: path.join(resolved, "claude-dashboard.json"), signals: sourceSignals }
|
|
78
|
+
: sourceSignals.length
|
|
79
|
+
? { type: "dashboard-source-directory", supported: false, adapter: "claude-dashboard", signals: sourceSignals, reason: "Add a documented claude-dashboard.json manifest" }
|
|
80
|
+
: { type: "directory", supported: false, signals: [] };
|
|
81
|
+
}
|
|
82
|
+
if (result.type === "claude-dashboard-bundle") {
|
|
83
|
+
try {
|
|
84
|
+
const inspected = await inspectClaudeDashboard(result.manifestPath);
|
|
85
|
+
return { ok: inspected.ok, input: resolved, ...result, dashApis: inspected.dashApis, dataReads: inspected.dataReads, assets: inspected.assetReferences, diagnostics: inspected.diagnostics };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return { ok: false, input: resolved, ...result, supported: false, diagnostics: [error instanceof Error ? error.message : String(error)] };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { ok: result.supported, input: resolved, ...result };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function inspectProject(projectFile) {
|
|
94
|
+
const loaded = await loadProject(projectFile);
|
|
95
|
+
const diagnostics = validateCompilation(loaded);
|
|
96
|
+
const outputDirectory = path.resolve(loaded.root, loaded.project.output.directory);
|
|
97
|
+
let outputState = { exists: false };
|
|
98
|
+
if (await exists(path.join(outputDirectory, "supervisual.manifest.json"))) outputState = { exists: true, ...await verifySolutionManifest(outputDirectory) };
|
|
99
|
+
return {
|
|
100
|
+
ok: diagnostics.length === 0,
|
|
101
|
+
projectFile: path.resolve(projectFile),
|
|
102
|
+
application: { id: loaded.project.application.id, title: loaded.project.application.title },
|
|
103
|
+
target: loaded.project.target,
|
|
104
|
+
capabilities: {
|
|
105
|
+
pages: loaded.pageSet.pages.map((page) => page.id),
|
|
106
|
+
datasets: loaded.broker.widgets.map((widget) => widget.id),
|
|
107
|
+
semanticTables: loaded.semanticModel.tables.map((table) => table.name),
|
|
108
|
+
policies: loaded.project.compliance?.policies ?? ["core", "powerbi"],
|
|
109
|
+
},
|
|
110
|
+
diagnostics,
|
|
111
|
+
output: { directory: outputDirectory, ...outputState },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function replaceTemplateTokens(root, replacements, relative = "") {
|
|
116
|
+
const entries = await readdir(path.join(root, relative), { withFileTypes: true });
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const child = path.join(relative, entry.name);
|
|
119
|
+
if (entry.isDirectory()) await replaceTemplateTokens(root, replacements, child);
|
|
120
|
+
else {
|
|
121
|
+
const file = path.join(root, child);
|
|
122
|
+
let content = await readFile(file, "utf8");
|
|
123
|
+
for (const [token, value] of Object.entries(replacements)) content = content.replaceAll(`{{${token}}}`, value);
|
|
124
|
+
await writeFile(file, content);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const blankApplication = `import { useEffect, type ReactElement } from "react";
|
|
130
|
+
import type { ReportApplicationProps } from "./reportRuntime";
|
|
131
|
+
|
|
132
|
+
export function ReportApplication({ model, onVisibleDatasetsChange }: ReportApplicationProps): ReactElement {
|
|
133
|
+
useEffect(() => onVisibleDatasetsChange?.(["sales"]), [onVisibleDatasetsChange]);
|
|
134
|
+
const rowCount = model.datasets.sales?.points.length ?? 0;
|
|
135
|
+
|
|
136
|
+
return <main className="supervisual-page">
|
|
137
|
+
<p className="eyebrow">Super Visual</p>
|
|
138
|
+
<h1>{{PROJECT_TITLE}}</h1>
|
|
139
|
+
<p>Your full-page React canvas is ready and bound to the semantic provider.</p>
|
|
140
|
+
<p className="status" role="status">{rowCount ? "Received " + rowCount + " semantic rows." : "Waiting for semantic data…"}</p>
|
|
141
|
+
</main>;
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
const blankStyle = `.supervisual-page { box-sizing: border-box; min-height: 100%; padding: 40px; color: #173c36; background: #f4f7f5; font-family: Aptos, "Segoe UI", sans-serif; }
|
|
146
|
+
.supervisual-page .eyebrow { margin: 0 0 8px; color: #167d6c; font-size: 12px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
|
|
147
|
+
.supervisual-page h1 { margin: 0; font: 600 clamp(36px, 6vw, 64px)/1 Georgia, serif; }
|
|
148
|
+
.supervisual-page .status { margin-top: 28px; color: #5e6d68; }
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
const blankReadme = (title) => `# ${title}
|
|
152
|
+
|
|
153
|
+
This is a clean full-page Super Visual project with the minimum governed
|
|
154
|
+
semantic binding. Build the page in \`source/reportApplication.tsx\` and define
|
|
155
|
+
its named datasets under \`contracts/\`.
|
|
156
|
+
|
|
157
|
+
With a workspace and Power BI semantic model saved during initialization:
|
|
158
|
+
|
|
159
|
+
\`\`\`bash
|
|
160
|
+
npx supervisual dev --open
|
|
161
|
+
npx supervisual publish
|
|
162
|
+
\`\`\`
|
|
163
|
+
|
|
164
|
+
Publish shows the exact Fabric plan and asks you to type its plan ID before it
|
|
165
|
+
changes the workspace.
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
async function applicationFiles(input) {
|
|
169
|
+
const resolved = path.resolve(input);
|
|
170
|
+
let root = path.dirname(resolved);
|
|
171
|
+
let entryPath = resolved;
|
|
172
|
+
let entryContent;
|
|
173
|
+
try {
|
|
174
|
+
entryContent = await readFile(resolved, "utf8");
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error?.code !== "EISDIR") throw error;
|
|
177
|
+
root = resolved;
|
|
178
|
+
entryPath = null;
|
|
179
|
+
entryContent = null;
|
|
180
|
+
for (const candidate of [path.join(root, "reportApplication.tsx"), path.join(root, "src", "reportApplication.tsx")]) {
|
|
181
|
+
const content = await readOptional(candidate);
|
|
182
|
+
if (content !== null) { entryPath = candidate; entryContent = content; break; }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!entryPath || entryContent === null) throw new Error(`React application must contain reportApplication.tsx: ${resolved}`);
|
|
186
|
+
const entryRoot = path.dirname(entryPath);
|
|
187
|
+
const files = [{ path: "src/reportApplication.tsx", content: entryContent }];
|
|
188
|
+
const reportRoot = path.join(entryRoot, "report");
|
|
189
|
+
const addReportFiles = async (directory, relative = "") => {
|
|
190
|
+
for (const item of await readdir(directory, { withFileTypes: true })) {
|
|
191
|
+
const child = path.join(directory, item.name);
|
|
192
|
+
const childRelative = path.posix.join(relative, item.name);
|
|
193
|
+
if (item.isDirectory()) await addReportFiles(child, childRelative);
|
|
194
|
+
else if (/\.(?:ts|tsx)$/.test(item.name)) files.push({ path: `src/report/${childRelative}`, content: await readFile(child, "utf8") });
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
try {
|
|
198
|
+
await addReportFiles(reportRoot);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error?.code !== "ENOENT") throw error;
|
|
201
|
+
}
|
|
202
|
+
const styleCandidates = [path.join(root, "report-design.less"), path.join(root, "style", "report-design.less"), path.join(entryRoot, "report-design.less")];
|
|
203
|
+
let style = null;
|
|
204
|
+
for (const candidate of styleCandidates) {
|
|
205
|
+
style = await readOptional(candidate);
|
|
206
|
+
if (style !== null) break;
|
|
207
|
+
}
|
|
208
|
+
return { files, style };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function installApplication(target, input, projectTitle) {
|
|
212
|
+
const imported = await applicationFiles(input);
|
|
213
|
+
const sourceRoot = path.join(target, "source");
|
|
214
|
+
await writeFile(path.join(sourceRoot, "reportApplication.tsx"), imported.files[0].content);
|
|
215
|
+
for (const file of imported.files.slice(1)) {
|
|
216
|
+
const destination = path.join(sourceRoot, file.path.slice("src/".length));
|
|
217
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
218
|
+
await writeFile(destination, file.content);
|
|
219
|
+
}
|
|
220
|
+
if (imported.style) await writeFile(path.join(sourceRoot, "report-design.less"), imported.style);
|
|
221
|
+
const bundlePath = path.join(target, "contracts", "report-source.json");
|
|
222
|
+
const bundle = JSON.parse(await readFile(bundlePath, "utf8"));
|
|
223
|
+
const style = imported.style ?? await readFile(path.join(sourceRoot, "report-design.less"), "utf8");
|
|
224
|
+
bundle.files = [...imported.files, { path: "style/report-design.less", content: style }];
|
|
225
|
+
bundle.designRationale.direction = `${projectTitle} React application hosted as a governed full-page Super Visual.`;
|
|
226
|
+
await writeFile(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function syncProjectSource(projectFile) {
|
|
230
|
+
const loaded = await loadProject(projectFile);
|
|
231
|
+
if (loaded.reportSource.adapter) return { ok: true, synced: false, reason: "adapter-managed-source" };
|
|
232
|
+
const sourceRoot = path.join(loaded.root, "source");
|
|
233
|
+
if (!await exists(sourceRoot)) return { ok: true, synced: false, reason: "source-directory-missing" };
|
|
234
|
+
const imported = await applicationFiles(sourceRoot);
|
|
235
|
+
const style = imported.style ?? loaded.reportSource.files.find((file) => file.path === "style/report-design.less")?.content;
|
|
236
|
+
if (!style) throw new Error("React application source requires source/report-design.less");
|
|
237
|
+
const sourceHash = createHash("sha256").update(style).digest("hex");
|
|
238
|
+
const nextFiles = [...imported.files, { path: "style/report-design.less", content: style }];
|
|
239
|
+
const nextBundle = { ...loaded.reportSource, designSystem: { ...loaded.reportSource.designSystem, sha256: sourceHash }, files: nextFiles };
|
|
240
|
+
const changed = JSON.stringify(nextBundle) !== JSON.stringify(loaded.reportSource);
|
|
241
|
+
if (!changed) return { ok: true, synced: false, files: nextFiles.length };
|
|
242
|
+
const bundlePath = path.resolve(loaded.root, loaded.project.source.bundle);
|
|
243
|
+
await writeFile(bundlePath, `${JSON.stringify(nextBundle, null, 2)}\n`);
|
|
244
|
+
const pageSetPath = path.resolve(loaded.root, loaded.project.semantic.pageSet);
|
|
245
|
+
const pageSet = await readFile(pageSetPath, "utf8");
|
|
246
|
+
if (!/sha256:\s*[a-f0-9]{64}/.test(pageSet)) throw new Error(`Page set does not contain a design-system SHA-256: ${pageSetPath}`);
|
|
247
|
+
await writeFile(pageSetPath, pageSet.replace(/sha256:\s*[a-f0-9]{64}/, `sha256: ${sourceHash}`));
|
|
248
|
+
return { ok: true, synced: true, files: nextFiles.length, bundlePath, pageSetPath };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function initializeProject({ directory, template = "blank", name, title, app, force = false, onProgress = null }) {
|
|
252
|
+
if (!supportedTemplates().some((candidate) => candidate.id === template)) throw new Error(`Unknown template ${template}`);
|
|
253
|
+
const target = path.resolve(directory);
|
|
254
|
+
onProgress?.({ status: "running", step: "scaffold", message: `Creating ${template} project in ${target}` });
|
|
255
|
+
if (await exists(target)) {
|
|
256
|
+
const metadata = await stat(target);
|
|
257
|
+
if (!metadata.isDirectory()) throw new Error(`Target exists and is not a directory: ${target}`);
|
|
258
|
+
const entries = await readdir(target);
|
|
259
|
+
if (entries.length || !force) throw new Error(`Target directory already exists${entries.length ? " and is not empty" : ""}; choose another path or use --force for an empty directory`);
|
|
260
|
+
} else await mkdir(target, { recursive: true });
|
|
261
|
+
const projectId = slug(name ?? path.basename(target));
|
|
262
|
+
const projectTitle = title ?? displayTitle(projectId);
|
|
263
|
+
const visualSuffix = randomUUID().replaceAll("-", "").toUpperCase();
|
|
264
|
+
const visualName = `${projectId.replace(/[^A-Za-z0-9]/g, "")}Visual`;
|
|
265
|
+
const visualGuid = `superVisual${visualSuffix}`;
|
|
266
|
+
await cp(path.join(templatesRoot, template), target, { recursive: true });
|
|
267
|
+
await replaceTemplateTokens(target, {
|
|
268
|
+
PROJECT_ID: projectId,
|
|
269
|
+
PROJECT_TITLE: projectTitle,
|
|
270
|
+
VISUAL_NAME: visualName,
|
|
271
|
+
VISUAL_GUID: visualGuid,
|
|
272
|
+
});
|
|
273
|
+
onProgress?.({ status: "passed", step: "scaffold", message: `Created project files from the ${template} template` });
|
|
274
|
+
onProgress?.({ status: "passed", step: "identity", message: `Generated unique visual identity ${visualGuid}` });
|
|
275
|
+
if (template === "blank") {
|
|
276
|
+
const entry = blankApplication.replaceAll("{{PROJECT_TITLE}}", projectTitle);
|
|
277
|
+
await writeFile(path.join(target, "source", "reportApplication.tsx"), entry);
|
|
278
|
+
await writeFile(path.join(target, "source", "report-design.less"), blankStyle);
|
|
279
|
+
const bundlePath = path.join(target, "contracts", "report-source.json");
|
|
280
|
+
const bundle = JSON.parse(await readFile(bundlePath, "utf8"));
|
|
281
|
+
bundle.designSystem.id = "super-visual-blank";
|
|
282
|
+
bundle.files = [{ path: "src/reportApplication.tsx", content: entry }, { path: "style/report-design.less", content: blankStyle }];
|
|
283
|
+
bundle.designRationale.direction = "A clean, full-page React canvas with a minimal governed semantic binding.";
|
|
284
|
+
bundle.designRationale.dataVisualisation = ["No prescribed visualisation; the page reports semantic data readiness."];
|
|
285
|
+
await writeFile(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
286
|
+
await writeFile(path.join(target, "README.md"), blankReadme(projectTitle));
|
|
287
|
+
}
|
|
288
|
+
if (app) {
|
|
289
|
+
await installApplication(target, app, projectTitle);
|
|
290
|
+
onProgress?.({ status: "passed", step: "application", message: `Imported React application from ${path.resolve(app)}` });
|
|
291
|
+
}
|
|
292
|
+
const style = await readFile(path.join(target, "source", "report-design.less"), "utf8");
|
|
293
|
+
const designHash = createHash("sha256").update(style).digest("hex");
|
|
294
|
+
await replaceTemplateTokens(target, { DESIGN_HASH: designHash });
|
|
295
|
+
const bundlePath = path.join(target, "contracts", "report-source.json");
|
|
296
|
+
const bundle = JSON.parse(await readFile(bundlePath, "utf8"));
|
|
297
|
+
bundle.designSystem.sha256 = designHash;
|
|
298
|
+
await writeFile(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
299
|
+
await syncProjectSource(path.join(target, "supervisual.yaml"));
|
|
300
|
+
onProgress?.({ status: "passed", step: "source", message: "Synchronized React source and report-source contract" });
|
|
301
|
+
onProgress?.({ status: "running", step: "validation", message: "Validating generated contracts, bindings, and source policy" });
|
|
302
|
+
const inspection = await inspectProject(path.join(target, "supervisual.yaml"));
|
|
303
|
+
if (!inspection.ok) throw new Error(`Generated project did not validate:\n- ${inspection.diagnostics.join("\n- ")}`);
|
|
304
|
+
const validation = { ok: true, status: "passed", scope: "local", checks: initializationChecks, diagnostics: inspection.diagnostics };
|
|
305
|
+
onProgress?.({ status: "passed", step: "validation", message: `${initializationChecks.length} local validation checks passed with no diagnostics` });
|
|
306
|
+
return { ok: true, status: "ready", directory: target, projectFile: path.join(target, "supervisual.yaml"), template, application: inspection.application, validation, ...(app ? { reactApplication: path.resolve(app) } : {}), next: [`cd ${target}`, "supervisual dev --open", "supervisual publish"] };
|
|
307
|
+
}
|