@datashow/super-visual-cli 0.0.0-stage → 0.1.0-rc.0

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.
Files changed (30) hide show
  1. package/bin/supervisual.mjs +9 -0
  2. package/package.json +45 -4
  3. package/src/fabric-client.mjs +101 -0
  4. package/src/main.mjs +663 -0
  5. package/src/project-target.mjs +80 -0
  6. package/src/project.mjs +289 -0
  7. package/src/semantic-preview.mjs +528 -0
  8. package/templates/blank/README.md +26 -0
  9. package/templates/blank/contracts/page-set.yaml +78 -0
  10. package/templates/blank/contracts/report-source.json +30 -0
  11. package/templates/blank/contracts/semantic-model.yaml +39 -0
  12. package/templates/blank/contracts/widget-broker.json +33 -0
  13. package/templates/blank/deployment/CERTIFICATION.md +18 -0
  14. package/templates/blank/package.json +15 -0
  15. package/templates/blank/source/report-design.less +20 -0
  16. package/templates/blank/source/reportApplication.tsx +29 -0
  17. package/templates/blank/supervisual.yaml +24 -0
  18. package/templates/hello-world/README.md +29 -0
  19. package/templates/hello-world/contracts/page-set.yaml +74 -0
  20. package/templates/hello-world/contracts/report-source.json +45 -0
  21. package/templates/hello-world/contracts/semantic-model.yaml +32 -0
  22. package/templates/hello-world/contracts/widget-broker.json +37 -0
  23. package/templates/hello-world/deployment/CERTIFICATION.md +17 -0
  24. package/templates/hello-world/package.json +15 -0
  25. package/templates/hello-world/source/report-design.less +35 -0
  26. package/templates/hello-world/source/reportApplication.tsx +69 -0
  27. package/templates/hello-world/supervisual.yaml +24 -0
  28. package/templates/semantic-preview/index.html +14 -0
  29. package/templates/semantic-preview/src/main.tsx +172 -0
  30. package/README.md +0 -3
