@luquimbo/bi-superpowers 3.2.0 → 4.1.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.
Files changed (91) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +28 -2
  3. package/.claude-plugin/skill-manifest.json +22 -6
  4. package/.plugin/plugin.json +1 -1
  5. package/AGENTS.md +53 -36
  6. package/CHANGELOG.md +310 -0
  7. package/README.md +77 -26
  8. package/bin/build-plugin.js +11 -4
  9. package/bin/cli.js +113 -16
  10. package/bin/commands/build-desktop.js +35 -16
  11. package/bin/commands/diff.js +31 -13
  12. package/bin/commands/install.js +7 -3
  13. package/bin/commands/lint.js +40 -26
  14. package/bin/commands/mcp-setup.js +3 -10
  15. package/bin/commands/update-check.js +403 -0
  16. package/bin/lib/generators/claude-plugin.js +162 -6
  17. package/bin/lib/generators/shared.js +29 -33
  18. package/bin/lib/mcp-config.js +168 -12
  19. package/bin/lib/skills.js +115 -27
  20. package/bin/postinstall.js +4 -2
  21. package/bin/utils/mcp-detect.js +2 -2
  22. package/commands/bi-start.md +197 -0
  23. package/commands/pbi-connect.md +43 -65
  24. package/commands/project-kickoff.md +393 -673
  25. package/commands/report-design.md +403 -0
  26. package/desktop-extension/manifest.json +3 -3
  27. package/package.json +7 -5
  28. package/skills/bi-start/SKILL.md +199 -0
  29. package/skills/bi-start/scripts/update-check.js +403 -0
  30. package/skills/pbi-connect/SKILL.md +45 -67
  31. package/skills/pbi-connect/scripts/update-check.js +403 -0
  32. package/skills/project-kickoff/SKILL.md +395 -675
  33. package/skills/project-kickoff/scripts/update-check.js +403 -0
  34. package/skills/report-design/SKILL.md +405 -0
  35. package/skills/report-design/references/cli-commands.md +184 -0
  36. package/skills/report-design/references/cli-setup.md +101 -0
  37. package/skills/report-design/references/close-write-open-pattern.md +80 -0
  38. package/skills/report-design/references/layouts/finance.md +65 -0
  39. package/skills/report-design/references/layouts/generic.md +46 -0
  40. package/skills/report-design/references/layouts/hr.md +48 -0
  41. package/skills/report-design/references/layouts/marketing.md +45 -0
  42. package/skills/report-design/references/layouts/operations.md +44 -0
  43. package/skills/report-design/references/layouts/sales.md +50 -0
  44. package/skills/report-design/references/native-visuals.md +341 -0
  45. package/skills/report-design/references/pbi-desktop-installation.md +87 -0
  46. package/skills/report-design/references/pbir-preview-activation.md +40 -0
  47. package/skills/report-design/references/slicer.md +89 -0
  48. package/skills/report-design/references/textbox.md +101 -0
  49. package/skills/report-design/references/themes/BISuperpowers.json +915 -0
  50. package/skills/report-design/references/troubleshooting.md +135 -0
  51. package/skills/report-design/references/visual-types.md +78 -0
  52. package/skills/report-design/scripts/apply-theme.js +243 -0
  53. package/skills/report-design/scripts/create-visual.js +878 -0
  54. package/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  55. package/skills/report-design/scripts/update-check.js +403 -0
  56. package/skills/report-design/scripts/validate-pbir.js +322 -0
  57. package/src/content/base.md +12 -68
  58. package/src/content/mcp-requirements.json +0 -25
  59. package/src/content/routing.md +19 -74
  60. package/src/content/skills/bi-start.md +191 -0
  61. package/src/content/skills/pbi-connect.md +22 -65
  62. package/src/content/skills/project-kickoff.md +372 -673
  63. package/src/content/skills/report-design/SKILL.md +376 -0
  64. package/src/content/skills/report-design/references/cli-commands.md +184 -0
  65. package/src/content/skills/report-design/references/cli-setup.md +101 -0
  66. package/src/content/skills/report-design/references/close-write-open-pattern.md +80 -0
  67. package/src/content/skills/report-design/references/layouts/finance.md +65 -0
  68. package/src/content/skills/report-design/references/layouts/generic.md +46 -0
  69. package/src/content/skills/report-design/references/layouts/hr.md +48 -0
  70. package/src/content/skills/report-design/references/layouts/marketing.md +45 -0
  71. package/src/content/skills/report-design/references/layouts/operations.md +44 -0
  72. package/src/content/skills/report-design/references/layouts/sales.md +50 -0
  73. package/src/content/skills/report-design/references/native-visuals.md +341 -0
  74. package/src/content/skills/report-design/references/pbi-desktop-installation.md +87 -0
  75. package/src/content/skills/report-design/references/pbir-preview-activation.md +40 -0
  76. package/src/content/skills/report-design/references/slicer.md +89 -0
  77. package/src/content/skills/report-design/references/textbox.md +101 -0
  78. package/src/content/skills/report-design/references/themes/BISuperpowers.json +915 -0
  79. package/src/content/skills/report-design/references/troubleshooting.md +135 -0
  80. package/src/content/skills/report-design/references/visual-types.md +78 -0
  81. package/src/content/skills/report-design/scripts/apply-theme.js +243 -0
  82. package/src/content/skills/report-design/scripts/create-visual.js +878 -0
  83. package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  84. package/src/content/skills/report-design/scripts/validate-pbir.js +322 -0
  85. package/bin/commands/install.test.js +0 -289
  86. package/bin/commands/lint.test.js +0 -103
  87. package/bin/lib/generators/claude-plugin.test.js +0 -111
  88. package/bin/lib/mcp-config.test.js +0 -310
  89. package/bin/lib/microsoft-mcp.test.js +0 -115
  90. package/bin/utils/mcp-detect.test.js +0 -81
  91. package/bin/utils/tui.test.js +0 -127
