@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.
- package/.claude-plugin/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +28 -2
- package/.claude-plugin/skill-manifest.json +22 -6
- package/.plugin/plugin.json +1 -1
- package/AGENTS.md +53 -36
- package/CHANGELOG.md +310 -0
- package/README.md +77 -26
- package/bin/build-plugin.js +11 -4
- package/bin/cli.js +113 -16
- package/bin/commands/build-desktop.js +35 -16
- package/bin/commands/diff.js +31 -13
- package/bin/commands/install.js +7 -3
- package/bin/commands/lint.js +40 -26
- package/bin/commands/mcp-setup.js +3 -10
- package/bin/commands/update-check.js +403 -0
- package/bin/lib/generators/claude-plugin.js +162 -6
- package/bin/lib/generators/shared.js +29 -33
- package/bin/lib/mcp-config.js +168 -12
- package/bin/lib/skills.js +115 -27
- package/bin/postinstall.js +4 -2
- package/bin/utils/mcp-detect.js +2 -2
- package/commands/bi-start.md +197 -0
- package/commands/pbi-connect.md +43 -65
- package/commands/project-kickoff.md +393 -673
- package/commands/report-design.md +403 -0
- package/desktop-extension/manifest.json +3 -3
- package/package.json +7 -5
- package/skills/bi-start/SKILL.md +199 -0
- package/skills/bi-start/scripts/update-check.js +403 -0
- package/skills/pbi-connect/SKILL.md +45 -67
- package/skills/pbi-connect/scripts/update-check.js +403 -0
- package/skills/project-kickoff/SKILL.md +395 -675
- package/skills/project-kickoff/scripts/update-check.js +403 -0
- package/skills/report-design/SKILL.md +405 -0
- package/skills/report-design/references/cli-commands.md +184 -0
- package/skills/report-design/references/cli-setup.md +101 -0
- package/skills/report-design/references/close-write-open-pattern.md +80 -0
- package/skills/report-design/references/layouts/finance.md +65 -0
- package/skills/report-design/references/layouts/generic.md +46 -0
- package/skills/report-design/references/layouts/hr.md +48 -0
- package/skills/report-design/references/layouts/marketing.md +45 -0
- package/skills/report-design/references/layouts/operations.md +44 -0
- package/skills/report-design/references/layouts/sales.md +50 -0
- package/skills/report-design/references/native-visuals.md +341 -0
- package/skills/report-design/references/pbi-desktop-installation.md +87 -0
- package/skills/report-design/references/pbir-preview-activation.md +40 -0
- package/skills/report-design/references/slicer.md +89 -0
- package/skills/report-design/references/textbox.md +101 -0
- package/skills/report-design/references/themes/BISuperpowers.json +915 -0
- package/skills/report-design/references/troubleshooting.md +135 -0
- package/skills/report-design/references/visual-types.md +78 -0
- package/skills/report-design/scripts/apply-theme.js +243 -0
- package/skills/report-design/scripts/create-visual.js +878 -0
- package/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
- package/skills/report-design/scripts/update-check.js +403 -0
- package/skills/report-design/scripts/validate-pbir.js +322 -0
- package/src/content/base.md +12 -68
- package/src/content/mcp-requirements.json +0 -25
- package/src/content/routing.md +19 -74
- package/src/content/skills/bi-start.md +191 -0
- package/src/content/skills/pbi-connect.md +22 -65
- package/src/content/skills/project-kickoff.md +372 -673
- package/src/content/skills/report-design/SKILL.md +376 -0
- package/src/content/skills/report-design/references/cli-commands.md +184 -0
- package/src/content/skills/report-design/references/cli-setup.md +101 -0
- package/src/content/skills/report-design/references/close-write-open-pattern.md +80 -0
- package/src/content/skills/report-design/references/layouts/finance.md +65 -0
- package/src/content/skills/report-design/references/layouts/generic.md +46 -0
- package/src/content/skills/report-design/references/layouts/hr.md +48 -0
- package/src/content/skills/report-design/references/layouts/marketing.md +45 -0
- package/src/content/skills/report-design/references/layouts/operations.md +44 -0
- package/src/content/skills/report-design/references/layouts/sales.md +50 -0
- package/src/content/skills/report-design/references/native-visuals.md +341 -0
- package/src/content/skills/report-design/references/pbi-desktop-installation.md +87 -0
- package/src/content/skills/report-design/references/pbir-preview-activation.md +40 -0
- package/src/content/skills/report-design/references/slicer.md +89 -0
- package/src/content/skills/report-design/references/textbox.md +101 -0
- package/src/content/skills/report-design/references/themes/BISuperpowers.json +915 -0
- package/src/content/skills/report-design/references/troubleshooting.md +135 -0
- package/src/content/skills/report-design/references/visual-types.md +78 -0
- package/src/content/skills/report-design/scripts/apply-theme.js +243 -0
- package/src/content/skills/report-design/scripts/create-visual.js +878 -0
- package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
- package/src/content/skills/report-design/scripts/validate-pbir.js +322 -0
- package/bin/commands/install.test.js +0 -289
- package/bin/commands/lint.test.js +0 -103
- package/bin/lib/generators/claude-plugin.test.js +0 -111
- package/bin/lib/mcp-config.test.js +0 -310
- package/bin/lib/microsoft-mcp.test.js +0 -115
- package/bin/utils/mcp-detect.test.js +0 -81
- 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();
|