package/src/main.mjs ADDED
@@ -0,0 +1,663 @@
1
+ import { cp, mkdir, mkdtemp, readFile, readdir, rename, writeFile } from "node:fs/promises";
2
+ import { execFile } from "node:child_process";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { promisify } from "node:util";
7
+ import { Command, CommanderError } from "commander";
8
+ import { importClaudeDashboard, inspectClaudeDashboard } from "@datashow/super-visual-adapter-claude-dashboard";
9
+ import { buildSolution, embedVisualPackage, loadProject, refreshSolutionManifest, validateCompilation, verifySolutionManifest } from "@datashow/super-visual-compiler";
10
+ import { evaluateLiveCertification, loadContract, migrateProjectContracts } from "@datashow/super-visual-contracts";
11
+ import { applyDeploymentPlan, applyRestorePlan, checkSemanticCompatibility, createDeploymentDiff, createDeploymentPlan, createRestorePlan, discoverFabric, discoverFabricWorkspaces, inspectSemanticModel, readDeploymentStatus } from "@datashow/super-visual-fabric";
12
+ import { discoverInput, initializeProject, inspectProject, supportedAdapters, supportedTemplates, syncProjectSource } from "./project.mjs";
13
+ import { FabricHttpClient } from "./fabric-client.mjs";
14
+ import { resolveProjectTarget, targetUpdate, writeProjectTarget } from "./project-target.mjs";
15
+ import { runSemanticPreviewTest, startSemanticPreview } from "./semantic-preview.mjs";
16
+
17
+ const run = promisify(execFile);
18
+ const VERSION = "0.1.0-rc.0";
19
+
20
+ function projectFile(input = ".") {
21
+ const resolved = path.resolve(input);
22
+ return path.extname(resolved) ? resolved : path.join(resolved, "supervisual.yaml");
23
+ }
24
+
25
+ function output(value, json) {
26
+ process.stdout.write(`${json ? JSON.stringify(value) : typeof value === "string" ? value : JSON.stringify(value, null, 2)}\n`);
27
+ }
28
+
29
+ function jsonEnabled(command) {
30
+ return Boolean(command.optsWithGlobals().json);
31
+ }
32
+
33
+ async function packageVisual(result) {
34
+ const temporary = await mkdtemp(path.join(os.tmpdir(), "supervisual-package-"));
35
+ const visual = path.join(temporary, "Visual");
36
+ await cp(path.join(result.outputDirectory, "Visual"), visual, { recursive: true });
37
+ await run("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund"], { cwd: visual, maxBuffer: 10 * 1024 * 1024 });
38
+ await run("npm", ["run", "typecheck"], { cwd: visual, maxBuffer: 10 * 1024 * 1024 });
39
+ await run("npm", ["run", "package"], { cwd: visual, maxBuffer: 20 * 1024 * 1024 });
40
+ const name = (await readdir(path.join(visual, "dist"))).find((file) => file.endsWith(".pbiviz"));
41
+ if (!name) throw new Error("pbiviz package did not produce a visual archive");
42
+ const artifacts = path.join(result.outputDirectory, "Artifacts");
43
+ await mkdir(artifacts, { recursive: true });
44
+ const destination = path.join(artifacts, name);
45
+ await cp(path.join(visual, "dist", name), destination);
46
+ await embedVisualPackage(destination, path.join(result.outputDirectory, "Report.Report"));
47
+ await refreshSolutionManifest(result.outputDirectory);
48
+ return destination;
49
+ }
50
+
51
+ async function semanticParameters(file) {
52
+ if (!file) return null;
53
+ const profile = JSON.parse(await readFile(path.resolve(file), "utf8"));
54
+ if (profile?.contractVersion !== 1 || profile?.kind !== "fabric-sql-source") throw new Error("Source profile must use contractVersion 1 and kind fabric-sql-source");
55
+ const keys = Object.keys(profile.source ?? {});
56
+ if (!keys.length || keys.some((key) => !["server", "database"].includes(key))) throw new Error("Source profile may contain only source.server and source.database");
57
+ if (!["server", "database"].every((key) => typeof profile.source[key] === "string" && profile.source[key].trim())) throw new Error("Source profile requires non-empty source.server and source.database strings");
58
+ return { server: profile.source.server.trim(), database: profile.source.database.trim() };
59
+ }
60
+
61
+ async function build(project, shouldPackage, parametersFile, semanticCatalogSource = null, semanticParameterOverride = null) {
62
+ await syncProjectSource(projectFile(project));
63
+ const parameters = semanticParameterOverride ?? await semanticParameters(parametersFile);
64
+ const result = await buildSolution(projectFile(project), { semanticParameters: parameters, semanticCatalogSource });
65
+ const artifact = shouldPackage ? await packageVisual(result) : null;
66
+ const verified = await verifySolutionManifest(result.outputDirectory);
67
+ return {
68
+ ok: verified.ok,
69
+ outputDirectory: result.outputDirectory,
70
+ fileCount: verified.fileCount,
71
+ sourceProfileApplied: Boolean(parameters),
72
+ ...(artifact ? { visualPackage: artifact } : {}),
73
+ };
74
+ }
75
+
76
+ export function selectedConnectionDefaults(references) {
77
+ const bound = (references ?? []).filter((reference) => reference?.id && reference?.connectionDetails?.path);
78
+ const identities = new Set(bound.map((reference) => `${reference.id}\u0000${reference.connectivityType ?? "ShareableCloud"}`));
79
+ const paths = new Set(bound.map((reference) => `${reference.connectionDetails.type ?? ""}\u0000${reference.connectionDetails.path}`));
80
+ if (identities.size !== 1 || paths.size !== 1) return null;
81
+ const reference = bound[0];
82
+ const pathParts = String(reference.connectionDetails.path).split(";").map((part) => part.trim()).filter(Boolean);
83
+ const sqlSource = String(reference.connectionDetails.type).toLowerCase().includes("sql") && pathParts.length === 2
84
+ ? { server: pathParts[0], database: pathParts[1] }
85
+ : null;
86
+ return { connectionId: reference.id, connectionMode: reference.connectivityType ?? "ShareableCloud", semanticParameters: sqlSource };
87
+ }
88
+
89
+ function addJsonOption(command) {
90
+ return command.option("--json", "emit one machine-readable JSON value");
91
+ }
92
+
93
+ function usesSemanticCatalog(broker) {
94
+ return (broker.widgets ?? []).some((widget) => [
95
+ widget.axis,
96
+ ...(widget.dimensions ?? []).flatMap((dimension) => [dimension.key, dimension.label]),
97
+ ...(widget.measures ?? []),
98
+ ].some((reference) => typeof reference === "string" && reference.startsWith("Super Visual Model Catalog[")));
99
+ }
100
+
101
+ async function writeJsonAtomic(file, value) {
102
+ await mkdir(path.dirname(file), { recursive: true });
103
+ const temporary = `${file}.tmp`;
104
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`);
105
+ await rename(temporary, file);
106
+ }
107
+
108
+ async function waitForPreviewShutdown(preview) {
109
+ await new Promise((resolve) => {
110
+ const stop = () => resolve();
111
+ process.once("SIGINT", stop);
112
+ process.once("SIGTERM", stop);
113
+ });
114
+ await preview.close();
115
+ }
116
+
117
+ async function confirmDeploymentPlan(plan) {
118
+ printDeploymentPlan(plan);
119
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
120
+ try {
121
+ const answer = await prompt.question(`Type ${plan.planId} to publish, or press Enter to stop: `);
122
+ return answer.trim() === plan.planId;
123
+ } finally {
124
+ prompt.close();
125
+ }
126
+ }
127
+
128
+ function printDeploymentPlan(plan) {
129
+ const operations = plan.actions.map((action) => `${action.operation} ${action.type} “${action.displayName}”`).join("\n - ");
130
+ process.stdout.write(`\nReady to publish to ${plan.workspace.displayName} (${plan.workspace.id}):\n - ${operations}\n\nPlan ID: ${plan.planId}\n`);
131
+ }
132
+
133
+ async function applySavedDeployment({ loaded, planPath, confirm, backup, receipt: receiptOption, resume, timeout }) {
134
+ const resolvedPlanPath = path.resolve(planPath);
135
+ const plan = JSON.parse(await readFile(resolvedPlanPath, "utf8"));
136
+ if (confirm !== plan.planId) throw new Error(`Deployment requires exact plan confirmation ${plan.planId}`);
137
+ if (receiptOption && resume && path.resolve(receiptOption) !== path.resolve(resume)) throw new Error("--receipt and --resume must identify the same file when both are supplied");
138
+ const receiptPath = path.resolve(receiptOption ?? resume ?? path.join(loaded.root, ".supervisual", "deploy-receipt.json"));
139
+ const receipt = resume
140
+ ? JSON.parse(await readFile(path.resolve(resume), "utf8"))
141
+ : { contractVersion: 1, kind: "supervisual-deployment-receipt", planId: plan.planId, workspace: plan.workspace, startedAt: new Date().toISOString(), status: "in-progress", items: {} };
142
+ await writeJsonAtomic(receiptPath, receipt);
143
+ const client = new FabricHttpClient({ timeoutSeconds: Number(timeout) });
144
+ try {
145
+ const result = await applyDeploymentPlan({
146
+ client,
147
+ plan,
148
+ confirmPlanId: confirm,
149
+ backupDirectory: backup ? path.resolve(backup) : undefined,
150
+ resumeState: resume ? receipt : null,
151
+ onProgress: async ({ action, item }) => {
152
+ receipt.items[action] = item;
153
+ receipt.status = "in-progress";
154
+ receipt.updatedAt = new Date().toISOString();
155
+ await writeJsonAtomic(receiptPath, receipt);
156
+ },
157
+ });
158
+ receipt.items = result.items;
159
+ receipt.status = result.status;
160
+ receipt.completedAt = new Date().toISOString();
161
+ receipt.verification = result.verification;
162
+ await writeJsonAtomic(receiptPath, receipt);
163
+ return { ...result, receiptPath, planPath: resolvedPlanPath };
164
+ } catch (error) {
165
+ receipt.status = "interrupted";
166
+ receipt.updatedAt = new Date().toISOString();
167
+ await writeJsonAtomic(receiptPath, receipt);
168
+ throw error;
169
+ }
170
+ }
171
+
172
+ function createProgram() {
173
+ const program = new Command();
174
+ program
175
+ .name("supervisual")
176
+ .description("Compile governed React dashboards into deployable Power BI applications.")
177
+ .version(VERSION)
178
+ .showHelpAfterError()
179
+ .configureOutput({
180
+ writeOut: (message) => process.stdout.write(message),
181
+ writeErr: () => {},
182
+ })
183
+ .exitOverride();
184
+
185
+ addJsonOption(program.command("discover [input]"))
186
+ .description("detect a Super Visual project or supported dashboard input")
187
+ .action(async (input = ".", options, command) => output(await discoverInput(input), jsonEnabled(command)));
188
+
189
+ addJsonOption(program.command("inspect [project]"))
190
+ .description("summarize a project, its bindings, policies, and generated output")
191
+ .action(async (project = ".", options, command) => output(await inspectProject(projectFile(project)), jsonEnabled(command)));
192
+
193
+ addJsonOption(program.command("migrate [project]"))
194
+ .description("inspect contract versions and optionally apply recoverable source migrations")
195
+ .option("--write", "apply available migrations; the default is a dry run")
196
+ .option("--backup <directory>", "backup destination used with --write")
197
+ .action(async (project = ".", options, command) => {
198
+ if (options.backup && !options.write) throw new Error("--backup requires --write");
199
+ const result = await migrateProjectContracts(projectFile(project), { write: Boolean(options.write), backupDirectory: options.backup ? path.resolve(options.backup) : undefined });
200
+ output(result, jsonEnabled(command));
201
+ return result;
202
+ });
203
+
204
+ addJsonOption(program.command("templates"))
205
+ .description("list project templates available to init")
206
+ .action((options, command) => output({ ok: true, templates: supportedTemplates() }, jsonEnabled(command)));
207
+
208
+ addJsonOption(program.command("adapters"))
209
+ .description("list source adapters and their compatibility status")
210
+ .action((options, command) => output({ ok: true, adapters: supportedAdapters() }, jsonEnabled(command)));
211
+
212
+ const importCommand = program.command("import").description("import a supported dashboard source into a Super Visual project");
213
+ addJsonOption(importCommand.command("claude-dashboard <input>"))
214
+ .description("statically inspect and import a Super Visual Claude-style dashboard bundle")
215
+ .option("--project <directory>", "existing Super Visual project to update")
216
+ .option("--inspect-only", "validate and report detected APIs without modifying the project")
217
+ .action(async (input, options, command) => {
218
+ if (options.inspectOnly) {
219
+ const inspected = await inspectClaudeDashboard(input);
220
+ const result = { ok: inspected.ok, input: inspected.input, manifestPath: inspected.manifestPath, adapter: "claude-dashboard", dashApis: inspected.dashApis, dataReads: inspected.dataReads, assets: inspected.assetReferences, diagnostics: inspected.diagnostics };
221
+ output(result, jsonEnabled(command));
222
+ if (!result.ok) process.exitCode = 1;
223
+ return result;
224
+ }
225
+ if (!options.project) throw new Error("--project is required unless --inspect-only is used");
226
+ const result = await importClaudeDashboard({ input, projectFile: projectFile(options.project) });
227
+ output(result, jsonEnabled(command));
228
+ return result;
229
+ });
230
+
231
+ addJsonOption(program.command("init <directory>"))
232
+ .description("create a new Super Visual project")
233
+ .option("--template <name>", "project template", "blank")
234
+ .option("--app <path>", "existing Super Visual-compatible React application source")
235
+ .option("--name <id>", "project identifier")
236
+ .option("--title <title>", "display title")
237
+ .option("--workspace-id <id>", "bind the project to an immutable Fabric workspace ID")
238
+ .option("--workspace-name <name>", "expected workspace display name")
239
+ .option("--semantic-model-id <id>", "Power BI semantic model that the report uses")
240
+ .option("--report-name <name>", "target report name")
241
+ .option("--connection-id <id>", "advanced override for the selected model's Fabric connection")
242
+ .option("--connection-mode <mode>", "advanced Fabric connectivity override", "ShareableCloud")
243
+ .option("--force", "write into an existing empty directory")
244
+ .action(async (directory, options, command) => {
245
+ if ((options.workspaceName || options.semanticModelId || options.reportName || options.connectionId) && !options.workspaceId) throw new Error("--workspace-id is required when saving a Fabric binding");
246
+ const result = await initializeProject({ directory, template: options.template, name: options.name, title: options.title, app: options.app, force: options.force });
247
+ if (options.workspaceId) {
248
+ const saved = await writeProjectTarget(result.directory, targetUpdate({
249
+ ...options,
250
+ reportName: options.reportName ?? result.application.title,
251
+ }));
252
+ result.binding = { file: saved.file, ...saved.value };
253
+ }
254
+ output(result, jsonEnabled(command));
255
+ });
256
+
257
+ addJsonOption(program.command("check [project]"))
258
+ .description("validate project contracts, semantic bindings, and source policy")
259
+ .action(async (project = ".", options, command) => {
260
+ const loaded = await loadProject(projectFile(project));
261
+ const diagnostics = validateCompilation(loaded);
262
+ const result = { ok: diagnostics.length === 0, project: projectFile(project), diagnostics };
263
+ output(result, jsonEnabled(command));
264
+ if (!result.ok) process.exitCode = 1;
265
+ return result;
266
+ });
267
+
268
+ addJsonOption(program.command("build [project]").alias("compile"))
269
+ .description("compile a deterministic, inspectable solution bundle")
270
+ .option("--package", "also create and embed the deployable .pbiviz archive")
271
+ .option("--parameters-file <file>", "deployment source profile with service-reachable server and database")
272
+ .action(async (project = ".", options, command) => output(await build(project, Boolean(options.package), options.parametersFile), jsonEnabled(command)));
273
+
274
+ addJsonOption(program.command("package [project]"))
275
+ .description("compile, package, embed, and verify a deployable solution")
276
+ .option("--parameters-file <file>", "deployment source profile with service-reachable server and database")
277
+ .action(async (project = ".", options, command) => output(await build(project, true, options.parametersFile), jsonEnabled(command)));
278
+
279
+ addJsonOption(program.command("verify [project]"))
280
+ .description("verify every generated file against the solution manifest")
281
+ .action(async (project = ".", options, command) => {
282
+ const loaded = await loadProject(projectFile(project));
283
+ const result = await verifySolutionManifest(path.resolve(loaded.root, loaded.project.output.directory));
284
+ output(result, jsonEnabled(command));
285
+ if (!result.ok) process.exitCode = 1;
286
+ return result;
287
+ });
288
+
289
+ addJsonOption(program.command("dev [project]"))
290
+ .description("serve the local React application against a live semantic model")
291
+ .option("--workspace-id <id>", "immutable Fabric workspace ID; defaults to the saved project binding")
292
+ .option("--workspace-name <name>", "expected workspace display name")
293
+ .option("--semantic-model-id <id>", "Power BI semantic model ID; defaults to the saved project binding")
294
+ .option("--host <host>", "loopback host", "127.0.0.1")
295
+ .option("--port <port>", "local preview port", "5173")
296
+ .option("--open", "open the preview in the default browser")
297
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
298
+ .action(async (project = ".", options, command) => {
299
+ await syncProjectSource(projectFile(project));
300
+ const loaded = await loadProject(projectFile(project));
301
+ const target = await resolveProjectTarget(loaded.root, options, { requireSemanticModel: true });
302
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
303
+ const preview = await startSemanticPreview({
304
+ client,
305
+ projectFile: projectFile(project),
306
+ workspaceId: target.workspaceId,
307
+ workspaceName: target.workspaceName,
308
+ semanticModelId: target.semanticModelId ?? target.managedSemanticModelId,
309
+ host: options.host,
310
+ port: Number(options.port),
311
+ open: Boolean(options.open),
312
+ });
313
+ const { close, ...result } = preview;
314
+ output(result, jsonEnabled(command));
315
+ await waitForPreviewShutdown({ close });
316
+ });
317
+
318
+ const preview = program.command("preview").description("exercise the local React host against a live semantic model");
319
+ addJsonOption(preview.command("test [project]"))
320
+ .description("run a headless semantic-data and interaction smoke test")
321
+ .option("--workspace-id <id>", "immutable Fabric workspace ID; defaults to the saved project binding")
322
+ .option("--workspace-name <name>", "expected workspace display name")
323
+ .option("--semantic-model-id <id>", "Power BI semantic model ID; defaults to the saved project binding")
324
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
325
+ .option("--browser-timeout <milliseconds>", "browser acceptance timeout", "60000")
326
+ .action(async (project = ".", options, command) => {
327
+ await syncProjectSource(projectFile(project));
328
+ const loaded = await loadProject(projectFile(project));
329
+ const target = await resolveProjectTarget(loaded.root, options, { requireSemanticModel: true });
330
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
331
+ const result = await runSemanticPreviewTest({
332
+ client,
333
+ projectFile: projectFile(project),
334
+ workspaceId: target.workspaceId,
335
+ workspaceName: target.workspaceName,
336
+ semanticModelId: target.semanticModelId ?? target.managedSemanticModelId,
337
+ timeoutMs: Number(options.browserTimeout),
338
+ });
339
+ output(result, jsonEnabled(command));
340
+ return result;
341
+ });
342
+
343
+ const certify = program.command("certify").description("validate redacted live deployment and browser evidence");
344
+ addJsonOption(certify.command("validate <record>"))
345
+ .description("validate and grade a live certification record")
346
+ .action(async (record, options, command) => {
347
+ const value = await loadContract("live-certification-record", path.resolve(record));
348
+ const result = evaluateLiveCertification(value);
349
+ output({ ...result, record: path.resolve(record) }, jsonEnabled(command));
350
+ if (!result.ok) process.exitCode = 1;
351
+ return result;
352
+ });
353
+
354
+ const semantic = program.command("semantic").description("inspect live semantic models and validate project compatibility without mutation");
355
+ addJsonOption(semantic.command("discover"))
356
+ .description("read a semantic model's TMDL inventory and optionally execute acceptance DAX")
357
+ .requiredOption("--workspace-id <id>", "immutable Fabric workspace ID")
358
+ .option("--workspace-name <name>", "expected workspace display name")
359
+ .requiredOption("--semantic-model-id <id>", "exact semantic model ID")
360
+ .option("--acceptance-query-file <file>", "DAX acceptance query to execute read-only")
361
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
362
+ .action(async (options, command) => {
363
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
364
+ const acceptanceDax = options.acceptanceQueryFile ? await readFile(path.resolve(options.acceptanceQueryFile), "utf8") : null;
365
+ const result = await inspectSemanticModel({ client, workspaceId: options.workspaceId, workspaceName: options.workspaceName, semanticModelId: options.semanticModelId, acceptanceDax });
366
+ output(result, jsonEnabled(command));
367
+ return result;
368
+ });
369
+
370
+ addJsonOption(semantic.command("check [project]"))
371
+ .description("compare a project's semantic and broker contracts with a live semantic model")
372
+ .requiredOption("--workspace-id <id>", "immutable Fabric workspace ID")
373
+ .option("--workspace-name <name>", "expected workspace display name")
374
+ .requiredOption("--semantic-model-id <id>", "exact semantic model ID")
375
+ .option("--acceptance-query-file <file>", "DAX acceptance query to execute read-only")
376
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
377
+ .action(async (project = ".", options, command) => {
378
+ const loaded = await loadProject(projectFile(project));
379
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
380
+ const acceptanceDax = options.acceptanceQueryFile ? await readFile(path.resolve(options.acceptanceQueryFile), "utf8") : null;
381
+ const inspected = await inspectSemanticModel({ client, workspaceId: options.workspaceId, workspaceName: options.workspaceName, semanticModelId: options.semanticModelId, acceptanceDax });
382
+ const compatibility = checkSemanticCompatibility(inspected.semanticModel, loaded.semanticModel, loaded.broker);
383
+ const result = { ...inspected, ok: compatibility.ok, compatibility };
384
+ output(result, jsonEnabled(command));
385
+ if (!result.ok) process.exitCode = 1;
386
+ return result;
387
+ });
388
+
389
+ addJsonOption(program.command("publish [project]"))
390
+ .description("package, plan, and safely publish the report using its saved Fabric binding")
391
+ .option("--workspace-id <id>", "immutable Fabric workspace ID; defaults to the saved project binding")
392
+ .option("--workspace-name <name>", "expected workspace display name")
393
+ .option("--semantic-model-id <id>", "Power BI semantic model ID; defaults to the saved project binding")
394
+ .option("--report-name <name>", "target report name")
395
+ .option("--connection-id <id>", "advanced override for the selected model's Fabric connection")
396
+ .option("--connection-mode <mode>", "advanced Fabric connectivity override", "ShareableCloud")
397
+ .option("--parameters-file <file>", "advanced source-path override for managed deployment")
398
+ .option("--acceptance-query-file <file>", "advanced DAX acceptance override")
399
+ .option("--allow-updates", "allow a plan that replaces same-named remote items")
400
+ .option("--confirm <plan-id>", "apply the previously reviewed exact plan ID")
401
+ .option("--pre-authorize", "authorize and apply the plan generated by this invocation without prompting")
402
+ .option("--plan <file>", "deployment plan path; defaults to .supervisual/deploy-plan.json")
403
+ .option("--backup <directory>", "required backup destination for updates")
404
+ .option("--receipt <file>", "deployment progress receipt destination")
405
+ .option("--resume <file>", "resume from a matching interrupted receipt")
406
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
407
+ .action(async (project = ".", options, command) => {
408
+ const loaded = await loadProject(projectFile(project));
409
+ const planPath = path.resolve(options.plan ?? path.join(loaded.root, ".supervisual", "deploy-plan.json"));
410
+ if (options.confirm && options.preAuthorize) throw new Error("--confirm and --pre-authorize are mutually exclusive");
411
+ if (options.confirm) {
412
+ const result = await applySavedDeployment({ loaded, planPath, confirm: options.confirm, backup: options.backup, receipt: options.receipt, resume: options.resume, timeout: options.timeout });
413
+ await writeProjectTarget(loaded.root, targetUpdate({
414
+ workspaceId: result.workspace.id,
415
+ workspaceName: result.workspace.displayName,
416
+ managedSemanticModelId: result.items.semanticModel?.id,
417
+ managedSemanticModelName: result.items.semanticModel?.displayName,
418
+ reportId: result.items.report?.id,
419
+ reportName: result.items.report?.displayName,
420
+ }));
421
+ output({ ok: result.accepted, ...result }, jsonEnabled(command));
422
+ if (!result.accepted) process.exitCode = 3;
423
+ return result;
424
+ }
425
+
426
+ const target = await resolveProjectTarget(loaded.root, options, { requireSemanticModel: true });
427
+ const reportName = target.reportName ?? loaded.project.application.title;
428
+ const semanticModelName = target.managedSemanticModelName ?? `${reportName} — Super Visual data`;
429
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
430
+ const selectedSemanticModelId = target.semanticModelId ?? target.managedSemanticModelId;
431
+ const inspected = await inspectSemanticModel({ client, workspaceId: target.workspaceId, workspaceName: target.workspaceName, semanticModelId: selectedSemanticModelId });
432
+ const compatibility = usesSemanticCatalog(loaded.broker)
433
+ ? { ok: true, summary: { required: 1, matched: 1, missing: 0, ambiguous: 0 }, diagnostics: [] }
434
+ : checkSemanticCompatibility(inspected.semanticModel, loaded.semanticModel, loaded.broker);
435
+ if (!compatibility.ok) throw new Error(`Selected semantic model is incompatible:\n- ${compatibility.diagnostics.map((item) => item.message).join("\n- ")}`);
436
+ const selectedConnection = selectedConnectionDefaults(await client.getSemanticConnections(target.workspaceId, selectedSemanticModelId));
437
+ const connectionId = target.connectionId ?? selectedConnection?.connectionId;
438
+ const connectionMode = target.connectionId ? target.connectionMode : selectedConnection?.connectionMode ?? target.connectionMode;
439
+ const automaticParameters = options.parametersFile ? null : selectedConnection?.semanticParameters;
440
+ if (!connectionId) throw new Error("The selected semantic model does not expose one reusable Fabric connection. Use the advanced --connection-id override.");
441
+ if (!options.parametersFile && !automaticParameters) throw new Error("The selected semantic model source path cannot be reused automatically. Use the advanced --parameters-file override.");
442
+ const packaged = await build(project, true, options.parametersFile, inspected.semanticModel, automaticParameters);
443
+ if (!packaged.ok) throw new Error("Packaged solution manifest verification failed");
444
+ const acceptanceDax = options.acceptanceQueryFile ? await readFile(path.resolve(options.acceptanceQueryFile), "utf8") : 'EVALUATE ROW("Super Visual", 1)';
445
+ const postDeployment = { connectionId, connectivityType: connectionMode, timeoutSeconds: Number(options.timeout), acceptanceDax };
446
+ const plan = await createDeploymentPlan({
447
+ client,
448
+ workspaceId: target.workspaceId,
449
+ workspaceName: target.workspaceName,
450
+ solutionDirectory: packaged.outputDirectory,
451
+ semanticModelName,
452
+ reportName,
453
+ allowUpdates: Boolean(options.allowUpdates),
454
+ postDeployment,
455
+ });
456
+ await writeJsonAtomic(planPath, plan);
457
+ await writeProjectTarget(loaded.root, targetUpdate({
458
+ workspaceId: target.workspaceId,
459
+ workspaceName: plan.workspace.displayName,
460
+ semanticModelId: target.semanticModelId,
461
+ semanticModelName: target.semanticModelId ? inspected.semanticModel.displayName : target.semanticModelName,
462
+ managedSemanticModelName: semanticModelName,
463
+ reportName,
464
+ connectionId,
465
+ connectionMode,
466
+ }));
467
+ const result = {
468
+ ok: plan.blockers.length === 0,
469
+ status: plan.blockers.length ? "blocked" : "planned",
470
+ packaged,
471
+ selectedSemanticModel: { id: inspected.semanticModel.id, displayName: inspected.semanticModel.displayName, compatibility: compatibility.summary },
472
+ ...plan,
473
+ planPath,
474
+ next: plan.blockers.length ? null : `supervisual publish --confirm ${plan.planId}`,
475
+ };
476
+ if (!plan.blockers.length && options.preAuthorize) {
477
+ if (!jsonEnabled(command)) {
478
+ printDeploymentPlan(plan);
479
+ process.stdout.write("Pre-authorization accepted; applying this generated plan without a second prompt.\n");
480
+ }
481
+ const applied = await applySavedDeployment({ loaded, planPath, confirm: plan.planId, backup: options.backup, receipt: options.receipt, resume: options.resume, timeout: options.timeout });
482
+ await writeProjectTarget(loaded.root, targetUpdate({
483
+ workspaceId: applied.workspace.id,
484
+ workspaceName: applied.workspace.displayName,
485
+ semanticModelId: target.semanticModelId,
486
+ semanticModelName: target.semanticModelName,
487
+ managedSemanticModelId: applied.items.semanticModel?.id,
488
+ managedSemanticModelName: applied.items.semanticModel?.displayName,
489
+ reportId: applied.items.report?.id,
490
+ reportName: applied.items.report?.displayName,
491
+ }));
492
+ output({ ok: applied.accepted, preAuthorized: true, ...applied }, jsonEnabled(command));
493
+ if (!applied.accepted) process.exitCode = 3;
494
+ return applied;
495
+ }
496
+ if (!plan.blockers.length && !jsonEnabled(command) && process.stdin.isTTY && process.stdout.isTTY) {
497
+ const confirmed = await confirmDeploymentPlan(plan);
498
+ if (confirmed) {
499
+ const applied = await applySavedDeployment({ loaded, planPath, confirm: plan.planId, backup: options.backup, receipt: options.receipt, resume: options.resume, timeout: options.timeout });
500
+ await writeProjectTarget(loaded.root, targetUpdate({
501
+ workspaceId: applied.workspace.id,
502
+ workspaceName: applied.workspace.displayName,
503
+ semanticModelId: target.semanticModelId,
504
+ semanticModelName: target.semanticModelName,
505
+ managedSemanticModelId: applied.items.semanticModel?.id,
506
+ managedSemanticModelName: applied.items.semanticModel?.displayName,
507
+ reportId: applied.items.report?.id,
508
+ reportName: applied.items.report?.displayName,
509
+ }));
510
+ output({ ok: applied.accepted, ...applied }, false);
511
+ if (!applied.accepted) process.exitCode = 3;
512
+ return applied;
513
+ }
514
+ output(`Publish cancelled. The reviewed plan is saved at ${planPath}.`, false);
515
+ return result;
516
+ }
517
+ output(result, jsonEnabled(command));
518
+ if (plan.blockers.length) process.exitCode = 2;
519
+ return result;
520
+ });
521
+
522
+ const deploy = program.command("deploy").description("plan and apply guarded Microsoft Fabric deployments");
523
+ addJsonOption(deploy.command("workspaces"))
524
+ .description("list accessible Fabric workspaces and immutable IDs without mutation")
525
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
526
+ .action(async (options, command) => {
527
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
528
+ output(await discoverFabricWorkspaces({ client }), jsonEnabled(command));
529
+ });
530
+
531
+ addJsonOption(deploy.command("discover"))
532
+ .description("read workspace identity, items, and optionally accessible connections")
533
+ .requiredOption("--workspace-id <id>", "immutable Fabric workspace ID")
534
+ .option("--workspace-name <name>", "expected workspace display name")
535
+ .option("--connections", "include accessible Fabric connections")
536
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
537
+ .action(async (options, command) => {
538
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
539
+ output(await discoverFabric({ client, workspaceId: options.workspaceId, workspaceName: options.workspaceName, includeConnections: Boolean(options.connections) }), jsonEnabled(command));
540
+ });
541
+
542
+ addJsonOption(deploy.command("status"))
543
+ .description("read deployed binding, refresh, and optional live acceptance status")
544
+ .requiredOption("--workspace-id <id>", "immutable Fabric workspace ID")
545
+ .option("--workspace-name <name>", "expected workspace display name")
546
+ .option("--semantic-model-id <id>", "exact deployed semantic model ID")
547
+ .option("--report-id <id>", "exact deployed report ID")
548
+ .option("--connection-id <id>", "expected bound Fabric connection ID")
549
+ .option("--acceptance-query-file <file>", "DAX acceptance query to execute read-only")
550
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
551
+ .action(async (options, command) => {
552
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
553
+ const acceptanceDax = options.acceptanceQueryFile ? await readFile(path.resolve(options.acceptanceQueryFile), "utf8") : null;
554
+ const result = await readDeploymentStatus({ client, workspaceId: options.workspaceId, workspaceName: options.workspaceName, semanticModelId: options.semanticModelId, reportId: options.reportId, connectionId: options.connectionId, acceptanceDax });
555
+ output(result, jsonEnabled(command));
556
+ });
557
+
558
+ addJsonOption(deploy.command("diff [project]"))
559
+ .description("compare local definitions with exact same-named Fabric items without mutation")
560
+ .requiredOption("--workspace-id <id>", "immutable Fabric workspace ID")
561
+ .option("--workspace-name <name>", "expected workspace display name")
562
+ .requiredOption("--semantic-model-name <name>", "target semantic model name")
563
+ .requiredOption("--report-name <name>", "target report name")
564
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
565
+ .action(async (project = ".", options, command) => {
566
+ const loaded = await loadProject(projectFile(project));
567
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
568
+ const solutionDirectory = path.resolve(loaded.root, loaded.project.output.directory);
569
+ const result = await createDeploymentDiff({ client, workspaceId: options.workspaceId, workspaceName: options.workspaceName, solutionDirectory, semanticModelName: options.semanticModelName, reportName: options.reportName });
570
+ output(result, jsonEnabled(command));
571
+ return result;
572
+ });
573
+
574
+ addJsonOption(deploy.command("plan [project]"))
575
+ .description("inspect Fabric and write an immutable deployment plan")
576
+ .requiredOption("--workspace-id <id>", "immutable Fabric workspace ID")
577
+ .option("--workspace-name <name>", "expected workspace display name")
578
+ .requiredOption("--semantic-model-name <name>", "target semantic model name")
579
+ .requiredOption("--report-name <name>", "target report name")
580
+ .option("--connection-id <id>", "Fabric connection ID for post-deployment binding")
581
+ .option("--connection-mode <mode>", "Fabric connectivity type", "ShareableCloud")
582
+ .option("--acceptance-query-file <file>", "DAX acceptance query to run after refresh")
583
+ .option("--allow-updates", "allow plans that replace same-named remote items")
584
+ .option("--output <file>", "deployment plan destination")
585
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
586
+ .action(async (project = ".", options, command) => {
587
+ const loaded = await loadProject(projectFile(project));
588
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
589
+ const solutionDirectory = path.resolve(loaded.root, loaded.project.output.directory);
590
+ const acceptanceDax = options.acceptanceQueryFile ? await readFile(path.resolve(options.acceptanceQueryFile), "utf8") : null;
591
+ const postDeployment = options.connectionId ? { connectionId: options.connectionId, connectivityType: options.connectionMode, timeoutSeconds: Number(options.timeout), acceptanceDax } : null;
592
+ const plan = await createDeploymentPlan({ client, workspaceId: options.workspaceId, workspaceName: options.workspaceName, solutionDirectory, semanticModelName: options.semanticModelName, reportName: options.reportName, allowUpdates: Boolean(options.allowUpdates), postDeployment });
593
+ const planPath = path.resolve(options.output ?? path.join(loaded.root, ".supervisual", "deploy-plan.json"));
594
+ await mkdir(path.dirname(planPath), { recursive: true });
595
+ await writeFile(planPath, `${JSON.stringify(plan, null, 2)}\n`);
596
+ output({ ...plan, planPath }, jsonEnabled(command));
597
+ if (plan.blockers.length) process.exitCode = 2;
598
+ return plan;
599
+ });
600
+
601
+ addJsonOption(deploy.command("apply [project]"))
602
+ .description("apply an exact deployment plan after explicit confirmation")
603
+ .requiredOption("--plan <file>", "deployment plan created by deploy plan")
604
+ .requiredOption("--confirm <plan-id>", "exact plan ID authorized by a human")
605
+ .option("--backup <directory>", "required backup destination for updates")
606
+ .option("--receipt <file>", "deployment progress receipt destination")
607
+ .option("--resume <file>", "resume from a receipt written by an interrupted apply")
608
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
609
+ .action(async (project = ".", options, command) => {
610
+ const loaded = await loadProject(projectFile(project));
611
+ const result = await applySavedDeployment({ loaded, planPath: options.plan, confirm: options.confirm, backup: options.backup, receipt: options.receipt, resume: options.resume, timeout: options.timeout });
612
+ output({ ok: result.accepted, ...result }, jsonEnabled(command));
613
+ if (!result.accepted) process.exitCode = 3;
614
+ return result;
615
+ });
616
+
617
+ const restore = deploy.command("restore").description("plan and apply an exact backup restoration");
618
+ addJsonOption(restore.command("plan"))
619
+ .description("verify a deployment backup and write an immutable restore plan")
620
+ .requiredOption("--backup <directory>", "backup directory containing backup-manifest.json")
621
+ .requiredOption("--workspace-id <id>", "immutable Fabric workspace ID")
622
+ .option("--workspace-name <name>", "expected workspace display name")
623
+ .option("--output <file>", "restore plan destination")
624
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
625
+ .action(async (options, command) => {
626
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
627
+ const plan = await createRestorePlan({ client, backupDirectory: path.resolve(options.backup), workspaceId: options.workspaceId, workspaceName: options.workspaceName });
628
+ const planPath = path.resolve(options.output ?? path.join(options.backup, "restore-plan.json"));
629
+ await mkdir(path.dirname(planPath), { recursive: true });
630
+ await writeFile(planPath, `${JSON.stringify(plan, null, 2)}\n`);
631
+ output({ ...plan, planPath }, jsonEnabled(command));
632
+ return plan;
633
+ });
634
+
635
+ addJsonOption(restore.command("apply"))
636
+ .description("restore exact backed-up definitions after explicit plan confirmation")
637
+ .requiredOption("--plan <file>", "restore plan created by deploy restore plan")
638
+ .requiredOption("--confirm <plan-id>", "exact restore plan ID authorized by a human")
639
+ .option("--timeout <seconds>", "Fabric operation timeout", "900")
640
+ .action(async (options, command) => {
641
+ const plan = JSON.parse(await readFile(path.resolve(options.plan), "utf8"));
642
+ const client = new FabricHttpClient({ timeoutSeconds: Number(options.timeout) });
643
+ const result = await applyRestorePlan({ client, plan, confirmPlanId: options.confirm });
644
+ output({ ok: false, ...result }, jsonEnabled(command));
645
+ process.exitCode = 3;
646
+ return result;
647
+ });
648
+
649
+ return program;
650
+ }
651
+
652
+ export async function main(argv = process.argv.slice(2)) {
653
+ const program = createProgram();
654
+ if (argv.length === 0) argv = ["--help"];
655
+ try {
656
+ await program.parseAsync(argv, { from: "user" });
657
+ } catch (error) {
658
+ if (error instanceof CommanderError && ["commander.helpDisplayed", "commander.version"].includes(error.code)) return;
659
+ throw error;
660
+ }
661
+ }
662
+
663
+ export { createProgram };