@datashow/super-visual-cli 0.1.0-rc.0 → 0.1.0-rc.2
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 -2
- package/package.json +5 -5
- package/src/main.mjs +123 -3
- package/src/project.mjs +21 -3
- package/templates/blank/package.json +1 -1
- package/templates/hello-world/package.json +1 -1
package/bin/supervisual.mjs
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { main } from "../src/main.mjs";
|
|
2
|
+
import { main, stopActiveProgress } from "../src/main.mjs";
|
|
3
3
|
|
|
4
4
|
main().catch((error) => {
|
|
5
|
+
stopActiveProgress();
|
|
5
6
|
const json = process.argv.includes("--json");
|
|
6
7
|
const message = error instanceof Error ? error.message : String(error);
|
|
7
|
-
|
|
8
|
+
const summary = error?.summary ?? "Super Visual could not complete the command.";
|
|
9
|
+
const guidance = error?.guidance;
|
|
10
|
+
if (json) {
|
|
11
|
+
process.stderr.write(`${JSON.stringify({ ok: false, message: summary, ...(guidance ? { guidance } : {}), error: message })}\n`);
|
|
12
|
+
} else {
|
|
13
|
+
process.stderr.write(`\n${summary}\n${guidance ? `${guidance}\n` : ""}\nTechnical details: ${message}\n`);
|
|
14
|
+
}
|
|
8
15
|
process.exitCode = 1;
|
|
9
16
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datashow/super-visual-cli",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.2",
|
|
4
4
|
"description": "CLI for validating, compiling, packaging, verifying, and deploying Super Visual projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@datashow/super-visual-adapter-claude-dashboard": "0.1.0-rc.
|
|
29
|
-
"@datashow/super-visual-compiler": "0.1.0-rc.
|
|
30
|
-
"@datashow/super-visual-contracts": "0.1.0-rc.
|
|
31
|
-
"@datashow/super-visual-fabric": "0.1.0-rc.
|
|
28
|
+
"@datashow/super-visual-adapter-claude-dashboard": "0.1.0-rc.2",
|
|
29
|
+
"@datashow/super-visual-compiler": "0.1.0-rc.2",
|
|
30
|
+
"@datashow/super-visual-contracts": "0.1.0-rc.2",
|
|
31
|
+
"@datashow/super-visual-fabric": "0.1.0-rc.2",
|
|
32
32
|
"@fontsource/source-serif-4": "5.3.1",
|
|
33
33
|
"@nivo/bar": "0.99.0",
|
|
34
34
|
"@nivo/line": "0.99.0",
|
package/src/main.mjs
CHANGED
|
@@ -15,7 +15,29 @@ import { resolveProjectTarget, targetUpdate, writeProjectTarget } from "./projec
|
|
|
15
15
|
import { runSemanticPreviewTest, startSemanticPreview } from "./semantic-preview.mjs";
|
|
16
16
|
|
|
17
17
|
const run = promisify(execFile);
|
|
18
|
-
const VERSION = "0.1.0-rc.
|
|
18
|
+
const VERSION = "0.1.0-rc.2";
|
|
19
|
+
const FABRIC_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
|
+
const SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
21
|
+
let activeProgress = null;
|
|
22
|
+
|
|
23
|
+
export class CliUserError extends Error {
|
|
24
|
+
constructor(message, { summary, guidance } = {}) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "CliUserError";
|
|
27
|
+
this.summary = summary;
|
|
28
|
+
this.guidance = guidance;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validateFabricId(value, optionName) {
|
|
33
|
+
if (value === undefined) return;
|
|
34
|
+
if (FABRIC_ID_PATTERN.test(value)) return;
|
|
35
|
+
const target = optionName === "--workspace-id" ? "workspace" : "semantic model";
|
|
36
|
+
throw new CliUserError(`${optionName} must be a 36-character GUID in 8-4-4-4-12 format; received "${value}" (${String(value).length} characters).`, {
|
|
37
|
+
summary: `Super Visual could not initialize the project because the ${target} ID is not valid.`,
|
|
38
|
+
guidance: `Copy the complete ${target} ID from Fabric and run the command again.`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
19
41
|
|
|
20
42
|
function projectFile(input = ".") {
|
|
21
43
|
const resolved = path.resolve(input);
|
|
@@ -26,6 +48,58 @@ function output(value, json) {
|
|
|
26
48
|
process.stdout.write(`${json ? JSON.stringify(value) : typeof value === "string" ? value : JSON.stringify(value, null, 2)}\n`);
|
|
27
49
|
}
|
|
28
50
|
|
|
51
|
+
export function stopActiveProgress() {
|
|
52
|
+
if (!activeProgress) return;
|
|
53
|
+
clearInterval(activeProgress.timer);
|
|
54
|
+
process.stdout.write(`\r${" ".repeat(activeProgress.width)}\r`);
|
|
55
|
+
activeProgress = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeInitProgress(event) {
|
|
59
|
+
if (event.status === "running" && process.stdout.isTTY) {
|
|
60
|
+
stopActiveProgress();
|
|
61
|
+
let frame = 0;
|
|
62
|
+
const render = () => {
|
|
63
|
+
const line = `${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${event.message}`;
|
|
64
|
+
process.stdout.write(`\r${line}`);
|
|
65
|
+
activeProgress.width = Math.max(activeProgress.width, line.length);
|
|
66
|
+
frame += 1;
|
|
67
|
+
};
|
|
68
|
+
activeProgress = { timer: null, width: event.message.length + 2 };
|
|
69
|
+
render();
|
|
70
|
+
activeProgress.timer = setInterval(render, 80);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
stopActiveProgress();
|
|
74
|
+
const marker = event.status === "passed" ? "✓" : "…";
|
|
75
|
+
process.stdout.write(`${marker} ${event.message}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function semanticInventory(model) {
|
|
79
|
+
const tables = model.tables ?? [];
|
|
80
|
+
return {
|
|
81
|
+
tables: tables.length,
|
|
82
|
+
columns: tables.reduce((total, table) => total + (table.columns?.length ?? 0), 0),
|
|
83
|
+
measures: tables.reduce((total, table) => total + (table.measures?.length ?? 0), 0),
|
|
84
|
+
hierarchies: tables.reduce((total, table) => total + (table.hierarchies?.length ?? 0), 0),
|
|
85
|
+
hierarchyLevels: tables.reduce((total, table) => total + (table.hierarchies ?? []).reduce((levels, hierarchy) => levels + (hierarchy.levels?.length ?? 0), 0), 0),
|
|
86
|
+
relationships: model.relationships?.length ?? 0,
|
|
87
|
+
parameters: model.parameters?.length ?? 0,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function printInitializationResult(result) {
|
|
92
|
+
process.stdout.write(`\nInitialization complete — ${result.application.title} is ready.\n`);
|
|
93
|
+
process.stdout.write(`Project: ${result.directory}\n`);
|
|
94
|
+
process.stdout.write(`Project manifest: ${result.projectFile}\n`);
|
|
95
|
+
if (result.binding) {
|
|
96
|
+
if (result.binding.workspace) process.stdout.write(`Workspace: ${result.binding.workspace.name ?? "(name not recorded)"} (${result.binding.workspace.id})\n`);
|
|
97
|
+
if (result.binding.semanticModel) process.stdout.write(`Semantic model: ${result.binding.semanticModel.name ?? "(name not recorded)"} (${result.binding.semanticModel.id})\n`);
|
|
98
|
+
}
|
|
99
|
+
process.stdout.write(`\nInitialization manifest (JSON):\n${JSON.stringify(result, null, 2)}\n`);
|
|
100
|
+
process.stdout.write(`\nNext:\n${result.next.map((command) => ` ${command}`).join("\n")}\n`);
|
|
101
|
+
}
|
|
102
|
+
|
|
29
103
|
function jsonEnabled(command) {
|
|
30
104
|
return Boolean(command.optsWithGlobals().json);
|
|
31
105
|
}
|
|
@@ -240,18 +314,64 @@ function createProgram() {
|
|
|
240
314
|
.option("--report-name <name>", "target report name")
|
|
241
315
|
.option("--connection-id <id>", "advanced override for the selected model's Fabric connection")
|
|
242
316
|
.option("--connection-mode <mode>", "advanced Fabric connectivity override", "ShareableCloud")
|
|
317
|
+
.option("--skip-live-validation", "save the binding without checking the workspace and semantic model")
|
|
318
|
+
.option("--timeout <seconds>", "Fabric validation timeout", "900")
|
|
243
319
|
.option("--force", "write into an existing empty directory")
|
|
244
320
|
.action(async (directory, options, command) => {
|
|
245
321
|
if ((options.workspaceName || options.semanticModelId || options.reportName || options.connectionId) && !options.workspaceId) throw new Error("--workspace-id is required when saving a Fabric binding");
|
|
246
|
-
|
|
322
|
+
validateFabricId(options.workspaceId, "--workspace-id");
|
|
323
|
+
validateFabricId(options.semanticModelId, "--semantic-model-id");
|
|
324
|
+
const json = jsonEnabled(command);
|
|
325
|
+
if (!json) process.stdout.write(`\nSuper Visual init\n\n`);
|
|
326
|
+
let liveValidation = null;
|
|
327
|
+
if (options.workspaceId && options.semanticModelId && !options.skipLiveValidation) {
|
|
328
|
+
if (!json) writeInitProgress({ status: "running", message: "Verifying the Fabric workspace and Power BI semantic model (read-only)" });
|
|
329
|
+
let inspected;
|
|
330
|
+
try {
|
|
331
|
+
const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
|
|
332
|
+
inspected = await inspectSemanticModel({ client, workspaceId: options.workspaceId, workspaceName: options.workspaceName, semanticModelId: options.semanticModelId });
|
|
333
|
+
} catch (error) {
|
|
334
|
+
throw new CliUserError(error instanceof Error ? error.message : String(error), {
|
|
335
|
+
summary: "Super Visual could not verify the Fabric workspace and semantic model.",
|
|
336
|
+
guidance: "Confirm that you are signed in, can access the workspace, and copied both IDs from the same Fabric workspace.",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
liveValidation = {
|
|
340
|
+
ok: true,
|
|
341
|
+
status: "passed",
|
|
342
|
+
scope: "fabric-read-only",
|
|
343
|
+
workspace: inspected.workspace,
|
|
344
|
+
semanticModel: { id: inspected.semanticModel.id, displayName: inspected.semanticModel.displayName, format: inspected.semanticModel.format },
|
|
345
|
+
inventory: semanticInventory(inspected.semanticModel),
|
|
346
|
+
};
|
|
347
|
+
if (!json) writeInitProgress({ status: "passed", message: `Verified workspace ${inspected.workspace.displayName} and semantic model ${inspected.semanticModel.displayName}` });
|
|
348
|
+
} else if (!json && options.skipLiveValidation) {
|
|
349
|
+
process.stdout.write("! Live Fabric validation skipped by request\n");
|
|
350
|
+
} else if (!json) {
|
|
351
|
+
process.stdout.write("• No complete Fabric binding supplied; live validation was not run\n");
|
|
352
|
+
}
|
|
353
|
+
const result = await initializeProject({
|
|
354
|
+
directory,
|
|
355
|
+
template: options.template,
|
|
356
|
+
name: options.name,
|
|
357
|
+
title: options.title,
|
|
358
|
+
app: options.app,
|
|
359
|
+
force: options.force,
|
|
360
|
+
onProgress: json ? null : writeInitProgress,
|
|
361
|
+
});
|
|
247
362
|
if (options.workspaceId) {
|
|
248
363
|
const saved = await writeProjectTarget(result.directory, targetUpdate({
|
|
249
364
|
...options,
|
|
365
|
+
workspaceName: options.workspaceName ?? liveValidation?.workspace.displayName,
|
|
366
|
+
semanticModelName: liveValidation?.semanticModel.displayName,
|
|
250
367
|
reportName: options.reportName ?? result.application.title,
|
|
251
368
|
}));
|
|
252
369
|
result.binding = { file: saved.file, ...saved.value };
|
|
370
|
+
if (!json) writeInitProgress({ status: "passed", message: `Saved Fabric binding to ${saved.file}` });
|
|
253
371
|
}
|
|
254
|
-
|
|
372
|
+
result.validation = { local: result.validation, live: liveValidation ?? { ok: true, status: "not-run", scope: "fabric-read-only" } };
|
|
373
|
+
if (json) output(result, true);
|
|
374
|
+
else printInitializationResult(result);
|
|
255
375
|
});
|
|
256
376
|
|
|
257
377
|
addJsonOption(program.command("check [project]"))
|
package/src/project.mjs
CHANGED
|
@@ -7,6 +7,14 @@ import { inspectClaudeDashboard } from "@datashow/super-visual-adapter-claude-da
|
|
|
7
7
|
|
|
8
8
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
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
|
+
];
|
|
10
18
|
|
|
11
19
|
async function exists(file) {
|
|
12
20
|
try { await access(file); return true; } catch { return false; }
|
|
@@ -240,9 +248,10 @@ export async function syncProjectSource(projectFile) {
|
|
|
240
248
|
return { ok: true, synced: true, files: nextFiles.length, bundlePath, pageSetPath };
|
|
241
249
|
}
|
|
242
250
|
|
|
243
|
-
export async function initializeProject({ directory, template = "blank", name, title, app, force = false }) {
|
|
251
|
+
export async function initializeProject({ directory, template = "blank", name, title, app, force = false, onProgress = null }) {
|
|
244
252
|
if (!supportedTemplates().some((candidate) => candidate.id === template)) throw new Error(`Unknown template ${template}`);
|
|
245
253
|
const target = path.resolve(directory);
|
|
254
|
+
onProgress?.({ status: "running", step: "scaffold", message: `Creating ${template} project in ${target}` });
|
|
246
255
|
if (await exists(target)) {
|
|
247
256
|
const metadata = await stat(target);
|
|
248
257
|
if (!metadata.isDirectory()) throw new Error(`Target exists and is not a directory: ${target}`);
|
|
@@ -261,6 +270,8 @@ export async function initializeProject({ directory, template = "blank", name, t
|
|
|
261
270
|
VISUAL_NAME: visualName,
|
|
262
271
|
VISUAL_GUID: visualGuid,
|
|
263
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}` });
|
|
264
275
|
if (template === "blank") {
|
|
265
276
|
const entry = blankApplication.replaceAll("{{PROJECT_TITLE}}", projectTitle);
|
|
266
277
|
await writeFile(path.join(target, "source", "reportApplication.tsx"), entry);
|
|
@@ -274,7 +285,10 @@ export async function initializeProject({ directory, template = "blank", name, t
|
|
|
274
285
|
await writeFile(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
275
286
|
await writeFile(path.join(target, "README.md"), blankReadme(projectTitle));
|
|
276
287
|
}
|
|
277
|
-
if (app)
|
|
288
|
+
if (app) {
|
|
289
|
+
await installApplication(target, app, projectTitle);
|
|
290
|
+
onProgress?.({ status: "passed", step: "application", message: `Imported React application from ${path.resolve(app)}` });
|
|
291
|
+
}
|
|
278
292
|
const style = await readFile(path.join(target, "source", "report-design.less"), "utf8");
|
|
279
293
|
const designHash = createHash("sha256").update(style).digest("hex");
|
|
280
294
|
await replaceTemplateTokens(target, { DESIGN_HASH: designHash });
|
|
@@ -283,7 +297,11 @@ export async function initializeProject({ directory, template = "blank", name, t
|
|
|
283
297
|
bundle.designSystem.sha256 = designHash;
|
|
284
298
|
await writeFile(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
285
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" });
|
|
286
302
|
const inspection = await inspectProject(path.join(target, "supervisual.yaml"));
|
|
287
303
|
if (!inspection.ok) throw new Error(`Generated project did not validate:\n- ${inspection.diagnostics.join("\n- ")}`);
|
|
288
|
-
|
|
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"] };
|
|
289
307
|
}
|