@@ -0,0 +1,135 @@
1
+ # Report design troubleshooting
2
+
3
+ Common failure modes when generating reports via `pbi-cli-tool` + the close-write-open pattern, and how to resolve them.
4
+
5
+ ## Install / environment
6
+
7
+ ### `pbi: command not found`
8
+
9
+ `pbi-cli-tool` is not installed or `pipx` bin dir is not on PATH.
10
+
11
+ 1. Install: `pipx install pbi-cli-tool`
12
+ 2. If still missing: `python -m pipx ensurepath`
13
+ 3. **Close and reopen the terminal** to pick up the new PATH entries.
14
+
15
+ ### `Error: pywin32 is not installed. Install with: pip install pywin32`
16
+
17
+ You installed `pywin32` into the wrong Python environment. The CLI runs in its own isolated `pipx` venv and doesn't see user-site packages.
18
+
19
+ Fix:
20
+
21
+ ```bash
22
+ python -m pipx inject pbi-cli-tool pywin32
23
+ ```
24
+
25
+ ### `pipx --version` works but `pbi --version` says command not found
26
+
27
+ PATH race after `ensurepath`. Close and reopen the terminal. If still broken:
28
+
29
+ ```bash
30
+ python -m pipx list
31
+ ```
32
+
33
+ Confirm `pbi-cli-tool` is listed. If not, install it again. Look for the "apps are available" line that includes `pbi.exe`.
34
+
35
+ ### `Environment not ready` from `pbi setup`
36
+
37
+ Output usually names the specific check that failed. Common causes:
38
+ - Python version too old (need 3.10+)
39
+ - Missing Microsoft DLLs — re-run `pipx install pbi-cli-tool --force`
40
+ - No Power BI Desktop running — setup requires at least one .pbip open
41
+
42
+ ## Connection
43
+
44
+ ### `pbi connect` says "No Power BI Desktop instance found"
45
+
46
+ Desktop is not running, or the `.pbip` hasn't finished loading yet. Open Desktop with a .pbip, wait for the model to fully load (status bar shows "Connected"), then retry `pbi connect`.
47
+
48
+ ### `pbi connect` succeeds but `pbi measure list` returns empty
49
+
50
+ The connected Desktop instance isn't loaded with a model, or the model has no measures yet. Check with `pbi table list` — if tables exist but measures don't, the user hasn't created any measures yet. Stop and ask them to add at least one (via PBI Desktop UI or `/project-kickoff`).
51
+
52
+ ## Generation failures
53
+
54
+ ### Visuals don't render after relaunch
55
+
56
+ The #1 cause is **not actually doing the close-write-open cycle**. Checks:
57
+
58
+ 1. Did `PBIDesktop.exe` actually die before the writes? Run `tasklist | grep PBIDesktop` before and after the kill. If before shows a PID and after still shows it, the kill failed (permission issue, maybe).
59
+ 2. Did `Start-Process` launch a *new* process? Its PID must differ from the one you killed. Compare.
60
+ 3. Does `pbi report validate` report `valid: True`? If not, the report won't load visuals that have invalid JSON.
61
+
62
+ If all three are correct and visuals still don't render, see `close-write-open-pattern.md` — there might be a specific gotcha for the user's system (AV scanner, path locking).
63
+
64
+ ### `pbi report validate` errors
65
+
66
+ Typical causes:
67
+
68
+ - **"Binding references a measure/column that doesn't exist"** → the `Table[Measure]` or `Table[Column]` you passed to `pbi visual bind` doesn't match the model. Rerun `pbi measure list` / `pbi column list --table X` and use the exact names.
69
+ - **"Page name collision"** → you tried to add a page with the same `--name` as an existing one. Use a different name or delete the existing page first (`pbi report delete-page`).
70
+ - **"Visual of that name already exists"** → same rule, different object.
71
+
72
+ ### "Visual rendered but shows (Blank) or #REF"
73
+
74
+ The binding resolved at write time but the data returns nothing at query time. Usually:
75
+ - The measure filters to zero rows with the current page filters
76
+ - The column you bound as `--category` doesn't have data for the measure's grain
77
+ - You bound to a calculated column that depends on a hidden measure
78
+
79
+ Fix: change the binding to a better-grained dim column, or add a date filter to the page.
80
+
81
+ ### Bar chart shows `barChart` but we specified `bar_chart`
82
+
83
+ Expected — the `--type bar_chart` is a user-friendly alias. The CLI writes `"visualType": "barChart"` to disk. Desktop uses the raw ID. This is fine, not a bug.
84
+
85
+ ## After relaunch
86
+
87
+ ### Desktop crashes or hangs after relaunch
88
+
89
+ Usually an AV scanner quarantining the .pbip during reopen. Wait 30s, relaunch. If persistent, add the project folder to AV exclusions.
90
+
91
+ ### Only some visuals render
92
+
93
+ Race condition: Desktop started loading while the CLI was still writing. Can happen if `Start-Process` is called before the last CLI write finished.
94
+
95
+ Fix:
96
+ 1. Wait 2-3 seconds after the last CLI command before launching Desktop
97
+ 2. Or run `pbi report validate` as a barrier — it won't complete until all writes are flushed
98
+
99
+ ### A visual renders but at wrong position
100
+
101
+ The CLI may have ignored one of your `--x`/`--y`/`--width`/`--height` flags, or you passed them without `--no-sync` during a phase where Desktop was still running and it normalized the position.
102
+
103
+ Fix with `pbi visual update`:
104
+
105
+ ```bash
106
+ pbi visual --no-sync update "{name}" --page "{page}" --x 16 --y 16 --width 280 --height 120
107
+ ```
108
+
109
+ Then close-write-open again to see the change.
110
+
111
+ ### User says "I don't see the changes" after relaunch
112
+
113
+ Did they open the .pbip that we wrote to, or a different one? Confirm the full path:
114
+
115
+ ```
116
+ El archivo que tenía que abrir era:
117
+ C:\...\pbip-files\{projectName}.pbip
118
+
119
+ ¿Es el que está viendo?
120
+ ```
121
+
122
+ ## When in doubt: checkpoint + retry
123
+
124
+ If things get confusing:
125
+
126
+ 1. `pbi report validate` — confirms disk state
127
+ 2. `pbi visual list --page X` — confirms which visuals exist per page
128
+ 3. Full close-write-open cycle once more
129
+
130
+ If that doesn't resolve it, capture:
131
+ - Output of the last few CLI commands (especially any error text)
132
+ - `tasklist | grep PBIDesktop` — process state
133
+ - File listing of `./pbip-files/{proj}.Report/definition/pages/`
134
+
135
+ And hand off to the user for a manual debug session.
@@ -0,0 +1,78 @@
1
+ # PBIR visualType IDs — what works, what doesn't (PBI Desktop March 2026)
2
+
3
+ The `pbi visual add --type` flag accepts a small set of friendly aliases. When you write `visual.json` directly (e.g. for textbox/slicer or for a chart variant the CLI doesn't expose), you set `visual.visualType` to a raw PBIR typeId.
4
+
5
+ **The problem**: PBIR's JSON schema accepts MANY typeIds (`pbi report validate` returns `valid: True`), but PBI Desktop only renders a subset of them. The rest fail silently — the visual container appears empty, no error toast.
6
+
7
+ This doc lists the typeIds we've personally tested in `examples/smoke-test/` against PBI Desktop standalone. Treat the **NOT NATIVE** column as authoritative for this Desktop build; if Microsoft adds support in a later build, update the table.
8
+
9
+ ## Confirmed native (renders correctly)
10
+
11
+ | visualType | Use for | Notes |
12
+ |---|---|---|
13
+ | `card` | Single KPI value | |
14
+ | `cardVisual` | New card visual (multi-row, image, reference label) | Mar 2026 — schema overrides risky, prefer `card` until validated |
15
+ | `multiRowCard` | Compact KPI list | |
16
+ | `kpi` | KPI with goal + trend | Use `directionGood: Positive` in theme |
17
+ | `gauge` | Circular gauge | |
18
+ | `slicer` | Default slicer (list / dropdown) | Modes via `mode` property — see `slicer.md` |
19
+ | `advancedSlicerVisual` | New slicer (Mar 2026 unified slicer) | Modes via `mode` |
20
+ | `listSlicer` | List-only slicer | Older typeId |
21
+ | `textSlicer` | Text input slicer | |
22
+ | `barChart` | Horizontal bars | Add `Series` field for stacking |
23
+ | `clusteredBarChart` | Side-by-side horizontal bars | |
24
+ | `columnChart` | Vertical bars | Add `Series` for stacking |
25
+ | `clusteredColumnChart` | Side-by-side vertical bars | |
26
+ | `hundredPercentStackedBarChart` | 100% horizontal | |
27
+ | `hundredPercentStackedColumnChart` | 100% vertical | |
28
+ | `lineChart` | Time series | |
29
+ | `areaChart` | Area | Add `Series` for stacking |
30
+ | `lineClusteredColumnComboChart` | Line + clustered columns | The only combo variant that works native |
31
+ | `pieChart` | Single-level part-to-whole | |
32
+ | `donutChart` | Pie with hole | |
33
+ | `treemap` | Hierarchical part-to-whole | |
34
+ | `funnelChart` | Funnel / pipeline | |
35
+ | `scatterChart` | X/Y/Size correlation | |
36
+ | `waterfallChart` | Increase/decrease bridges | |
37
+ | `tableEx` | Modern table | |
38
+ | `pivotTable` | Matrix / pivot | |
39
+ | `textbox` | Static text panels | Schema in `textbox.md` |
40
+
41
+ ## NOT NATIVE — passes `validate` but Desktop won't render
42
+
43
+ If you write any of these, the visual container appears EMPTY in Desktop. No error message. Use the workaround instead.
44
+
45
+ | visualType (DON'T USE) | Replace with | Workaround note |
46
+ |---|---|---|
47
+ | `stackedBarChart` | `barChart` | Add a `Series` (Legend) field — Desktop auto-stacks when a series is present |
48
+ | `stackedColumnChart` | `columnChart` | Same workaround — Series field triggers stacking |
49
+ | `stackedAreaChart` | `areaChart` | Series field stacks the areas |
50
+ | `lineStackedColumnComboChart` | `lineClusteredColumnComboChart` | The clustered variant works; the stacked variant does not |
51
+ | `funnel` | `funnelChart` | The bare `funnel` typeId is from older PBIR; modern Desktop uses `funnelChart` |
52
+ | `ribbonChart` | (test before using) | Untested as of Mar 2026 — likely needs Power BI Visuals preview flag |
53
+
54
+ ## Maps (special case — need geo data)
55
+
56
+ | visualType | Renders without bindings? |
57
+ |---|---|
58
+ | `map` | yes (placeholder/world map) |
59
+ | `filledMap` | yes |
60
+ | `azureMap` | yes (requires Azure Maps preview enabled in Desktop options) |
61
+ | `shapeMap` | needs preview flag — many users have it off |
62
+
63
+ For a model without geo columns, the map visual still draws an empty world — useful for theme galleries, useless for data.
64
+
65
+ ## Source of this list
66
+
67
+ - Personally tested in `examples/smoke-test/pbip-files/super-test.Report/definition/pages/theme/` against PBI Desktop standalone (build 2.152.x, March 2026).
68
+ - The `stackedBarChart` finding was first captured in commit `0243acf` (B4 fix). The `stackedAreaChart` and `lineStackedColumnComboChart` findings came from the smoke test "Galería de Visuales" iteration.
69
+ - If you discover a new typeId that fails silently or works unexpectedly, update this table and reference it from `SKILL.md` PHASE 4.2.
70
+
71
+ ## How to verify a typeId yourself
72
+
73
+ 1. Write a minimal `visual.json` with the typeId in question + a known-good binding.
74
+ 2. Run `pbi report validate` — must return `valid: True`. (If it fails, the typeId is wrong-spelled.)
75
+ 3. Close-write-open: kill PBIDesktop, save, relaunch via `cmd /c start "" "{exePath}" "{pbipPath}"`.
76
+ 4. Open the page. If the container is empty (no chart, no error), the typeId is **NOT NATIVE**. Add it to the bottom table above with the workaround.
77
+
78
+ This is cheap to test — under 30s per typeId — and the only way to know for sure, since Microsoft doesn't publish a definitive list of supported typeIds per Desktop build.
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * apply-theme.js — register a custom PBIR theme correctly
4
+ *
5
+ * Power BI Desktop March 2026 (build 2.152.x) rejects the report.json
6
+ * shape that `pbi report set-theme` writes for custom themes:
7
+ *
8
+ * - It writes `themeCollection.customTheme.reportVersionAtImport`
9
+ * with the wrong type (string instead of {visual, report, page}).
10
+ * - It writes `resourcePackages[].items[].type` with the wrong value
11
+ * ("RegisteredResources" instead of "CustomTheme").
12
+ *
13
+ * Both errors block the .pbip from opening. This script bypasses that
14
+ * regression and writes the canonical PBIR shape verified against the
15
+ * K201-MonthSlicer.Report example shipped with Kurt Buhler's pbip
16
+ * skill (research/.../K201-MonthSlicer.Report).
17
+ *
18
+ * Canonical shape produced:
19
+ *
20
+ * themeCollection.customTheme = {
21
+ * name: "<themeFileName>.json",
22
+ * reportVersionAtImport: { visual, report, page }, // copied from baseTheme
23
+ * type: "RegisteredResources" // package name (where it lives)
24
+ * }
25
+ *
26
+ * resourcePackages += {
27
+ * name: "RegisteredResources",
28
+ * type: "RegisteredResources",
29
+ * items: [
30
+ * { name: "<themeFileName>.json", path: "<themeFileName>.json", type: "CustomTheme" }
31
+ * ]
32
+ * }
33
+ *
34
+ * <reportPath>/StaticResources/RegisteredResources/<themeFileName>.json <- physical theme JSON
35
+ *
36
+ * Usage:
37
+ * node apply-theme.js --report-path ./pbip-files/MyProj.Report \
38
+ * --theme-file ./references/themes/BISuperpowers.json
39
+ *
40
+ * Idempotent: re-running with the same theme file replaces the existing
41
+ * customTheme entry in place; re-running with a different theme replaces
42
+ * the previous custom theme so only one is registered at a time.
43
+ */
44
+
45
+ const fs = require('fs');
46
+ const path = require('path');
47
+
48
+ function fail(msg) {
49
+ process.stderr.write(`apply-theme: ${msg}\n`);
50
+ process.exit(1);
51
+ }
52
+
53
+ function parseArgs(argv) {
54
+ const out = {};
55
+ for (let i = 0; i < argv.length; i += 1) {
56
+ const a = argv[i];
57
+ if (a === '--report-path' || a === '-p') out.reportPath = argv[++i];
58
+ else if (a === '--theme-file' || a === '-f') out.themeFile = argv[++i];
59
+ else if (a === '--name' || a === '-n') out.name = argv[++i];
60
+ else if (a === '--help' || a === '-h') out.help = true;
61
+ else fail(`unknown argument: ${a}`);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ function help() {
67
+ process.stdout.write(
68
+ [
69
+ 'Usage: apply-theme.js --report-path <path> --theme-file <path> [--name <name>]',
70
+ '',
71
+ 'Options:',
72
+ ' -p, --report-path Path to the .Report folder (e.g. ./pbip-files/MyProj.Report).',
73
+ ' -f, --theme-file Path to the theme JSON file to register.',
74
+ ' -n, --name Optional override for the registered file name (defaults to basename of --theme-file).',
75
+ ' -h, --help Show this help.',
76
+ '',
77
+ ].join('\n')
78
+ );
79
+ }
80
+
81
+ function readJson(filePath, label) {
82
+ let raw;
83
+ try {
84
+ raw = fs.readFileSync(filePath, 'utf8');
85
+ } catch (err) {
86
+ fail(`could not read ${label} (${filePath}): ${err.message}`);
87
+ }
88
+ try {
89
+ return JSON.parse(raw);
90
+ } catch (err) {
91
+ fail(`${label} is not valid JSON (${filePath}): ${err.message}`);
92
+ }
93
+ return null; // unreachable
94
+ }
95
+
96
+ function writeJsonAtomic(filePath, data) {
97
+ // Backup once if a file already exists, write to .tmp, atomic rename.
98
+ // Same pattern as bin/lib/mcp-config.js writeFileAtomic — protects
99
+ // against half-written report.json on crash mid-write.
100
+ const dir = path.dirname(filePath);
101
+ fs.mkdirSync(dir, { recursive: true });
102
+ if (fs.existsSync(filePath)) {
103
+ fs.copyFileSync(filePath, `${filePath}.bak`);
104
+ }
105
+ const tmp = `${filePath}.tmp`;
106
+ try {
107
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
108
+ fs.renameSync(tmp, filePath);
109
+ } catch (err) {
110
+ try {
111
+ fs.unlinkSync(tmp);
112
+ } catch (_) {
113
+ // best-effort cleanup
114
+ }
115
+ throw err;
116
+ }
117
+ }
118
+
119
+ function main() {
120
+ const args = parseArgs(process.argv.slice(2));
121
+ if (args.help) {
122
+ help();
123
+ return;
124
+ }
125
+ if (!args.reportPath) fail('--report-path is required');
126
+ if (!args.themeFile) fail('--theme-file is required');
127
+
128
+ const reportPath = path.resolve(args.reportPath);
129
+ const themeFile = path.resolve(args.themeFile);
130
+
131
+ if (!fs.existsSync(reportPath)) fail(`report path not found: ${reportPath}`);
132
+ if (!fs.existsSync(themeFile)) fail(`theme file not found: ${themeFile}`);
133
+
134
+ const reportJsonPath = path.join(reportPath, 'definition', 'report.json');
135
+ if (!fs.existsSync(reportJsonPath))
136
+ fail(`expected ${reportJsonPath} to exist; is this a .Report folder?`);
137
+
138
+ const reportJson = readJson(reportJsonPath, 'report.json');
139
+ // Sanity-validate the theme JSON. PBI themes always have a `name` field.
140
+ const themeJson = readJson(themeFile, 'theme JSON');
141
+ if (typeof themeJson.name !== 'string' || themeJson.name.length === 0)
142
+ fail('theme JSON must contain a non-empty top-level "name" field');
143
+
144
+ // Resolve the canonical registered file name. Default to the basename
145
+ // of the input file so paths in report.json stay stable across reruns.
146
+ const registeredName = args.name || path.basename(themeFile);
147
+ if (!/\.json$/i.test(registeredName))
148
+ fail(`registered name must end with .json (got: ${registeredName})`);
149
+ // Reject path separators to avoid accidental writes outside the
150
+ // StaticResources/RegisteredResources/ directory via --name.
151
+ if (/[/\\]/.test(registeredName))
152
+ fail(`registered name must not contain path separators (got: ${registeredName})`);
153
+
154
+ // Copy the theme into StaticResources/RegisteredResources/.
155
+ const registeredDir = path.join(reportPath, 'StaticResources', 'RegisteredResources');
156
+ fs.mkdirSync(registeredDir, { recursive: true });
157
+ const registeredFile = path.join(registeredDir, registeredName);
158
+ fs.copyFileSync(themeFile, registeredFile);
159
+
160
+ // Determine reportVersionAtImport for customTheme. We mirror the
161
+ // baseTheme value so the version stamp matches the schema the report
162
+ // is currently running. This is what Desktop expects (verified against
163
+ // K201-MonthSlicer.Report).
164
+ const baseVersion =
165
+ reportJson &&
166
+ reportJson.themeCollection &&
167
+ reportJson.themeCollection.baseTheme &&
168
+ reportJson.themeCollection.baseTheme.reportVersionAtImport;
169
+
170
+ if (
171
+ !baseVersion ||
172
+ typeof baseVersion.visual !== 'string' ||
173
+ typeof baseVersion.report !== 'string' ||
174
+ typeof baseVersion.page !== 'string'
175
+ ) {
176
+ fail(
177
+ 'report.json themeCollection.baseTheme.reportVersionAtImport is missing or malformed; ' +
178
+ 'cannot derive customTheme version. Open the .pbip in Desktop once to let it write a clean baseTheme block, then retry.'
179
+ );
180
+ }
181
+
182
+ // 1) Patch themeCollection.customTheme
183
+ reportJson.themeCollection = reportJson.themeCollection || {};
184
+ reportJson.themeCollection.customTheme = {
185
+ name: registeredName,
186
+ reportVersionAtImport: {
187
+ visual: baseVersion.visual,
188
+ report: baseVersion.report,
189
+ page: baseVersion.page,
190
+ },
191
+ type: 'RegisteredResources',
192
+ };
193
+
194
+ // 2) Patch resourcePackages — find or create the RegisteredResources package.
195
+ reportJson.resourcePackages = Array.isArray(reportJson.resourcePackages)
196
+ ? reportJson.resourcePackages
197
+ : [];
198
+ let registered = reportJson.resourcePackages.find(
199
+ (p) => p && p.name === 'RegisteredResources' && p.type === 'RegisteredResources'
200
+ );
201
+ if (!registered) {
202
+ registered = { name: 'RegisteredResources', type: 'RegisteredResources', items: [] };
203
+ reportJson.resourcePackages.push(registered);
204
+ }
205
+ registered.items = Array.isArray(registered.items) ? registered.items : [];
206
+
207
+ // Replace (or insert) the CustomTheme item for this registered file.
208
+ // Drop any other CustomTheme items to keep a single active custom theme,
209
+ // matching how Desktop handles it through the UI.
210
+ const otherItems = registered.items.filter(
211
+ (item) => !item || item.type !== 'CustomTheme'
212
+ );
213
+ registered.items = [
214
+ ...otherItems,
215
+ {
216
+ name: registeredName,
217
+ path: registeredName,
218
+ type: 'CustomTheme',
219
+ },
220
+ ];
221
+
222
+ // 3) Write report.json atomically.
223
+ writeJsonAtomic(reportJsonPath, reportJson);
224
+
225
+ process.stdout.write(
226
+ [
227
+ 'apply-theme: ✓ custom theme registered',
228
+ ` theme file: ${themeFile}`,
229
+ ` copied to: ${registeredFile}`,
230
+ ` report.json: ${reportJsonPath}`,
231
+ ` customTheme: ${registeredName} (type: RegisteredResources)`,
232
+ ` versions: visual ${baseVersion.visual} / report ${baseVersion.report} / page ${baseVersion.page}`,
233
+ '',
234
+ 'Next:',
235
+ ' 1. pbi report -p "<reportPath>" validate',
236
+ ' 2. close PBI Desktop fully (taskkill /IM PBIDesktop.exe /F),',
237
+ ' 3. relaunch the .pbip via the standalone exe (see references/pbi-desktop-installation.md).',
238
+ '',
239
+ ].join('\n')
240
+ );
241
+ }
242
+
243
+ main();