Compare commits

..

74 commits

Author SHA1 Message Date
github-actions[bot]
6671155ec7 chore: refresh downloads chart
Some checks are pending
CI / test (windows-latest, 3.10) (push) Waiting to run
CI / test (windows-latest, 3.12) (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / test (windows-latest, 3.13) (push) Waiting to run
CI / lint (push) Waiting to run
2026-04-21 06:56:46 +00:00
github-actions[bot]
61aa3f4994 chore: refresh downloads chart
Some checks are pending
CI / test (windows-latest, 3.13) (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / test (windows-latest, 3.10) (push) Waiting to run
CI / test (windows-latest, 3.12) (push) Waiting to run
2026-04-20 07:04:46 +00:00
github-actions[bot]
68c6611e30 chore: refresh downloads chart
Some checks are pending
CI / test (windows-latest, 3.13) (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / test (windows-latest, 3.10) (push) Waiting to run
CI / test (windows-latest, 3.12) (push) Waiting to run
2026-04-19 06:51:52 +00:00
github-actions[bot]
890eafb883 chore: refresh downloads chart
Some checks are pending
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / test (windows-latest, 3.10) (push) Waiting to run
CI / test (windows-latest, 3.12) (push) Waiting to run
CI / test (windows-latest, 3.13) (push) Waiting to run
2026-04-18 06:42:41 +00:00
github-actions[bot]
d1bb583274 chore: refresh downloads chart 2026-04-17 06:56:36 +00:00
github-actions[bot]
04447e5a15 chore: refresh downloads chart 2026-04-16 06:56:14 +00:00
github-actions[bot]
2f1ad3b06e chore: refresh downloads chart 2026-04-15 06:55:24 +00:00
github-actions[bot]
d9851295e8 chore: refresh downloads chart 2026-04-14 06:54:54 +00:00
github-actions[bot]
f3ab0aaa76 chore: refresh downloads chart 2026-04-13 07:03:12 +00:00
github-actions[bot]
a81cf9cd5b chore: refresh downloads chart 2026-04-12 10:28:56 +00:00
MinaSaad1
3c6261dc9b fix: use PAT to bypass branch protection in downloads chart workflow 2026-04-12 12:28:37 +02:00
MinaSaad1
6642cf12d2 fix: use unicode chars instead of html entities in downloads chart
• and → are HTML entities, not valid XML, so GitHub's
image renderer failed to load the SVG and showed a broken image.
Replaced with literal U+2022 and U+2192 characters.
2026-04-10 22:09:18 +02:00
MinaSaad1
c6bc1b9338 chore: bump checkout/setup-python in downloads-chart workflow
Node.js 20 versions (checkout@v4, setup-python@v5) are deprecated.
2026-04-10 22:08:25 +02:00
MinaSaad1
cb71ba1cf7 feat: add cumulative downloads chart to README
- scripts/generate_downloads_chart.py fetches pypistats.org data
  (mirrors excluded, stdlib only) and renders a dark-theme SVG
  matching the existing asset style
- assets/downloads-chart.svg seeded with the current 15-day history
- .github/workflows/downloads-chart.yml runs daily at 06:15 UTC
  and commits only when the chart actually changes
- README shows the chart directly under stats.svg
2026-04-10 22:06:32 +02:00
MinaSaad1
ff96cc3f3b chore: bump version to 3.10.10 2026-04-07 22:42:10 +02:00
MinaSaad1
b723a134a7 chore: bump version to 3.10.9 2026-04-07 22:24:16 +02:00
MinaSaad1
f7ca7d87e7 docs: add --no-sync batching guide to report, pages, visuals, filters skills 2026-04-07 22:15:51 +02:00
MinaSaad1
dcb48fde7c chore: bump version to 3.10.8 2026-04-07 21:24:02 +02:00
MinaSaad1
e677b018cf fix: apply ruff format to desktop_reload.py 2026-04-07 21:21:46 +02:00
MinaSaad1
849d309228 fix: fix PBI Desktop window detection and fallback chain in desktop_reload
- _find_pbi_window_pywin32: also match by PBIDesktop.exe process name so
  newer Desktop versions that title windows with just the report name are
  found correctly
- _try_pywin32: return None (not an error dict) when window is not found,
  so reload_desktop() falls through to the PowerShell fallback as intended
- bump version to 3.10.7; sync __init__.py (was stale at 3.10.5)
2026-04-07 21:18:10 +02:00
MinaSaad1
93c4275848 fix: wrap long lines in tests to pass ruff E501 (>100 chars) 2026-04-07 17:17:13 +02:00
MinaSaad1
895e90d710 fix: correct 7 PBIR report-layer issues found during Desktop testing
- visual bind: remove legacy Commands/SemanticQueryDataShapeCommand block
  (PBIR 2.7.0 uses additionalProperties:false -- Commands is a hard schema error)
- visual bind: add active:true to column (category/row/detail) projections
  so Desktop treats the field as the active axis
- visual add: remove empty "objects:{}" from all 32 visual templates
  (noisy and rejected by strict schema validators)
- visual add: write position coordinates as integers not floats
  (Desktop normalises to int; 320.0 vs 320 caused inconsistency)
- report set-background: always write transparency:0 alongside color
  (Desktop defaults missing transparency to 100 = fully invisible)
- report validate: drop false-positive layoutOptimization required error
  (real Microsoft 3.2.0 schema does not require this field)
- all write commands: add --no-sync flag to report/visual/filters/bookmarks
  groups to suppress per-command Desktop reload during scripted builds;
  use pbi report reload for a single sync at the end
2026-04-07 17:13:41 +02:00
MinaSaad1
63f4738a2e fix: use official SVG art for ASCII banner, correct I/L rendering 2026-04-06 00:24:31 +02:00
MinaSaad1
acf82b006a chore: bump version to 3.10.4 2026-04-06 00:00:46 +02:00
MinaSaad1
c1d8176a6c fix: apply ruff format to banner.py 2026-04-05 23:34:29 +02:00
MinaSaad1
2dee744bef feat: show ASCII banner when pbi is invoked with no subcommand 2026-04-05 23:31:08 +02:00
MinaSaad1
700c489988 chore: exclude entire docs/ folder from git tracking 2026-04-05 23:06:48 +02:00
MinaSaad1
b83e710fcf fix: restore banner to VIBE BI header + PBI-CLI block art connecting Claude and Power BI 2026-04-05 22:58:01 +02:00
MinaSaad1
756a7c98e3 fix: revert all SVG assets to pre-marketing state
Remove the VIBE BI block-art header and marketing branding that was
added to all 17 assets in v3.10.3. Restore each SVG to its original
content focused on the feature it illustrates (chat demo, DAX
debugging, architecture flow, etc.) without the promotional overlay.
2026-04-05 22:52:35 +02:00
MinaSaad1
a1f806849e chore: untrack marketing folder and unused assets from git
Remove marketing/ directory (51 files) and 19 unused asset files
that are not referenced in README.md from git tracking. Update
.gitignore to prevent re-adding them.

Only the 17 SVGs actually used in README are now tracked in assets/.
2026-04-05 22:44:48 +02:00
MinaSaad1
5f40a4281c fix: apply ruff format to connection.py and skills_cmd.py 2026-04-05 22:32:38 +02:00
MinaSaad1
62706771b2 fix: sort imports in connection.py to satisfy ruff isort 2026-04-05 22:30:35 +02:00
MinaSaad1
8b7bef9e6d fix: resolve CI failures from _ensure_ready removal
- connection.py: move skills_cmd import to module level (fixes ruff I001)
- setup_cmd.py: remove dead _ensure_ready import and call (fixes mypy attr-defined)
- tests/conftest.py: remove _ensure_ready monkeypatch (function no longer exists)
- tests/test_commands/test_setup.py: remove _ensure_ready patch
2026-04-05 22:27:40 +02:00
MinaSaad1
5819c41ddd chore: migrate large PNG assets to git lfs 2026-04-05 21:46:17 +02:00
MinaSaad1
62680dd060 feat: v3.10.3 - opt-in Claude integration, dual-license DLL attribution, new pbi-cli entry point
## Claude Code integration now fully opt-in (Fix 3)
- `pbi connect` no longer writes to ~/.claude/ automatically
- New `pbi-cli` entry point: `pbi-cli skills install/uninstall/list`
- `pbi-cli skills install` shows exact paths before writing and requires y/N confirmation
- `pbi connect` prints a one-line tip if skills are not yet installed
- `pbi skills` subgroup removed from the `pbi` entry point

## DLL licensing compliance (Fix 1)
- pyproject.toml updated to PEP 639 SPDX dual expression:
  MIT AND LicenseRef-Microsoft-AS-Client-Libraries
- license-files declaration: LICENSE, THIRD_PARTY_LICENSES.md, NOTICE
- THIRD_PARTY_LICENSES.md: full verbatim MS Analysis Services Client Libraries EULA
- NOTICE: short-form attribution for wheel redistribution
- src/pbi_cli/dlls/README.md: in-directory sentinel for the MS DLLs
- setuptools requirement bumped to >=77.0 for PEP 639 support

## SECURITY.md rewrite (Fix 2)
- Supported versions table updated to 3.10.x
- Architecture section: no MCP server, no subprocess, direct pythonnet interop
- Global Configuration Modifications section updated to reflect opt-in model
- Bundled Binaries section references THIRD_PARTY_LICENSES.md

## Documentation
- README.md, README.pypi.md: corrected 3-step setup flow
- CHANGELOG.md: [3.10.3] entry
- CONTRIBUTING.md: pbi skills -> pbi-cli skills
- All 7 semantic model SKILL.md files: prerequisites updated to 3-step flow
- New SVG/PNG marketing and documentation assets
2026-04-05 20:37:05 +02:00
MinaSaad1
81abf49f37 chore: remove unused assets from git tracking
Keep only the 17 SVGs actually referenced in README.md.
Removed 13 unused files from tracking (still on local disk).
2026-04-03 17:38:25 +02:00
MinaSaad1
148956e91b feat: add VIBE BI release announcement image
AI-generated ad-quality PNG for v3.10.1 release showing the evolution
from Vibe Modeling to VIBE BI with Claude Code, PBI-CLI, and Power BI
flow, stats, install CTA, and project URLs.
2026-04-03 16:47:57 +02:00
MinaSaad1
50e28a52c4 fix: show skill install success before connection attempt
Print success message for skills and CLAUDE.md injection before
auto-discovery, so users see what succeeded even if Power BI Desktop
is not running. Bump version to 3.10.1.
2026-04-03 12:25:40 +02:00
MinaSaad1
fd3b33bcc0 feat: add visual-first SVG marketing assets and update README
Add 4 new SVG assets (layers, commands, stats, workflow) with drawn
icons, block art, and dark GitHub theme. Enhance banner and before-after
with visual redesigns. Update README to reference layers.svg and stats.svg.
2026-04-03 11:49:23 +02:00
MinaSaad1
f3db52c13d fix: PBI-CLI block art as center element between Claude and Power BI 2026-04-02 17:35:45 +02:00
MinaSaad1
2cb60d3ed3 fix: restore PBI-CLI block art below VIBE BI, compact layout at 310px 2026-04-02 17:33:33 +02:00
MinaSaad1
cbaad119ea fix: compact banner -- remove PBI-CLI block art, tighten spacing, reduce to 280px 2026-04-02 17:32:05 +02:00
MinaSaad1
65ffbb1859 feat: add VIBE BI block art header above PBI-CLI in banner 2026-04-02 17:28:24 +02:00
MinaSaad1
8390a228e3 feat: restructure README with separate Modeling and Report Layer sections 2026-04-02 17:18:08 +02:00
MinaSaad1
d10e227d7f fix: remove overlapping input bar from chat-demo-report SVG 2026-04-02 17:16:33 +02:00
MinaSaad1
98c9a23df3 fix: redesign chat-demo-report SVG to match Claude Code terminal mockup style 2026-04-02 17:15:30 +02:00
MinaSaad1
2d730f72f6 fix: center PBI-CLI block art horizontally in banner 2026-04-02 17:13:54 +02:00
MinaSaad1
05627e320b fix: center pbi-cli card in banner, add pill badges for Modeling + Reporting 2026-04-02 17:12:47 +02:00
MinaSaad1
b900b9c19a fix: simplify refresh icon to two-tone circle with PBI logo, remove arrows 2026-04-02 17:09:40 +02:00
MinaSaad1
beba2af69e fix: rebuild refresh icon with dashed circle + arrow triangles at gaps 2026-04-02 17:08:07 +02:00
MinaSaad1
be79eb94f3 fix: rebuild refresh icon with clean semicircular arcs 2026-04-02 17:07:25 +02:00
MinaSaad1
3f1f74f0f8 fix: enhance refresh icon in auto-sync SVG with cleaner arcs and larger arrows 2026-04-02 17:06:12 +02:00
MinaSaad1
ccdc9c618f feat: add 3 new report-layer SVGs (visual types grid, workflow, chat demo)
- visual-types.svg: grid of 18 visual types with mini icons across 3 rows
  (charts, cards/KPIs/tables, slicers/maps/decorative) + list of 14 more
- report-workflow.svg: 6-step flow (scaffold, pages, visuals, bind, theme, validate)
- chat-demo-report.svg: Claude chat example showing report commands in action
2026-04-02 17:05:21 +02:00
MinaSaad1
652428632a fix: move step card titles up in auto-sync SVG 2026-04-02 17:00:49 +02:00
MinaSaad1
fd574a46cd fix: move left card bullet points inside card border 2026-04-02 16:59:40 +02:00
MinaSaad1
8ae92a679d fix: narrow dual-layer cards from 390px to 340px to remove blank space 2026-04-02 16:58:46 +02:00
MinaSaad1
26d6fd645b fix: extend card height so skill badges sit inside the border 2026-04-02 16:57:29 +02:00
MinaSaad1
45303c68f2 fix: left-align commands and add spacing under skill badges 2026-04-02 16:56:43 +02:00
MinaSaad1
39516bb840 fix: increase card height and fix overlapping badges/footer in dual-layer SVG 2026-04-02 16:54:52 +02:00
MinaSaad1
f4130b45f4 fix: clean card headers in dual-layer SVG -- remove overlapping rects, use line separator 2026-04-02 16:53:12 +02:00
MinaSaad1
2636bd5aae fix: increase spacing between step badges and title labels in auto-sync SVG 2026-04-02 16:49:31 +02:00
MinaSaad1
32aad78e5d fix: redesign auto-sync SVG with centered equal-width cards and larger icons 2026-04-02 16:48:10 +02:00
MinaSaad1
9cfab6329d fix: redesign dual-layer SVG with header bars, larger icons, skill badges 2026-04-02 16:47:12 +02:00
MinaSaad1
b102d83af2 feat: add auto-sync workflow SVG showing the 4-step save-first pattern 2026-04-02 16:45:33 +02:00
MinaSaad1
2e85c4f15e fix: redesign report-layer SVG with large visual icons instead of heavy text 2026-04-02 16:43:31 +02:00
MinaSaad1
136f4d77f7 fix: redesign report-layer SVG as 2x2 grid to prevent text truncation 2026-04-02 16:39:04 +02:00
MinaSaad1
a788175862 fix: center text vertically in skills-hub card boxes 2026-04-02 16:36:52 +02:00
MinaSaad1
91293bdfd0 fix: reposition NEW badge in skills-hub SVG 2026-04-02 16:34:42 +02:00
MinaSaad1
06b86f488f fix: redesign skills-hub SVG with wider cards and better spacing 2026-04-02 16:32:52 +02:00
MinaSaad1
7a2797c839 fix: increase text sizes across all SVG assets for readability 2026-04-02 16:29:51 +02:00
MinaSaad1
6afc7d7a24 fix: banner SVG -- remove duplicate Modeling text, increase label font size 2026-04-02 16:28:18 +02:00
MinaSaad1
3a54f2d84c feat: new SVG assets and README for v3 dual-layer messaging
New SVGs:
- dual-layer.svg: hero graphic showing both Modeling + Report layers
- report-layer.svg: 4-column showcase of Visuals, Pages, Themes, Filters

Updated SVGs:
- banner.svg: "The First CLI for Both Power BI Modeling and Reporting"
- skills-hub.svg: 12 skills in two rows (7 modeling + 5 report) with stats bar
- architecture-flow.svg: dual-path showing TOM + PBIR backends
- before-after.svg: added report-layer pain points and solutions

README updated to feature new graphics.
2026-04-02 16:21:05 +02:00
MinaSaad1
22f75b1699 fix: use subprocess instead of os.startfile for cross-platform mypy compat 2026-04-02 16:03:09 +02:00
MinaSaad1
5acb3f33e3 fix: resolve CI failures (formatting, mypy, test import)
- Run ruff format on all 26 unformatted files
- Fix mypy strict errors: add explicit typing for json.loads returns,
  add pywin32/websockets to mypy ignore_missing_imports
- Remove yaml dependency from test_skill_triggering.py (use regex parser)
- Fix skill triggering test to handle both single-line and multi-line
  description formats in YAML frontmatter
2026-04-02 15:59:49 +02:00
121 changed files with 4331 additions and 1871 deletions

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
assets/auto-sync-ai*.png filter=lfs diff=lfs merge=lfs -text
assets/release-vibe-bi.png filter=lfs diff=lfs merge=lfs -text
marketing/images/*.png filter=lfs diff=lfs merge=lfs -text

37
.github/workflows/downloads-chart.yml vendored Normal file
View file

@ -0,0 +1,37 @@
name: Update downloads chart
on:
schedule:
# Daily at 06:15 UTC (after pypistats has processed the prior day)
- cron: "15 6 * * *"
workflow_dispatch:
permissions:
contents: write
jobs:
regenerate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
token: ${{ secrets.PAT }}
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Regenerate cumulative downloads chart
run: python scripts/generate_downloads_chart.py
- name: Commit and push updated chart
run: |
if git diff --quiet assets/downloads-chart.svg; then
echo "No changes to downloads chart."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add assets/downloads-chart.svg
git commit -m "chore: refresh downloads chart"
git push

34
.gitignore vendored
View file

@ -32,3 +32,37 @@ Thumbs.db
# pbi-cli specific
~/.pbi-cli/
# Extracted wheel/sdist build directories
pbi_cli_tool-*/
# Demo and scratch Power BI project files
demo.Report/
demo.SemanticModel/
demo.pbip
pbir_desktop_test/
"pbib examples/"
# Marketing assets (local only, not for GitHub)
marketing/
# Local-only documents (private notes, call prep, etc.)
docs/
# Assets not referenced in README
assets/*.png
assets/Claude_AI_symbol.svg
assets/New_Power_BI_Logo.svg
assets/commands.svg
assets/cta-start.svg
assets/dax-skill.svg
assets/deploy-secure.svg
assets/docs-diagnostics.svg
assets/dual-layer.svg
assets/feature-grid.svg
assets/header.svg
assets/how-it-works.svg
assets/modeling-skill.svg
assets/release-vibe-bi.svg
assets/token-cost.svg
assets/workflow.svg

View file

@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.10.6] - 2026-04-07
### Fixed
- `visual bind` no longer writes the legacy `Commands` block (SemanticQueryDataShapeCommand) to `visual.json`. PBIR 2.7.0 uses `additionalProperties: false` on the query object, so the `Commands` field is a hard schema violation. Only `queryState` projections are now written.
- `pbi report validate` and the full PBIR validator no longer flag a missing `layoutOptimization` field as an error. The real Microsoft schema does not list it as required; the previous check was against a stale internal schema.
- `pbi report set-background` now always writes `transparency: 0` alongside the color. Power BI Desktop defaults a missing `transparency` property to 100 (fully invisible), making the color silently unrendered. The new `--transparency` flag (0-100, default 0) lets callers override for semi-transparent backgrounds.
### Added
- `--no-sync` flag on `report`, `visual`, `filters`, and `bookmarks` command groups. Suppresses the per-command Desktop auto-sync for scripted multi-step builds. Use `pbi report reload` for a single explicit sync at the end of the script.
## [3.10.5] - 2026-04-06
### Fixed
- ASCII banner: replaced incorrect hand-crafted art with the official design from `assets/banner.svg`. The `I` in PBI is now correctly narrow, and a small `███╗/╚══╝` block serves as the visible `-` separator between PBI and CLI.
## [3.10.4] - 2026-04-06
### Added
- ASCII banner displayed in terminal when `pbi` is invoked with no subcommand. Renders in Power BI yellow on color terminals, plain text fallback on legacy terminals without Unicode support. Skipped when `--json` flag is used.
## [3.10.3] - 2026-04-05
### Changed
- Claude Code integration is now fully opt-in. `pbi connect` no longer writes to `~/.claude/`. Run `pbi-cli skills install` explicitly to register skills with Claude Code. See [SECURITY.md](SECURITY.md) for full details.
- `pbi-cli` is now a dedicated management entry point (`pbi-cli skills install/uninstall/list`). Skills subcommands have been removed from the `pbi` entry point.
### Fixed
- DLL licensing: Microsoft Analysis Services client library DLLs bundled in `src/pbi_cli/dlls/` are now correctly attributed under the Microsoft Software License Terms, not sublicensed under MIT. Full EULA text in `THIRD_PARTY_LICENSES.md`.
- `pyproject.toml` updated to PEP 639 dual SPDX expression (`MIT AND LicenseRef-Microsoft-AS-Client-Libraries`) with `license-files` declaration.
- README.md and README.pypi.md updated to reflect the correct 3-step setup flow: `pipx install``pbi-cli skills install``pbi connect`.
## [3.10.0] - 2026-04-02
### Added

View file

@ -61,7 +61,7 @@ tests/ # Mirrors src/ structure
1. Create `src/pbi_cli/skills/your-skill/SKILL.md`
2. Follow the frontmatter format from existing skills
3. Test with `pbi skills list` and `pbi skills install`
3. Test with `pbi-cli skills list` and `pbi-cli skills install`
## Reporting Issues

7
Claude_AI_symbol.svg Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.17 -->
<svg width="1200" height="1200" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<g id="g314">
<path id="path147" fill="#d97757" stroke="none" d="M 233.959793 800.214905 L 468.644287 668.536987 L 472.590637 657.100647 L 468.644287 650.738403 L 457.208069 650.738403 L 417.986633 648.322144 L 283.892639 644.69812 L 167.597321 639.865845 L 54.926208 633.825623 L 26.577238 627.785339 L 3.3e-05 592.751709 L 2.73832 575.27533 L 26.577238 559.248352 L 60.724873 562.228149 L 136.187973 567.382629 L 249.422867 575.194763 L 331.570496 580.026978 L 453.261841 592.671082 L 472.590637 592.671082 L 475.328857 584.859009 L 468.724915 580.026978 L 463.570557 575.194763 L 346.389313 495.785217 L 219.543671 411.865906 L 153.100723 363.543762 L 117.181267 339.060425 L 99.060455 316.107361 L 91.248367 266.01355 L 123.865784 230.093994 L 167.677887 233.073853 L 178.872513 236.053772 L 223.248367 270.201477 L 318.040283 343.570496 L 441.825592 434.738342 L 459.946411 449.798706 L 467.194672 444.64447 L 468.080597 441.020203 L 459.946411 427.409485 L 392.617493 305.718323 L 320.778564 181.932983 L 288.80542 130.630859 L 280.348999 99.865845 C 277.369171 87.221436 275.194641 76.590698 275.194641 63.624268 L 312.322174 13.20813 L 332.8591 6.604126 L 382.389313 13.20813 L 403.248352 31.328979 L 434.013519 101.71814 L 483.865753 212.537048 L 561.181274 363.221497 L 583.812134 407.919434 L 595.892639 449.315491 L 600.40271 461.959839 L 608.214783 461.959839 L 608.214783 454.711609 L 614.577271 369.825623 L 626.335632 265.61084 L 637.771851 131.516846 L 641.718201 93.745117 L 660.402832 48.483276 L 697.530334 24.000122 L 726.52356 37.852417 L 750.362549 72 L 747.060486 94.067139 L 732.886047 186.201416 L 705.100708 330.52356 L 686.979919 427.167847 L 697.530334 427.167847 L 709.61084 415.087341 L 758.496704 350.174561 L 840.644348 247.490051 L 876.885925 206.738342 L 919.167847 161.71814 L 946.308838 140.29541 L 997.61084 140.29541 L 1035.38269 196.429626 L 1018.469849 254.416199 L 965.637634 321.422852 L 921.825562 378.201538 L 859.006714 462.765259 L 819.785278 530.41626 L 823.409424 535.812073 L 832.75177 534.92627 L 974.657776 504.724915 L 1051.328979 490.872559 L 1142.818848 475.167786 L 1184.214844 494.496582 L 1188.724854 514.147644 L 1172.456421 554.335693 L 1074.604126 578.496765 L 959.838989 601.449829 L 788.939636 641.879272 L 786.845764 643.409485 L 789.261841 646.389343 L 866.255127 653.637634 L 899.194702 655.409424 L 979.812134 655.409424 L 1129.932861 666.604187 L 1169.154419 692.537109 L 1192.671265 724.268677 L 1188.724854 748.429688 L 1128.322144 779.194641 L 1046.818848 759.865845 L 856.590759 714.604126 L 791.355774 698.335754 L 782.335693 698.335754 L 782.335693 703.731567 L 836.69812 756.885986 L 936.322205 846.845581 L 1061.073975 962.81897 L 1067.436279 991.490112 L 1051.409424 1014.120911 L 1034.496704 1011.704712 L 924.885986 929.234924 L 882.604126 892.107544 L 786.845764 811.48999 L 780.483276 811.48999 L 780.483276 819.946289 L 802.550415 852.241699 L 919.087341 1027.409424 L 925.127625 1081.127686 L 916.671204 1098.604126 L 886.469849 1109.154419 L 853.288696 1103.114136 L 785.073914 1007.355835 L 714.684631 899.516785 L 657.906067 802.872498 L 650.979858 806.81897 L 617.476624 1167.704834 L 601.771851 1186.147705 L 565.530212 1200 L 535.328857 1177.046997 L 519.302124 1139.919556 L 535.328857 1066.550537 L 554.657776 970.792053 L 570.362488 894.68457 L 584.536926 800.134277 L 592.993347 768.724976 L 592.429626 766.630859 L 585.503479 767.516968 L 514.22821 865.369263 L 405.825531 1011.865906 L 320.053711 1103.677979 L 299.516815 1111.812256 L 263.919525 1093.369263 L 267.221497 1060.429688 L 287.114136 1031.114136 L 405.825531 880.107361 L 477.422913 786.52356 L 523.651062 732.483276 L 523.328918 724.671265 L 520.590698 724.671265 L 205.288605 929.395935 L 149.154434 936.644409 L 124.993355 914.01355 L 127.973183 876.885986 L 139.409409 864.80542 L 234.201385 799.570435 L 233.879227 799.8927 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB

38
NOTICE Normal file
View file

@ -0,0 +1,38 @@
pbi-cli
Copyright (c) 2026 pbi-cli contributors
This product is licensed under the MIT License. See the LICENSE file for
the full text of that license.
This product bundles the following third-party software inside the
`pbi-cli-tool` PyPI distribution. These components are NOT covered by the
MIT License and retain their original license terms. See
THIRD_PARTY_LICENSES.md for the full text of each license.
--------------------------------------------------------------------------
Microsoft Analysis Services Client Libraries
Copyright (c) Microsoft Corporation. All rights reserved.
--------------------------------------------------------------------------
Licensed under the Microsoft Software License Terms - Microsoft Analysis
Management Objects (AMO) and the Microsoft Software License Terms -
Microsoft Analysis Services - ADOMD.NET. The full text of both licenses is
reproduced in THIRD_PARTY_LICENSES.md at the root of this distribution.
Bundled binaries (src/pbi_cli/dlls/):
- Microsoft.AnalysisServices.dll
- Microsoft.AnalysisServices.Core.dll
- Microsoft.AnalysisServices.Tabular.dll
- Microsoft.AnalysisServices.Tabular.Json.dll
- Microsoft.AnalysisServices.AdomdClient.dll
These binaries are redistributed unmodified from the official Microsoft
NuGet packages:
- Microsoft.AnalysisServices.NetCore.retail.amd64
- Microsoft.AnalysisServices.AdomdClient.retail.amd64
By installing `pbi-cli-tool` you agree to the Microsoft license terms
applicable to these bundled binaries in addition to the MIT License that
applies to the rest of the package.

36
New_Power_BI_Logo.svg Normal file
View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="630px" height="630px" viewBox="0 0 630 630" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
<title>PBI Logo</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
<stop stop-color="#EBBB14" offset="0%"></stop>
<stop stop-color="#B25400" offset="100%"></stop>
</linearGradient>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
<stop stop-color="#F9E583" offset="0%"></stop>
<stop stop-color="#DE9800" offset="100%"></stop>
</linearGradient>
<path d="M346,604 L346,630 L320,630 L153,630 C138.640597,630 127,618.359403 127,604 L127,183 C127,168.640597 138.640597,157 153,157 L320,157 C334.359403,157 346,168.640597 346,183 L346,604 Z" id="path-3"></path>
<filter x="-9.1%" y="-6.3%" width="136.5%" height="116.9%" filterUnits="objectBoundingBox" id="filter-4">
<feOffset dx="20" dy="10" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="10" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0530211976 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-5">
<stop stop-color="#F9E68B" offset="0%"></stop>
<stop stop-color="#F3CD32" offset="100%"></stop>
</linearGradient>
</defs>
<g id="PBI-Logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(77.500000, 0.000000)">
<rect id="Rectangle" fill="url(#linearGradient-1)" x="256" y="0" width="219" height="630" rx="26"></rect>
<g id="Combined-Shape">
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-3"></use>
<use fill="url(#linearGradient-2)" fill-rule="evenodd" xlink:href="#path-3"></use>
</g>
<path d="M219,604 L219,630 L193,630 L26,630 C11.6405965,630 1.75851975e-15,618.359403 0,604 L0,341 C-1.75851975e-15,326.640597 11.6405965,315 26,315 L193,315 C207.359403,315 219,326.640597 219,341 L219,604 Z" id="Combined-Shape" fill="url(#linearGradient-5)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

161
README.md
View file

@ -1,5 +1,5 @@
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/banner.svg" alt="pbi-cli — Vibe Modeling" width="850"/>
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/banner.svg" alt="pbi-cli" width="850"/>
</p>
<p align="center">
@ -18,7 +18,8 @@
<p align="center">
<a href="#why-pbi-cli">Why pbi-cli</a> &bull;
<a href="#get-started">Get Started</a> &bull;
<a href="#just-ask-claude">Just Ask Claude</a> &bull;
<a href="#semantic-model-layer">Modeling</a> &bull;
<a href="#report-layer">Reporting</a> &bull;
<a href="#skills">Skills</a> &bull;
<a href="#all-commands">All Commands</a> &bull;
<a href="#contributing">Contributing</a>
@ -32,16 +33,33 @@
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/before-after.svg" alt="Why pbi-cli" width="850"/>
</p>
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/layers.svg" alt="Dual-Layer Architecture" width="850"/>
</p>
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/stats.svg" alt="pbi-cli at a Glance" width="850"/>
</p>
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/downloads-chart.svg" alt="pbi-cli cumulative downloads from PyPI" width="850"/>
</p>
<p align="center">
<sub>Cumulative downloads, refreshed daily from <a href="https://pypistats.org/packages/pbi-cli-tool">pypistats.org</a> via GitHub Actions.</sub>
</p>
---
## Get Started
```bash
pipx install pbi-cli-tool # 1. Install (handles PATH automatically)
pbi connect # 2. Auto-detects Power BI Desktop and installs skills
pbi-cli skills install # 2. Register Claude Code skills (one-time setup)
pbi connect # 3. Connect to Power BI Desktop
```
Open Power BI Desktop with a `.pbix` file, run `pbi connect`, and start asking Claude.
Open Power BI Desktop with a `.pbix` file, run the three commands above, and start asking Claude.
> **Requires:** Windows with Python 3.10+ and Power BI Desktop running.
@ -77,7 +95,9 @@ Add the printed path to your system PATH, then restart your terminal. We recomme
---
## Just Ask Claude
## Semantic Model Layer
Ask Claude to work with your Power BI semantic model. Requires `pbi connect`.
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/chat-demo.svg" alt="Just Ask Claude" width="850"/>
@ -115,9 +135,72 @@ Add the printed path to your system PATH, then restart your terminal. We recomme
---
## Report Layer
Ask Claude to build and manage your Power BI reports. No connection needed -- works directly on PBIR files.
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/chat-demo-report.svg" alt="Ask Claude to build reports" width="850"/>
</p>
### Build a report in 6 steps
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/report-workflow.svg" alt="Report workflow" width="850"/>
</p>
### Visuals, pages, themes, filters
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/report-layer.svg" alt="Report layer capabilities" width="850"/>
</p>
### 32 visual types
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/visual-types.svg" alt="32 Visual Types" width="850"/>
</p>
---
## Architecture
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/architecture-flow.svg" alt="Architecture" width="850"/>
</p>
**Two layers, one CLI:**
- **Semantic Model layer** -- Direct in-process .NET interop from Python to Power BI Desktop via TOM/ADOMD. No MCP server, no external binaries, sub-second execution.
- **Report layer** -- Reads and writes PBIR (Enhanced Report Format) JSON files directly. No connection needed. Works with `.pbip` projects.
### Desktop Auto-Sync
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/auto-sync.svg" alt="Desktop Auto-Sync" width="850"/>
</p>
<details>
<summary><b>Configuration details</b></summary>
All config lives in `~/.pbi-cli/`:
```
~/.pbi-cli/
config.json # Default connection preference
connections.json # Named connections
repl_history # REPL command history
```
Bundled DLLs ship inside the Python package (`pbi_cli/dlls/`).
</details>
---
## Skills
After running `pbi connect`, Claude Code discovers **12 Power BI skills** automatically. Each skill teaches Claude a different area. You don't need to memorize commands.
After running `pbi-cli skills install`, Claude Code discovers **12 Power BI skills**. Each skill teaches Claude a different area. You don't need to memorize commands.
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/skills-hub.svg" alt="12 Skills" width="850"/>
@ -147,35 +230,6 @@ After running `pbi connect`, Claude Code discovers **12 Power BI skills** automa
---
## Architecture
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/architecture-flow.svg" alt="Architecture" width="850"/>
</p>
**Two layers, one CLI:**
- **Semantic Model layer** -- Direct in-process .NET interop from Python to Power BI Desktop via TOM/ADOMD. No MCP server, no external binaries, sub-second execution.
- **Report layer** -- Reads and writes PBIR (Enhanced Report Format) JSON files directly. No connection needed. Works with `.pbip` projects.
<details>
<summary><b>Configuration details</b></summary>
All config lives in `~/.pbi-cli/`:
```
~/.pbi-cli/
config.json # Default connection preference
connections.json # Named connections
repl_history # REPL command history
```
Bundled DLLs ship inside the Python package (`pbi_cli/dlls/`).
</details>
---
## All Commands
27 command groups covering both the semantic model and the report layer.
@ -209,26 +263,6 @@ Run `pbi <command> --help` for full options.
---
## Supported Visual Types (32)
pbi-cli supports creating and binding data to 32 Power BI visual types:
**Charts:** bar, line, column, area, ribbon, waterfall, stacked bar, clustered bar, clustered column, scatter, funnel, combo, donut/pie, treemap
**Cards/KPIs:** card (legacy), cardVisual (modern), cardNew, multi-row card, KPI, gauge
**Tables:** table, matrix (pivot table)
**Slicers:** slicer, text slicer, list slicer, advanced slicer (tile/image)
**Maps:** Azure Map
**Decorative:** action button, image, shape, textbox, page navigator
Use friendly aliases: `pbi visual add --page p1 --type bar` instead of `--type barChart`.
---
## REPL Mode
For interactive work, the REPL keeps a persistent connection:
@ -264,6 +298,21 @@ pytest -m "not e2e" # Run tests (488 tests)
---
## Bundled third-party software
`pbi-cli-tool` ships with Microsoft Analysis Services client library
assemblies (`Microsoft.AnalysisServices.*.dll`) under `src/pbi_cli/dlls/`.
These binaries are **not** covered by pbi-cli's MIT license. They are
redistributed unmodified under the Microsoft Software License Terms for
Microsoft Analysis Management Objects (AMO) and Microsoft Analysis
Services - ADOMD.NET. Full terms are in
[THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and the companion
[NOTICE](NOTICE) file. By installing `pbi-cli-tool` you agree to those
terms in addition to the MIT License that applies to the rest of the
package.
---
## Contributing
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
@ -281,5 +330,5 @@ Contributions are welcome! Please open an issue first to discuss what you'd like
</p>
<p align="center">
<sub>MIT License</sub>
<sub>MIT License — bundled Microsoft DLLs are licensed separately, see <a href="THIRD_PARTY_LICENSES.md">THIRD_PARTY_LICENSES.md</a></sub>
</p>

View file

@ -39,10 +39,11 @@ Install and set up pbi-cli from https://github.com/MinaSaad1/pbi-cli.git
```bash
pipx install pbi-cli-tool # 1. Install (handles PATH automatically)
pbi connect # 2. Auto-detects Power BI Desktop and installs skills
pbi-cli skills install # 2. Register Claude Code skills (one-time setup)
pbi connect # 3. Connect to Power BI Desktop
```
That's it. Open Power BI Desktop with a `.pbix` file, run `pbi connect`, and everything is set up automatically. Open Claude Code and start asking.
Open Power BI Desktop with a `.pbix` file, run the three commands above, and start asking Claude.
> **Requires:** Windows with Python 3.10+ and Power BI Desktop running.
@ -70,7 +71,7 @@ Add the printed path to your system PATH, then restart your terminal. We recomme
## Skills
After running `pbi connect`, Claude Code discovers **12 Power BI skills**. Each skill teaches Claude a different area. You don't need to memorize commands.
After running `pbi-cli skills install`, Claude Code discovers **12 Power BI skills**. Each skill teaches Claude a different area. You don't need to memorize commands.
### Semantic Model (require `pbi connect`)
@ -172,6 +173,21 @@ pytest -m "not e2e" # Run tests (488 tests)
---
## Bundled third-party software
`pbi-cli-tool` ships with Microsoft Analysis Services client library
assemblies (`Microsoft.AnalysisServices.*.dll`) inside the PyPI wheel
under `src/pbi_cli/dlls/`. These binaries are **not** covered by
pbi-cli's MIT license. They are redistributed unmodified under the
Microsoft Software License Terms for Microsoft Analysis Management
Objects (AMO) and Microsoft Analysis Services - ADOMD.NET. Full terms
are in [THIRD_PARTY_LICENSES.md](https://github.com/MinaSaad1/pbi-cli/blob/master/THIRD_PARTY_LICENSES.md)
and the companion [NOTICE](https://github.com/MinaSaad1/pbi-cli/blob/master/NOTICE)
file. By installing `pbi-cli-tool` you agree to those terms in addition
to the MIT License that applies to the rest of the package.
---
## Contributing
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
@ -189,5 +205,5 @@ Contributions are welcome! Please open an issue first to discuss what you'd like
</p>
<p align="center">
<sub>MIT License</sub>
<sub>MIT License — bundled Microsoft DLLs are licensed separately, see <a href="https://github.com/MinaSaad1/pbi-cli/blob/master/THIRD_PARTY_LICENSES.md">THIRD_PARTY_LICENSES.md</a></sub>
</p>

View file

@ -2,18 +2,24 @@
## Supported Versions
| Version | Supported |
|---------|-----------|
| 1.x | Yes |
| < 1.0 | No |
Only the latest 3.x release line receives security fixes. Users on older
major versions should upgrade.
| Version | Supported |
|----------|------------------|
| 3.10.x | Yes |
| 3.9.x | Critical fixes |
| < 3.9 | No |
| 2.x | No |
| 1.x | No |
## Reporting a Vulnerability
If you discover a security vulnerability in pbi-cli, please report it responsibly.
If you discover a security vulnerability in pbi-cli, please report it
responsibly. **Do not open a public issue.**
**Do not open a public issue.**
Instead, email **security@pbi-cli.dev** or use [GitHub private vulnerability reporting](https://github.com/MinaSaad1/pbi-cli/security/advisories/new).
Use [GitHub private vulnerability reporting](https://github.com/MinaSaad1/pbi-cli/security/advisories/new)
to submit the report.
Please include:
@ -24,15 +30,96 @@ Please include:
## Response Timeline
- **Acknowledgment**: Within 48 hours
- **Initial assessment**: Within 1 week
- **Fix release**: As soon as possible, depending on severity
- **Acknowledgment**: within 48 hours
- **Initial assessment**: within 1 week
- **Fix release**: as soon as possible, severity-dependent
## Security Considerations
## Architecture and Trust Boundaries
pbi-cli connects to local Power BI instances via MCP (Model Context Protocol) over stdio. Key security notes:
pbi-cli connects to a locally running Power BI Desktop instance via direct
in-process .NET interop. **There is no MCP server, no subprocess, and no
network listener.** Earlier 1.x releases used a separate
`PBIDesktopMCPServer.exe` subprocess; that architecture was removed in 2.x
and this document reflects the current 3.x behavior.
- **No network exposure**: The MCP server binary communicates over local stdio pipes, not network sockets
- **No credentials stored**: Connection details are saved locally but passwords and tokens are never persisted
- **Local binary execution**: pbi-cli launches `PBIDesktopMCPServer.exe` as a subprocess; ensure the binary is from a trusted source
- **Config file permissions**: Connection store at `~/.pbi-cli/connections.json` should have user-only read/write permissions
- **Direct in-process .NET interop.** pbi-cli loads bundled Microsoft
Analysis Services Tabular Object Model (TOM) and ADOMD.NET client
assemblies into the Python process via `pythonnet` / `clr-loader`. See
`src/pbi_cli/core/dotnet_loader.py` and `src/pbi_cli/core/tom_backend.py`.
The bundled DLLs live in `src/pbi_cli/dlls/` and are Microsoft binaries
redistributed under the Microsoft Software License Terms for Microsoft
Analysis Management Objects (AMO) and Microsoft Analysis Services -
ADOMD.NET. See [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md) and
[`NOTICE`](NOTICE) for details.
- **Local-only connection.** pbi-cli connects to Power BI Desktop's
embedded Analysis Services instance over a loopback TCP port that
Desktop binds at launch. pbi-cli does not open any network sockets for
inbound traffic and does not communicate over the public network.
- **No credentials persisted.** Connection metadata (port, workspace
path, model name) is stored at `~/.pbi-cli/connections.json`. No
tokens, passwords, or session credentials are written to disk. The
file should have user-only read/write permissions.
- **Report-layer commands are fully offline.** `pbi report`, `pbi visual`,
`pbi filters`, `pbi bookmarks`, `pbi format`, and `pbi theme` operate on
PBIR JSON files on disk. They do not require `pbi connect` and do not
touch the Power BI Desktop process.
## Global Configuration Modifications
pbi-cli integrates with Claude Code by writing to the user's global Claude
configuration directory. Users should be aware of exactly which files are
modified and when. This section is authoritative; if behavior diverges from
the description below, please file a security advisory.
### Files written
| Path | Written by | Contents |
|--------------------------------|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `~/.claude/CLAUDE.md` | `pbi-cli skills install` | Appends a block wrapped in `<!-- pbi-cli:start -->` / `<!-- pbi-cli:end -->` markers that lists the 12 bundled skills and their trigger conditions. Source: `src/pbi_cli/core/claude_integration.py`. |
| `~/.claude/skills/power-bi-*/` | `pbi-cli skills install` | Copies the 12 bundled `SKILL.md` files from `src/pbi_cli/skills/` so Claude Code discovers them. Source: `src/pbi_cli/commands/skills_cmd.py`. |
No other paths under `~/.claude/` are read or modified. pbi-cli does not
access user conversation history, project memory files, unrelated skills,
or any other Claude Code state.
### When it happens
- **`pbi-cli skills install`** — the explicit install command (3.10.3+).
Before writing anything, the command displays the exact paths it will
modify and requires the user to confirm. Passing `--yes` / `-y` skips
the prompt for non-interactive use.
- **`pbi-cli skills uninstall`** — removes the skill files and, when
removing all skills, cleans up the `CLAUDE.md` block between its
marker comments.
- **`pbi connect`** — connects to Power BI Desktop only. It does **not**
write to `~/.claude/`. On a successful connection it checks whether
skills are installed and, if not, prints a one-line tip:
`Run 'pbi-cli skills install' to register pbi-cli skills with Claude Code.`
### Opt-out
Claude Code integration is fully opt-in as of 3.10.3. Simply do not run
`pbi-cli skills install` and `~/.claude/` is never touched.
To remove a previously installed integration, run:
```
pbi-cli skills uninstall
```
### Why this matters
`~/.claude/CLAUDE.md` is Claude Code's global instruction file and is
loaded into every Claude Code session across every project on the
machine. Modifying it affects Claude's behavior everywhere, not just for
Power BI work. The pbi-cli block is bounded by comment markers so it can
be removed cleanly, but users on multi-tenant or sensitive machines
should be aware of this before running `pbi-cli skills install`.
## Bundled Binaries
`pbi-cli-tool` ships Microsoft Analysis Services client library DLLs
inside the PyPI wheel under `src/pbi_cli/dlls/`. These are unmodified
Microsoft binaries redistributed under Microsoft's own license terms,
**not** under MIT. See [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md)
and [`NOTICE`](NOTICE) at the repo root for full terms and attribution.

329
THIRD_PARTY_LICENSES.md Normal file
View file

@ -0,0 +1,329 @@
# Third-Party Licenses
pbi-cli itself is licensed under the MIT License (see [LICENSE](LICENSE)). This
file lists third-party software bundled with the `pbi-cli-tool` PyPI
distribution and the license terms under which each component is
redistributed.
---
## Microsoft Analysis Services Client Libraries
The following assemblies shipped under `src/pbi_cli/dlls/` inside the PyPI
wheel are **not** covered by the MIT License. They are unmodified Microsoft
Corporation binaries redistributed under the *Microsoft Software License
Terms — Microsoft Analysis Management Objects (AMO)* and the materially
identical *Microsoft Software License Terms — Microsoft Analysis Services
ADOMD.NET*:
| File | Governing License | Source NuGet Package |
|--------------------------------------------------|-------------------------------|----------------------------------------------------------------|
| `Microsoft.AnalysisServices.dll` | Microsoft AMO License | `Microsoft.AnalysisServices.NetCore.retail.amd64` |
| `Microsoft.AnalysisServices.Core.dll` | Microsoft AMO License | `Microsoft.AnalysisServices.NetCore.retail.amd64` |
| `Microsoft.AnalysisServices.Tabular.dll` | Microsoft AMO License | `Microsoft.AnalysisServices.NetCore.retail.amd64` |
| `Microsoft.AnalysisServices.Tabular.Json.dll` | Microsoft AMO License | `Microsoft.AnalysisServices.NetCore.retail.amd64` |
| `Microsoft.AnalysisServices.AdomdClient.dll` | Microsoft ADOMD.NET License | `Microsoft.AnalysisServices.AdomdClient.retail.amd64` |
These files are the object code form of code listed on Microsoft's REDIST
list and are therefore distributable under the "DISTRIBUTABLE CODE" section
of the governing license. pbi-cli does not modify, reverse engineer, decompile,
or recompile them.
**Copyright © Microsoft Corporation. All rights reserved.**
By installing `pbi-cli-tool` you agree to the Microsoft license terms
reproduced below in addition to the MIT License that applies to the rest of
the package. If you do not agree to these terms, do not install or use
`pbi-cli-tool`.
### Canonical sources
The original license documents are published by Microsoft at:
- AMO license: <https://go.microsoft.com/fwlink/?linkid=852989> (distributed as
`AMO_Eula.zip`, English text in `Eula/EulaText_1033.rtf`)
- ADOMD.NET license: <https://go.microsoft.com/fwlink/?linkid=852895>
The text below was extracted from the English (locale 1033) RTF file published
by Microsoft. The ADOMD.NET license is materially identical to the AMO license
reproduced here; the only difference is the product name line ("MICROSOFT
ANALYSIS SERVICES ADOMD.NET" instead of "MICROSOFT ANALYSIS MANAGEMENT
OBJECTS (AMO)"). Both licenses apply to their respective bundled binaries as
listed in the table above.
---
## MICROSOFT SOFTWARE LICENSE TERMS
### MICROSOFT ANALYSIS MANAGEMENT OBJECTS (AMO)
IF YOU LIVE IN (OR ARE A BUSINESS WITH A PRINCIPAL PLACE OF BUSINESS IN) THE
UNITED STATES, PLEASE READ THE "BINDING ARBITRATION AND CLASS ACTION WAIVER"
SECTION BELOW. IT AFFECTS HOW DISPUTES ARE RESOLVED.
These license terms are an agreement between you and Microsoft Corporation
(or one of its affiliates). They apply to the software named above and any
Microsoft services or software updates (except to the extent such services or
updates are accompanied by new or additional terms, in which case those
different terms apply prospectively and do not alter your or Microsoft's
rights relating to pre-updated software or services). IF YOU COMPLY WITH
THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW. BY USING THE SOFTWARE, YOU
ACCEPT THESE TERMS.
1. **INSTALLATION AND USE RIGHTS.**
a. **General.** You may install and use any number of copies of the
software.
b. **Third Party Software.** The software may include third party
applications that Microsoft, not the third party, licenses to you under
this agreement. Any included notices for third party applications are
for your information only.
c. **Competitive Benchmarking.** If you are a direct competitor, and you
access or use the software for purposes of competitive benchmarking,
analysis, or intelligence gathering, you waive as against Microsoft,
its subsidiaries, and its affiliated companies (including prospectively)
any competitive use, access, and benchmarking test restrictions in the
terms governing your software to the extent your terms of use are, or
purport to be, more restrictive than Microsoft's terms. If you do not
waive any such purported restrictions in the terms governing your
software, you are not allowed to access or use this software, and will
not do so.
2. **DISTRIBUTABLE CODE.** The software may contain code you are permitted to
distribute (i.e. make available for third parties) in applications you
develop, as described in this Section.
a. **Distribution Rights.** The code and test files described below are
distributable if included with the software.
i. **REDIST.TXT Files.** You may copy and distribute the object code
form of code listed on the REDIST list in the software, if any, or
listed at Redist.txt;
ii. **Image Library.** You may copy and distribute images, graphics,
and animations in the Image Library as described in the software
documentation; and
iii. **Third Party Distribution.** You may permit distributors of your
applications to copy and distribute any of this distributable code
you elect to distribute with your applications.
b. **Distribution Requirements.** For any code you distribute, you must:
i. add significant primary functionality to it in your applications;
ii. require distributors and external end users to agree to terms that
protect it and Microsoft at least as much as this agreement; and
iii. indemnify, defend, and hold harmless Microsoft from any claims,
including attorneys' fees, related to the distribution or use of
your applications, except to the extent that any claim is based
solely on the unmodified distributable code.
c. **Distribution Restrictions.** You may not:
i. use Microsoft's trademarks or trade dress in your application in any
way that suggests your application comes from or is endorsed by
Microsoft; or
ii. modify or distribute the source code of any distributable code so
that any part of it becomes subject to any license that requires
that the distributable code, any other part of the software, or any
of Microsoft's other intellectual property be disclosed or
distributed in source code form, or that others have the right to
modify it.
3. **DATA COLLECTION.** The software may collect information about you and
your use of the software and send that to Microsoft. Microsoft may use
this information to provide services and improve Microsoft's products and
services. Your opt-out rights, if any, are described in the product
documentation. Some features in the software may enable collection of data
from users of your applications that access or use the software. If you
use these features to enable data collection in your applications, you
must comply with applicable law, including getting any required user
consent, and maintain a prominent privacy policy that accurately informs
users about how you use, collect, and share their data. You can learn more
about Microsoft's data collection and use in the product documentation and
the Microsoft Privacy Statement at
<https://go.microsoft.com/fwlink/?LinkId=521839>. You agree to comply with
all applicable provisions of the Microsoft Privacy Statement.
4. **SCOPE OF LICENSE.** The software is licensed, not sold. Microsoft
reserves all other rights. Unless applicable law gives you more rights
despite this limitation, you will not (and have no right to):
a. work around any technical limitations in the software that only allow
you to use it in certain ways;
b. reverse engineer, decompile or disassemble the software;
c. remove, minimize, block, or modify any notices of Microsoft or its
suppliers in the software;
d. use the software in any way that is against the law or to create or
propagate malware; or
e. share, publish, distribute, or lend the software (except for any
distributable code, subject to the terms above), provide the software
as a stand-alone hosted solution for others to use, or transfer the
software or this agreement to any third party.
5. **EXPORT RESTRICTIONS.** You must comply with all domestic and
international export laws and regulations that apply to the software,
which include restrictions on destinations, end users, and end use. For
further information on export restrictions, visit <http://aka.ms/exporting>.
6. **SUPPORT SERVICES.** Microsoft is not obligated under this agreement to
provide any support services for the software. Any support provided is
"as is", "with all faults", and without warranty of any kind.
7. **UPDATES.** The software may periodically check for updates, and download
and install them for you. You may obtain updates only from Microsoft or
authorized sources. Microsoft may need to update your system to provide
you with updates. You agree to receive these automatic updates without any
additional notice. Updates may not include or support all existing
software features, services, or peripheral devices.
8. **BINDING ARBITRATION AND CLASS ACTION WAIVER.** This Section applies if
you live in (or, if a business, your principal place of business is in)
the United States. If you and Microsoft have a dispute, you and Microsoft
agree to try for 60 days to resolve it informally. If you and Microsoft
can't, you and Microsoft agree to binding individual arbitration before
the American Arbitration Association under the Federal Arbitration Act
("FAA"), and not to sue in court in front of a judge or jury. Instead, a
neutral arbitrator will decide. Class action lawsuits, class-wide
arbitrations, private attorney-general actions, and any other proceeding
where someone acts in a representative capacity are not allowed; nor is
combining individual proceedings without the consent of all parties. The
complete Arbitration Agreement contains more terms and is at
<http://aka.ms/arb-agreement-1>. You and Microsoft agree to these terms.
9. **ENTIRE AGREEMENT.** This agreement, and any other terms Microsoft may
provide for supplements, updates, or third-party applications, is the
entire agreement for the software.
10. **APPLICABLE LAW AND PLACE TO RESOLVE DISPUTES.** If you acquired the
software in the United States or Canada, the laws of the state or
province where you live (or, if a business, where your principal place of
business is located) govern the interpretation of this agreement, claims
for its breach, and all other claims (including consumer protection,
unfair competition, and tort claims), regardless of conflict of laws
principles, except that the FAA governs everything related to arbitration.
If you acquired the software in any other country, its laws apply, except
that the FAA governs everything related to arbitration. If U.S. federal
jurisdiction exists, you and Microsoft consent to exclusive jurisdiction
and venue in the federal court in King County, Washington for all
disputes heard in court (excluding arbitration). If not, you and
Microsoft consent to exclusive jurisdiction and venue in the Superior
Court of King County, Washington for all disputes heard in court
(excluding arbitration).
11. **CONSUMER RIGHTS; REGIONAL VARIATIONS.** This agreement describes
certain legal rights. You may have other rights, including consumer
rights, under the laws of your state, province, or country. Separate and
apart from your relationship with Microsoft, you may also have rights
with respect to the party from which you acquired the software. This
agreement does not change those other rights if the laws of your state,
province, or country do not permit it to do so. For example, if you
acquired the software in one of the below regions, or mandatory country
law applies, then the following provisions apply to you:
a. **Australia.** You have statutory guarantees under the Australian
Consumer Law and nothing in this agreement is intended to affect those
rights.
b. **Canada.** If you acquired this software in Canada, you may stop
receiving updates by turning off the automatic update feature,
disconnecting your device from the Internet (if and when you
re-connect to the Internet, however, the software will resume checking
for and installing updates), or uninstalling the software. The product
documentation, if any, may also specify how to turn off updates for
your specific device or software.
c. **Germany and Austria.**
i. **Warranty.** The properly licensed software will perform
substantially as described in any Microsoft materials that
accompany the software. However, Microsoft gives no contractual
guarantee in relation to the licensed software.
ii. **Limitation of Liability.** In case of intentional conduct, gross
negligence, claims based on the Product Liability Act, as well as,
in case of death or personal or physical injury, Microsoft is
liable according to the statutory law.
Subject to the foregoing clause ii., Microsoft will only be liable for
slight negligence if Microsoft is in breach of such material
contractual obligations, the fulfillment of which facilitate the due
performance of this agreement, the breach of which would endanger the
purpose of this agreement and the compliance with which a party may
constantly trust in (so-called "cardinal obligations"). In other cases
of slight negligence, Microsoft will not be liable for slight
negligence.
12. **DISCLAIMER OF WARRANTY. THE SOFTWARE IS LICENSED "AS IS." YOU BEAR THE
RISK OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES, OR
CONDITIONS. TO THE EXTENT PERMITTED UNDER APPLICABLE LAWS, MICROSOFT
EXCLUDES ALL IMPLIED WARRANTIES, INCLUDING MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE, AND NON-INFRINGEMENT.**
13. **LIMITATION ON AND EXCLUSION OF DAMAGES. IF YOU HAVE ANY BASIS FOR
RECOVERING DAMAGES DESPITE THE PRECEDING DISCLAIMER OF WARRANTY, YOU CAN
RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S.
$5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL,
LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES.**
This limitation applies to (a) anything related to the software,
services, content (including code) on third party Internet sites, or
third party applications; and (b) claims for breach of contract, warranty,
guarantee, or condition; strict liability, negligence, or other tort; or
any other claim; in each case to the extent permitted by applicable law.
It also applies even if Microsoft knew or should have known about the
possibility of the damages. The above limitation or exclusion may not
apply to you because your state, province, or country may not allow the
exclusion or limitation of incidental, consequential, or other damages.
---
*Please note: As this software is distributed in Canada, some of the clauses
in this agreement are provided below in French.*
*Remarque: Ce logiciel étant distribué au Canada, certaines des clauses dans
ce contrat sont fournies ci-dessous en français.*
**EXONÉRATION DE GARANTIE.** Le logiciel visé par une licence est offert
« tel quel ». Toute utilisation de ce logiciel est à votre seule risque et
péril. Microsoft n'accorde aucune autre garantie expresse. Vous pouvez
bénéficier de droits additionnels en vertu du droit local sur la protection
des consommateurs, que ce contrat ne peut modifier. La ou elles sont
permises par le droit locale, les garanties implicites de qualité marchande,
d'adéquation à un usage particulier et d'absence de contrefaçon sont exclues.
**LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES
DOMMAGES.** Vous pouvez obtenir de Microsoft et de ses fournisseurs une
indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US.
Vous ne pouvez prétendre à aucune indemnisation pour les autres dommages, y
compris les dommages spéciaux, indirects ou accessoires et pertes de
bénéfices.
Cette limitation concerne:
- tout ce qui est relié au logiciel, aux services ou au contenu (y compris le
code) figurant sur des sites Internet tiers ou dans des programmes tiers; et
- les réclamations au titre de violation de contrat ou de garantie, ou au
titre de responsabilité stricte, de négligence ou d'une autre faute dans la
limite autorisée par la loi en vigueur.
Elle s'applique également, même si Microsoft connaissait ou devrait connaître
l'éventualité d'un tel dommage. Si votre pays n'autorise pas l'exclusion ou
la limitation de responsabilité pour les dommages indirects, accessoires ou
de quelque nature que ce soit, il se peut que la limitation ou l'exclusion
ci-dessus ne s'appliquera pas à votre égard.
**EFFET JURIDIQUE.** Le présent contrat décrit certains droits juridiques.
Vous pourriez avoir d'autres droits prévus par les lois de votre pays. Le
présent contrat ne modifie pas les droits que vous confèrent les lois de
votre pays si celles-ci ne le permettent pas.

View file

@ -1,51 +1,80 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="150" viewBox="0 0 850 150">
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="230" viewBox="0 0 850 230">
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="28" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">Architecture</text>
<text x="425" y="28" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">Architecture: Two Layers, One CLI</text>
<!-- Box 1: Claude Code -->
<rect x="15" y="50" width="130" height="75" rx="8" fill="#161b22" stroke="#d97757" stroke-width="2"/>
<text x="80" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#d97757" text-anchor="middle" font-weight="bold">Claude Code</text>
<text x="80" y="104" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">AI Agent</text>
<rect x="15" y="65" width="110" height="130" rx="8" fill="#161b22" stroke="#d97757" stroke-width="2"/>
<text x="70" y="105" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#d97757" text-anchor="middle" font-weight="bold">Claude</text>
<text x="70" y="120" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#d97757" text-anchor="middle" font-weight="bold">Code</text>
<text x="70" y="142" font-family="'Courier New', Courier, monospace" font-size="13" fill="#8b949e" text-anchor="middle">AI Agent</text>
<text x="70" y="157" font-family="'Courier New', Courier, monospace" font-size="13" fill="#8b949e" text-anchor="middle">12 Skills</text>
<!-- Arrow 1 > 2 -->
<line x1="149" y1="88" x2="185" y2="88" stroke="#F2C811" stroke-width="2" stroke-dasharray="4,3" stroke-opacity="0.5"/>
<polygon points="189,88 182,83 182,93" fill="#F2C811" fill-opacity="0.5"/>
<text x="169" y="78" font-family="'Segoe UI', Arial, sans-serif" font-size="8" fill="#8b949e" text-anchor="middle">CLI</text>
<!-- Arrow Claude > pbi-cli -->
<line x1="129" y1="130" x2="172" y2="130" stroke="#F2C811" stroke-width="2" stroke-dasharray="4,3" stroke-opacity="0.4"/>
<polygon points="176,130 169,125 169,135" fill="#F2C811" fill-opacity="0.5"/>
<!-- Box 2: pbi-cli -->
<rect x="193" y="50" width="130" height="75" rx="8" fill="#161b22" stroke="#58a6ff" stroke-width="2"/>
<text x="258" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#58a6ff" text-anchor="middle" font-weight="bold">pbi-cli</text>
<text x="258" y="104" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">Python + Click</text>
<!-- Box 2: pbi-cli (taller, showing two paths) -->
<rect x="180" y="65" width="120" height="130" rx="8" fill="#161b22" stroke="#F2C811" stroke-width="2"/>
<text x="240" y="95" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#F2C811" text-anchor="middle" font-weight="bold">pbi-cli</text>
<line x1="195" y1="105" x2="285" y2="105" stroke="#F2C811" stroke-width="1" stroke-opacity="0.15"/>
<text x="240" y="125" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#58a6ff" text-anchor="middle">TOM Backend</text>
<text x="240" y="140" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">(.NET interop)</text>
<line x1="195" y1="148" x2="285" y2="148" stroke="#F2C811" stroke-width="1" stroke-opacity="0.1"/>
<text x="240" y="167" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0" text-anchor="middle">PBIR Backend</text>
<text x="240" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">(JSON files)</text>
<!-- Arrow 2 > 3 (wider gap for "in-process" label) -->
<line x1="327" y1="88" x2="393" y2="88" stroke="#F2C811" stroke-width="2" stroke-dasharray="4,3" stroke-opacity="0.5"/>
<polygon points="397,88 390,83 390,93" fill="#F2C811" fill-opacity="0.5"/>
<text x="362" y="78" font-family="'Segoe UI', Arial, sans-serif" font-size="9" fill="#8b949e" text-anchor="middle">in-process</text>
<!-- ============= TOP PATH: Modeling Layer ============= -->
<!-- Box 3: pythonnet -->
<rect x="401" y="50" width="120" height="75" rx="8" fill="#161b22" stroke="#7b61ff" stroke-width="2"/>
<text x="461" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#7b61ff" text-anchor="middle" font-weight="bold">pythonnet</text>
<text x="461" y="104" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">CLR Bridge</text>
<!-- Arrow pbi-cli > pythonnet (top) -->
<line x1="304" y1="105" x2="362" y2="105" stroke="#58a6ff" stroke-width="2" stroke-dasharray="4,3" stroke-opacity="0.4"/>
<polygon points="366,105 359,100 359,110" fill="#58a6ff" fill-opacity="0.5"/>
<text x="335" y="96" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">in-process</text>
<!-- Arrow 3 > 4 -->
<line x1="525" y1="88" x2="561" y2="88" stroke="#F2C811" stroke-width="2" stroke-dasharray="4,3" stroke-opacity="0.5"/>
<polygon points="565,88 558,83 558,93" fill="#F2C811" fill-opacity="0.5"/>
<text x="545" y="78" font-family="'Segoe UI', Arial, sans-serif" font-size="8" fill="#8b949e" text-anchor="middle">.NET calls</text>
<!-- Box 3a: pythonnet -->
<rect x="370" y="73" width="100" height="55" rx="8" fill="#161b22" stroke="#7b61ff" stroke-width="1.5"/>
<text x="420" y="98" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#7b61ff" text-anchor="middle" font-weight="bold">pythonnet</text>
<text x="420" y="114" font-family="'Courier New', Courier, monospace" font-size="13" fill="#8b949e" text-anchor="middle">CLR Bridge</text>
<!-- Box 4: .NET TOM -->
<rect x="569" y="50" width="130" height="75" rx="8" fill="#161b22" stroke="#F2C811" stroke-width="2"/>
<text x="634" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#F2C811" text-anchor="middle" font-weight="bold">.NET TOM</text>
<text x="634" y="104" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">ADOMD.NET</text>
<!-- Arrow pythonnet > .NET TOM -->
<line x1="474" y1="100" x2="512" y2="100" stroke="#58a6ff" stroke-width="2" stroke-dasharray="4,3" stroke-opacity="0.4"/>
<polygon points="516,100 509,95 509,105" fill="#58a6ff" fill-opacity="0.5"/>
<!-- Arrow 4 > 5 -->
<line x1="703" y1="88" x2="727" y2="88" stroke="#F2C811" stroke-width="2" stroke-dasharray="4,3" stroke-opacity="0.5"/>
<polygon points="731,88 724,83 724,93" fill="#F2C811" fill-opacity="0.5"/>
<text x="717" y="78" font-family="'Segoe UI', Arial, sans-serif" font-size="8" fill="#8b949e" text-anchor="middle">XMLA</text>
<!-- Box 4a: .NET TOM -->
<rect x="520" y="73" width="110" height="55" rx="8" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
<text x="575" y="98" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#58a6ff" text-anchor="middle" font-weight="bold">.NET TOM</text>
<text x="575" y="114" font-family="'Courier New', Courier, monospace" font-size="13" fill="#8b949e" text-anchor="middle">ADOMD.NET</text>
<!-- Box 5: Power BI -->
<rect x="735" y="50" width="100" height="75" rx="8" fill="#161b22" stroke="#F2C811" stroke-width="2"/>
<text x="785" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#F2C811" text-anchor="middle" font-weight="bold">Power BI</text>
<text x="785" y="104" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">msmdsrv.exe</text>
<!-- Arrow .NET TOM > Power BI (top) -->
<line x1="634" y1="100" x2="700" y2="100" stroke="#58a6ff" stroke-width="2" stroke-dasharray="4,3" stroke-opacity="0.4"/>
<polygon points="704,100 697,95 697,105" fill="#58a6ff" fill-opacity="0.5"/>
<text x="670" y="91" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">XMLA</text>
<!-- ============= BOTTOM PATH: Report Layer ============= -->
<!-- Arrow pbi-cli > PBIR Files (bottom) -->
<line x1="304" y1="170" x2="520" y2="170" stroke="#06d6a0" stroke-width="2" stroke-dasharray="4,3" stroke-opacity="0.4"/>
<polygon points="524,170 517,165 517,175" fill="#06d6a0" fill-opacity="0.5"/>
<text x="412" y="161" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">read/write JSON</text>
<!-- Box 3b: PBIR Files -->
<rect x="528" y="143" width="100" height="55" rx="8" fill="#161b22" stroke="#06d6a0" stroke-width="1.5"/>
<text x="578" y="168" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0" text-anchor="middle" font-weight="bold">PBIR Files</text>
<text x="578" y="184" font-family="'Courier New', Courier, monospace" font-size="13" fill="#8b949e" text-anchor="middle">.Report/</text>
<!-- Arrow PBIR > Power BI (bottom, with auto-sync label) -->
<line x1="632" y1="170" x2="700" y2="170" stroke="#06d6a0" stroke-width="2" stroke-dasharray="4,3" stroke-opacity="0.4"/>
<polygon points="704,170 697,165 697,175" fill="#06d6a0" fill-opacity="0.5"/>
<text x="670" y="161" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">auto-sync</text>
<!-- Box 5: Power BI Desktop (spans both paths) -->
<rect x="708" y="65" width="128" height="140" rx="8" fill="#161b22" stroke="#F2C811" stroke-width="2"/>
<text x="772" y="112" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#F2C811" text-anchor="middle" font-weight="bold">Power BI</text>
<text x="772" y="130" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#F2C811" text-anchor="middle" font-weight="bold">Desktop</text>
<text x="772" y="155" font-family="'Courier New', Courier, monospace" font-size="13" fill="#8b949e" text-anchor="middle">Semantic Model</text>
<text x="772" y="170" font-family="'Courier New', Courier, monospace" font-size="13" fill="#8b949e" text-anchor="middle">+ Report</text>
<!-- Footer -->
<text x="425" y="218" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">No MCP server, no external binaries. Sub-second execution.</text>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

118
assets/auto-sync.svg Normal file
View file

@ -0,0 +1,118 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="380" viewBox="0 0 850 380">
<defs>
<linearGradient id="sync-glow" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#06d6a0" stop-opacity="0"/>
<stop offset="50%" stop-color="#06d6a0" stop-opacity="0.15"/>
<stop offset="100%" stop-color="#06d6a0" stop-opacity="0"/>
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="30" font-family="'Segoe UI', Arial, sans-serif" font-size="20" fill="#06d6a0" text-anchor="middle" font-weight="bold">Desktop Auto-Sync</text>
<text x="425" y="52" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Report changes appear in Desktop automatically. Your modeling work is preserved.</text>
<!-- ============ 4 Steps: equal width, centered ============ -->
<!-- Card width=160, arrow gap=30, total = 4*160 + 3*30 = 730, start x = (850-730)/2 = 60 -->
<!-- Step 1: CLI Writes -->
<rect x="60" y="75" width="160" height="165" rx="10" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
<!-- Step badge -->
<circle cx="140" cy="92" r="14" fill="#58a6ff"/>
<text x="140" y="98" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#0d1117" text-anchor="middle" font-weight="bold">1</text>
<text x="140" y="126" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#58a6ff" text-anchor="middle" font-weight="bold">CLI Writes</text>
<!-- File icon -->
<g transform="translate(110, 140)">
<rect x="0" y="0" width="32" height="40" rx="3" fill="none" stroke="#58a6ff" stroke-width="1.5"/>
<line x1="7" y1="11" x2="25" y2="11" stroke="#58a6ff" stroke-width="1" stroke-opacity="0.4"/>
<line x1="7" y1="19" x2="25" y2="19" stroke="#58a6ff" stroke-width="1" stroke-opacity="0.4"/>
<line x1="7" y1="27" x2="18" y2="27" stroke="#58a6ff" stroke-width="1" stroke-opacity="0.4"/>
<!-- Pencil -->
<line x1="26" y1="30" x2="38" y2="18" stroke="#06d6a0" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="38" cy="18" r="2.5" fill="#06d6a0"/>
</g>
<text x="140" y="200" font-family="'Courier New', Courier, monospace" font-size="11" fill="#8b949e" text-anchor="middle">page.json</text>
<text x="140" y="215" font-family="'Courier New', Courier, monospace" font-size="11" fill="#8b949e" text-anchor="middle">visual.json</text>
<text x="140" y="230" font-family="'Courier New', Courier, monospace" font-size="11" fill="#8b949e" text-anchor="middle">filters</text>
<!-- Arrow 1 > 2 -->
<line x1="225" y1="155" x2="248" y2="155" stroke="#F2C811" stroke-width="2.5"/>
<polygon points="253,155 245,149 245,161" fill="#F2C811"/>
<!-- Step 2: Snapshot -->
<rect x="258" y="75" width="160" height="165" rx="10" fill="#161b22" stroke="#F2C811" stroke-width="1.5"/>
<circle cx="338" cy="92" r="14" fill="#F2C811"/>
<text x="338" y="98" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#0d1117" text-anchor="middle" font-weight="bold">2</text>
<text x="338" y="126" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#F2C811" text-anchor="middle" font-weight="bold">Snapshot</text>
<!-- Camera icon -->
<g transform="translate(308, 140)">
<rect x="0" y="10" width="50" height="34" rx="5" fill="none" stroke="#F2C811" stroke-width="1.5"/>
<circle cx="25" cy="27" r="11" fill="none" stroke="#F2C811" stroke-width="1.5"/>
<circle cx="25" cy="27" r="5" fill="#F2C811" opacity="0.3"/>
<rect x="16" y="4" width="18" height="10" rx="3" fill="none" stroke="#F2C811" stroke-width="1.2"/>
</g>
<text x="338" y="205" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">Captures your</text>
<text x="338" y="221" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#F2C811" text-anchor="middle" font-weight="600">PBIR changes</text>
<!-- Arrow 2 > 3 -->
<line x1="423" y1="155" x2="446" y2="155" stroke="#F2C811" stroke-width="2.5"/>
<polygon points="451,155 443,149 443,161" fill="#F2C811"/>
<!-- Step 3: Desktop Saves -->
<rect x="456" y="75" width="160" height="165" rx="10" fill="#161b22" stroke="#06d6a0" stroke-width="1.5"/>
<circle cx="536" cy="92" r="14" fill="#06d6a0"/>
<text x="536" y="98" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#0d1117" text-anchor="middle" font-weight="bold">3</text>
<text x="536" y="126" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#06d6a0" text-anchor="middle" font-weight="bold">Desktop Saves</text>
<!-- Save icon with checkmark -->
<g transform="translate(508, 140)">
<rect x="0" y="0" width="44" height="44" rx="5" fill="none" stroke="#06d6a0" stroke-width="1.5"/>
<rect x="10" y="0" width="24" height="16" rx="3" fill="none" stroke="#06d6a0" stroke-width="1" stroke-opacity="0.5"/>
<rect x="7" y="26" width="30" height="16" rx="3" fill="#06d6a0" opacity="0.12"/>
<!-- Large checkmark -->
<polyline points="14,36 20,42 32,28" fill="none" stroke="#06d6a0" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<text x="536" y="205" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">Preserves your</text>
<text x="536" y="221" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0" text-anchor="middle" font-weight="600">modeling work</text>
<!-- Arrow 3 > 4 -->
<line x1="621" y1="155" x2="644" y2="155" stroke="#F2C811" stroke-width="2.5"/>
<polygon points="649,155 641,149 641,161" fill="#F2C811"/>
<!-- Step 4: Re-apply + Reopen -->
<rect x="654" y="75" width="160" height="165" rx="10" fill="#161b22" stroke="#d97757" stroke-width="1.5"/>
<circle cx="734" cy="92" r="14" fill="#d97757"/>
<text x="734" y="98" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#0d1117" text-anchor="middle" font-weight="bold">4</text>
<text x="734" y="126" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#d97757" text-anchor="middle" font-weight="bold">Reopen</text>
<!-- Refresh icon: simple two-tone circle with PBI logo -->
<g transform="translate(710, 142)">
<circle cx="24" cy="24" r="22" fill="none" stroke="#d97757" stroke-width="2.5" stroke-dasharray="69 69" stroke-dashoffset="0"/>
<circle cx="24" cy="24" r="22" fill="none" stroke="#06d6a0" stroke-width="2.5" stroke-dasharray="69 69" stroke-dashoffset="69"/>
<!-- Center PBI mini icon -->
<rect x="13" y="13" width="7" height="20" rx="2" fill="#F2C811" opacity="0.6"/>
<rect x="21" y="18" width="7" height="15" rx="2" fill="#F2C811" opacity="0.8"/>
<rect x="29" y="22" width="7" height="11" rx="2" fill="#F2C811"/>
</g>
<text x="734" y="205" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">PBIR restored,</text>
<text x="734" y="221" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#d97757" text-anchor="middle" font-weight="600">Desktop reopens</text>
<!-- ============ Bottom comparison ============ -->
<rect x="60" y="258" width="730" height="1" fill="url(#sync-glow)"/>
<!-- Without -->
<rect x="60" y="275" width="350" height="85" rx="8" fill="#161b22" stroke="#ff6b6b" stroke-width="1" stroke-opacity="0.5"/>
<text x="235" y="300" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#ff6b6b" text-anchor="middle" font-weight="bold">Without Auto-Sync</text>
<text x="90" y="324" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#ff6b6b">&#x2717;</text>
<text x="110" y="324" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Close Desktop manually every time</text>
<text x="90" y="346" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#ff6b6b">&#x2717;</text>
<text x="110" y="346" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Risk losing unsaved modeling work</text>
<!-- With -->
<rect x="440" y="275" width="350" height="85" rx="8" fill="#161b22" stroke="#06d6a0" stroke-width="1" stroke-opacity="0.5"/>
<text x="615" y="300" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#06d6a0" text-anchor="middle" font-weight="bold">With Auto-Sync</text>
<text x="470" y="324" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#06d6a0">&#x2713;</text>
<text x="490" y="324" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#e6edf3">Desktop reopens automatically</text>
<text x="470" y="346" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#06d6a0">&#x2713;</text>
<text x="490" y="346" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#e6edf3">All work preserved -- both layers</text>
</svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -3,7 +3,7 @@
<!-- Title -->
<text x="425" y="32" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">Undo Anything. Instantly.</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Snapshot your model before risky changes, restore when things go wrong</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Snapshot your model before risky changes, restore when things go wrong</text>
<!-- Chat container -->
<rect x="40" y="65" width="770" height="420" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
@ -14,7 +14,7 @@
<circle cx="62" cy="80" r="5" fill="#ff5f57"/>
<circle cx="78" cy="80" r="5" fill="#febc2e"/>
<circle cx="94" cy="80" r="5" fill="#28c840"/>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#d97757" font-weight="bold">Claude Code</text>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#d97757" font-weight="bold">Claude Code</text>
<!-- User message 1 -->
<rect x="280" y="106" width="510" height="28" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -22,14 +22,14 @@
<!-- Claude response 1 -->
<rect x="60" y="146" width="520" height="62" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="166" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">Running:</text>
<text x="130" y="166" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi database export-tmdl ./snapshots/before-restructure/</text>
<text x="78" y="166" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="166" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi database export-tmdl ./snapshots/before-restructure/</text>
<text x="78" y="186" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713; Snapshot saved: 12 tables, 34 measures, 11 relationships</text>
<text x="78" y="202" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Go ahead and make your changes. You can always roll back.</text>
<text x="78" y="202" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Go ahead and make your changes. You can always roll back.</text>
<!-- Time indicator -->
<rect x="340" y="222" width="170" height="22" rx="11" fill="#21262d"/>
<text x="425" y="237" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">30 minutes later...</text>
<text x="425" y="237" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">30 minutes later...</text>
<!-- User message 2 -->
<rect x="310" y="256" width="480" height="44" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -38,11 +38,11 @@
<!-- Claude response 2 -->
<rect x="60" y="312" width="540" height="80" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="332" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">Running:</text>
<text x="130" y="332" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi database import-tmdl ./snapshots/before-restructure/</text>
<text x="78" y="332" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="332" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi database import-tmdl ./snapshots/before-restructure/</text>
<text x="78" y="352" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713; Model restored to pre-restructure state</text>
<text x="78" y="370" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9">All 12 tables, 34 measures, and 11 relationships are back.</text>
<text x="78" y="386" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Your measures should be working again. Refresh Power BI to see the changes.</text>
<text x="78" y="370" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9">All 12 tables, 34 measures, and 11 relationships are back.</text>
<text x="78" y="386" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Your measures should be working again. Refresh Power BI to see the changes.</text>
<!-- User message 3 -->
<rect x="560" y="404" width="230" height="28" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -50,8 +50,8 @@
<!-- Claude response 3 -->
<rect x="60" y="440" width="300" height="32" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="461" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9">That's what snapshots are for. Want to save a new one?</text>
<text x="78" y="461" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9">That's what snapshots are for. Want to save a new one?</text>
<!-- Footer -->
<text x="425" y="510" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Never lose work again. Snapshot before, restore after.</text>
<text x="425" y="510" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Never lose work again. Snapshot before, restore after.</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="265" viewBox="0 0 850 265">
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="265" viewBox="40 0 720 265">
<defs>
<linearGradient id="pbi-bar1" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#EBBB14"/>
@ -14,76 +14,83 @@
</linearGradient>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<rect x="40" y="0" width="720" height="265" fill="#0d1117" rx="8"/>
<!-- "VIBE MODELING" block art (shadow layer) -->
<!-- "VIBE BI" block art (shadow) -->
<text font-family="'Courier New', Courier, monospace" font-size="9" fill="#7A6508" xml:space="preserve">
<tspan x="130" y="22">██╗ ██╗ ██╗ ██████╗ ███████╗ ███╗ ███╗ ██████╗ ██████╗ ███████╗ ██╗ ██╗ ███╗ ██╗ ██████╗ </tspan>
<tspan x="130" y="33">██║ ██║ ██║ ██╔══██╗ ██╔════╝ ████╗ ████║ ██╔═══██╗ ██╔══██╗ ██╔════╝ ██║ ██║ ████╗ ██║ ██╔════╝ </tspan>
<tspan x="130" y="44">██║ ██║ ██║ ██████╔╝ █████╗ ██████╔██║ ██║ ██║ ██║ ██║ █████╗ ██║ ██║ ██╔██╗ ██║ ██║ ███╗</tspan>
<tspan x="130" y="55">╚██╗ ██╔╝ ██║ ██╔══██╗ ██╔══╝ ██║╚██╔╝██║ ██║ ██║ ██║ ██║ ██╔══╝ ██║ ██║ ██║╚██╗██║ ██║ ██║</tspan>
<tspan x="130" y="66"> ╚████╔╝ ██║ ██████╔╝ ███████╗ ██║ ╚═╝ ██║ ╚██████╔╝ ██████╔╝ ███████╗ ███████╗ ██║ ██║ ╚████║ ╚██████╔╝</tspan>
<tspan x="130" y="77"> ╚═══╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ </tspan>
<tspan x="271" y="22">██╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗ ██╗</tspan>
<tspan x="271" y="33">██║ ██║ ██║ ██╔══██╗ ██╔════╝ ██╔══██╗ ██║</tspan>
<tspan x="271" y="44">██║ ██║ ██║ ██████╔╝ █████╗ ██████╔██║</tspan>
<tspan x="271" y="55">╚██╗ ██╔╝ ██║ ██╔══██╗ ██╔══╝ ██╔══██╗ ██║</tspan>
<tspan x="271" y="66"> ╚████╔╝ ██║ ██████╔╝ ███████╗ ██████╔╝ ██║</tspan>
<tspan x="271" y="77"> ╚═══╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝</tspan>
</text>
<!-- "VIBE MODELING" block art (main layer) -->
<!-- "VIBE BI" block art (main) -->
<text font-family="'Courier New', Courier, monospace" font-size="9" fill="#F2C811" xml:space="preserve">
<tspan x="129" y="21">██╗ ██╗ ██╗ ██████╗ ███████╗ ███╗ ███╗ ██████╗ ██████╗ ███████╗ ██╗ ██╗ ███╗ ██╗ ██████╗ </tspan>
<tspan x="129" y="32">██║ ██║ ██║ ██╔══██╗ ██╔════╝ ████╗ ████║ ██╔═══██╗ ██╔══██╗ ██╔════╝ ██║ ██║ ████╗ ██║ ██╔════╝ </tspan>
<tspan x="129" y="43">██║ ██║ ██║ ██████╔╝ █████╗ ██████╔██║ ██║ ██║ ██║ ██║ █████╗ ██║ ██║ ██╔██╗ ██║ ██║ ███╗</tspan>
<tspan x="129" y="54">╚██╗ ██╔╝ ██║ ██╔══██╗ ██╔══╝ ██║╚██╔╝██║ ██║ ██║ ██║ ██║ ██╔══╝ ██║ ██║ ██║╚██╗██║ ██║ ██║</tspan>
<tspan x="129" y="65"> ╚████╔╝ ██║ ██████╔╝ ███████╗ ██║ ╚═╝ ██║ ╚██████╔╝ ██████╔╝ ███████╗ ███████╗ ██║ ██║ ╚████║ ╚██████╔╝</tspan>
<tspan x="129" y="76"> ╚═══╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ </tspan>
<tspan x="270" y="21">██╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗ ██╗</tspan>
<tspan x="270" y="32">██║ ██║ ██║ ██╔══██╗ ██╔════╝ ██╔══██╗ ██║</tspan>
<tspan x="270" y="43">██║ ██║ ██║ ██████╔╝ █████╗ ██████╔██║</tspan>
<tspan x="270" y="54">╚██╗ ██╔╝ ██║ ██╔══██╗ ██╔══╝ ██╔══██╗ ██║</tspan>
<tspan x="270" y="65"> ╚████╔╝ ██║ ██████╔╝ ███████╗ ██████╔╝ ██║</tspan>
<tspan x="270" y="76"> ╚═══╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝</tspan>
</text>
<!-- Separator -->
<line x1="60" y1="95" x2="790" y2="95" stroke="#F2C811" stroke-opacity="0.15" stroke-width="1"/>
<line x1="50" y1="87" x2="750" y2="87" stroke="#F2C811" stroke-opacity="0.15" stroke-width="1"/>
<!-- Layout: Claude(130) ~ arrow ~ PBI-CLI(425) ~ arrow ~ PowerBI(720) -->
<!-- Tagline -->
<text x="400" y="106" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#e6edf3" text-anchor="middle" font-weight="600">The First CLI for Both Power BI Modeling and Reporting</text>
<!-- Claude AI logo (centered at x=130, y=168), scale=0.075 -> 90x90 -->
<g transform="translate(85, 123) scale(0.075)">
<!-- Layout: Claude ~ arrow ~ PBI-CLI block art ~ arrow ~ PowerBI -->
<!-- Claude AI icon (centered at x=100) -->
<g transform="translate(75, 125) scale(0.04)">
<path fill="#d97757" d="M 233.959793 800.214905 L 468.644287 668.536987 L 472.590637 657.100647 L 468.644287 650.738403 L 457.208069 650.738403 L 417.986633 648.322144 L 283.892639 644.69812 L 167.597321 639.865845 L 54.926208 633.825623 L 26.577238 627.785339 L 3.3e-05 592.751709 L 2.73832 575.27533 L 26.577238 559.248352 L 60.724873 562.228149 L 136.187973 567.382629 L 249.422867 575.194763 L 331.570496 580.026978 L 453.261841 592.671082 L 472.590637 592.671082 L 475.328857 584.859009 L 468.724915 580.026978 L 463.570557 575.194763 L 346.389313 495.785217 L 219.543671 411.865906 L 153.100723 363.543762 L 117.181267 339.060425 L 99.060455 316.107361 L 91.248367 266.01355 L 123.865784 230.093994 L 167.677887 233.073853 L 178.872513 236.053772 L 223.248367 270.201477 L 318.040283 343.570496 L 441.825592 434.738342 L 459.946411 449.798706 L 467.194672 444.64447 L 468.080597 441.020203 L 459.946411 427.409485 L 392.617493 305.718323 L 320.778564 181.932983 L 288.80542 130.630859 L 280.348999 99.865845 C 277.369171 87.221436 275.194641 76.590698 275.194641 63.624268 L 312.322174 13.20813 L 332.8591 6.604126 L 382.389313 13.20813 L 403.248352 31.328979 L 434.013519 101.71814 L 483.865753 212.537048 L 561.181274 363.221497 L 583.812134 407.919434 L 595.892639 449.315491 L 600.40271 461.959839 L 608.214783 461.959839 L 608.214783 454.711609 L 614.577271 369.825623 L 626.335632 265.61084 L 637.771851 131.516846 L 641.718201 93.745117 L 660.402832 48.483276 L 697.530334 24.000122 L 726.52356 37.852417 L 750.362549 72 L 747.060486 94.067139 L 732.886047 186.201416 L 705.100708 330.52356 L 686.979919 427.167847 L 697.530334 427.167847 L 709.61084 415.087341 L 758.496704 350.174561 L 840.644348 247.490051 L 876.885925 206.738342 L 919.167847 161.71814 L 946.308838 140.29541 L 997.61084 140.29541 L 1035.38269 196.429626 L 1018.469849 254.416199 L 965.637634 321.422852 L 921.825562 378.201538 L 859.006714 462.765259 L 819.785278 530.41626 L 823.409424 535.812073 L 832.75177 534.92627 L 974.657776 504.724915 L 1051.328979 490.872559 L 1142.818848 475.167786 L 1184.214844 494.496582 L 1188.724854 514.147644 L 1172.456421 554.335693 L 1074.604126 578.496765 L 959.838989 601.449829 L 788.939636 641.879272 L 786.845764 643.409485 L 789.261841 646.389343 L 866.255127 653.637634 L 899.194702 655.409424 L 979.812134 655.409424 L 1129.932861 666.604187 L 1169.154419 692.537109 L 1192.671265 724.268677 L 1188.724854 748.429688 L 1128.322144 779.194641 L 1046.818848 759.865845 L 856.590759 714.604126 L 791.355774 698.335754 L 782.335693 698.335754 L 782.335693 703.731567 L 836.69812 756.885986 L 936.322205 846.845581 L 1061.073975 962.81897 L 1067.436279 991.490112 L 1051.409424 1014.120911 L 1034.496704 1011.704712 L 924.885986 929.234924 L 882.604126 892.107544 L 786.845764 811.48999 L 780.483276 811.48999 L 780.483276 819.946289 L 802.550415 852.241699 L 919.087341 1027.409424 L 925.127625 1081.127686 L 916.671204 1098.604126 L 886.469849 1109.154419 L 853.288696 1103.114136 L 785.073914 1007.355835 L 714.684631 899.516785 L 657.906067 802.872498 L 650.979858 806.81897 L 617.476624 1167.704834 L 601.771851 1186.147705 L 565.530212 1200 L 535.328857 1177.046997 L 519.302124 1139.919556 L 535.328857 1066.550537 L 554.657776 970.792053 L 570.362488 894.68457 L 584.536926 800.134277 L 592.993347 768.724976 L 592.429626 766.630859 L 585.503479 767.516968 L 514.22821 865.369263 L 405.825531 1011.865906 L 320.053711 1103.677979 L 299.516815 1111.812256 L 263.919525 1093.369263 L 267.221497 1060.429688 L 287.114136 1031.114136 L 405.825531 880.107361 L 477.422913 786.52356 L 523.651062 732.483276 L 523.328918 724.671265 L 520.590698 724.671265 L 205.288605 929.395935 L 149.154434 936.644409 L 124.993355 914.01355 L 127.973183 876.885986 L 139.409409 864.80542 L 234.201385 799.570435 L 233.879227 799.8927 Z"/>
</g>
<text x="130" y="222" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#d97757" text-anchor="middle" font-weight="600">Claude Code</text>
<text x="100" y="192" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#d97757" text-anchor="middle" font-weight="600">Claude Code</text>
<!-- Left arrow -->
<line x1="190" y1="175" x2="288" y2="175" stroke="#F2C811" stroke-width="2" stroke-dasharray="6,4" stroke-opacity="0.5"/>
<polygon points="292,175 284,170 284,180" fill="#F2C811" fill-opacity="0.5"/>
<line x1="148" y1="155" x2="253" y2="155" stroke="#F2C811" stroke-width="2" stroke-dasharray="6,4" stroke-opacity="0.5"/>
<polygon points="257,155 249,150 249,160" fill="#F2C811" fill-opacity="0.5"/>
<!-- PBI-CLI block art (font-size 8, ~240px wide, centered at x=425) -->
<!-- shadow -->
<!-- PBI-CLI block art as the center element (shadow) -->
<text font-family="'Courier New', Courier, monospace" font-size="8" fill="#7A6508" xml:space="preserve">
<tspan x="306" y="143">██████╗ ██████╗ ██╗ ██████╗ ██╗ ██╗</tspan>
<tspan x="306" y="155">██╔══██╗ ██╔══██╗ ██║ ██╔════╝ ██║ ██║</tspan>
<tspan x="306" y="167">██████╔╝ ██████╔╝ ██║ ███╗ ██║ ██║ ██║</tspan>
<tspan x="306" y="179">██╔═══╝ ██╔══██╗ ██║ ╚══╝ ██║ ██║ ██║</tspan>
<tspan x="306" y="191">██║ ██████╔╝ ██║ ╚██████╗ ███████╗ ██║</tspan>
<tspan x="306" y="203">╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝</tspan>
<tspan x="276" y="133">██████╗ ██████╗ ██╗ ██████╗ ██╗ ██╗</tspan>
<tspan x="276" y="145">██╔══██╗ ██╔══██╗ ██║ ██╔════╝ ██║ ██║</tspan>
<tspan x="276" y="157">██████╔╝ ██████╔╝ ██║ ███╗ ██║ ██║ ██║</tspan>
<tspan x="276" y="169">██╔═══╝ ██╔══██╗ ██║ ╚══╝ ██║ ██║ ██║</tspan>
<tspan x="276" y="181">██║ ██████╔╝ ██║ ╚██████╗ ███████╗ ██║</tspan>
<tspan x="276" y="193">╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝</tspan>
</text>
<!-- main -->
<!-- PBI-CLI block art as the center element (main) -->
<text font-family="'Courier New', Courier, monospace" font-size="8" fill="#F2C811" xml:space="preserve">
<tspan x="305" y="142">██████╗ ██████╗ ██╗ ██████╗ ██╗ ██╗</tspan>
<tspan x="305" y="154">██╔══██╗ ██╔══██╗ ██║ ██╔════╝ ██║ ██║</tspan>
<tspan x="305" y="166">██████╔╝ ██████╔╝ ██║ ███╗ ██║ ██║ ██║</tspan>
<tspan x="305" y="178">██╔═══╝ ██╔══██╗ ██║ ╚══╝ ██║ ██║ ██║</tspan>
<tspan x="305" y="190">██║ ██████╔╝ ██║ ╚██████╗ ███████╗ ██║</tspan>
<tspan x="305" y="202">╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝</tspan>
<tspan x="275" y="132">██████╗ ██████╗ ██╗ ██████╗ ██╗ ██╗</tspan>
<tspan x="275" y="144">██╔══██╗ ██╔══██╗ ██║ ██╔════╝ ██║ ██║</tspan>
<tspan x="275" y="156">██████╔╝ ██████╔╝ ██║ ███╗ ██║ ██║ ██║</tspan>
<tspan x="275" y="168">██╔═══╝ ██╔══██╗ ██║ ╚══╝ ██║ ██║ ██║</tspan>
<tspan x="275" y="180">██║ ██████╔╝ ██║ ╚██████╗ ███████╗ ██║</tspan>
<tspan x="275" y="192">╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝</tspan>
</text>
<!-- Right arrow -->
<line x1="558" y1="175" x2="656" y2="175" stroke="#F2C811" stroke-width="2" stroke-dasharray="6,4" stroke-opacity="0.5"/>
<polygon points="660,175 652,170 652,180" fill="#F2C811" fill-opacity="0.5"/>
<line x1="533" y1="155" x2="640" y2="155" stroke="#F2C811" stroke-width="2" stroke-dasharray="6,4" stroke-opacity="0.5"/>
<polygon points="644,155 636,150 636,160" fill="#F2C811" fill-opacity="0.5"/>
<!-- Power BI logo (3-bar chart, centered at x=720) -->
<g transform="translate(685, 125)">
<rect x="40" y="0" width="30" height="90" rx="5" fill="url(#pbi-bar1)"/>
<rect x="20" y="24" width="30" height="66" rx="5" fill="url(#pbi-bar2)"/>
<rect x="0" y="46" width="30" height="44" rx="5" fill="url(#pbi-bar3)"/>
<!-- Power BI logo (centered at x=700) -->
<g transform="translate(668, 120)">
<rect x="40" y="0" width="24" height="70" rx="4" fill="url(#pbi-bar1)"/>
<rect x="20" y="18" width="24" height="52" rx="4" fill="url(#pbi-bar2)"/>
<rect x="0" y="34" width="24" height="36" rx="4" fill="url(#pbi-bar3)"/>
</g>
<text x="720" y="236" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#F2C811" text-anchor="middle" font-weight="600">Power BI</text>
<text x="700" y="208" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#F2C811" text-anchor="middle" font-weight="600">Power BI</text>
<!-- Modeling + Reporting pills below PBI-CLI -->
<rect x="283" y="205" width="100" height="22" rx="11" fill="#58a6ff" fill-opacity="0.1" stroke="#58a6ff" stroke-width="1"/>
<text x="333" y="220" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#58a6ff" text-anchor="middle" font-weight="600">Modeling</text>
<text x="401" y="220" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">+</text>
<rect x="413" y="205" width="100" height="22" rx="11" fill="#06d6a0" fill-opacity="0.1" stroke="#06d6a0" stroke-width="1"/>
<text x="463" y="220" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#06d6a0" text-anchor="middle" font-weight="600">Reporting</text>
<!-- Install command -->
<text x="425" y="252" font-family="'Courier New', Courier, monospace" font-size="14" fill="#58a6ff" text-anchor="middle" font-weight="bold">pipx install pbi-cli-tool</text>
<text x="400" y="252" font-family="'Courier New', Courier, monospace" font-size="14" fill="#58a6ff" text-anchor="middle" font-weight="bold">pipx install pbi-cli-tool</text>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,68 +1,111 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="380" viewBox="0 0 850 380">
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="345" viewBox="0 0 850 345" font-family="'Segoe UI', system-ui, sans-serif">
<!-- Background -->
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="35" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">Why pbi-cli?</text>
<text x="425" y="36" font-size="19" fill="#F2C811" font-weight="bold" text-anchor="middle">Why pbi-cli?</text>
<!-- Left Panel: WITHOUT -->
<rect x="20" y="55" width="395" height="305" rx="8" fill="#161b22" stroke="#ff6b6b" stroke-width="2"/>
<text x="217" y="85" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#ff6b6b" text-anchor="middle" font-weight="bold">WITHOUT pbi-cli</text>
<!-- LEFT panel -->
<rect x="15" y="50" width="385" height="285" rx="10" fill="#160a0a" stroke="#ff4444" stroke-width="1.5"/>
<!-- Pain point 1 -->
<text x="50" y="120" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#ff6b6b">&#x2717;</text>
<text x="70" y="120" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Manual GUI clicking for every change</text>
<!-- LEFT panel header (top-rounded only via path) -->
<path d="M 25,50 L 390,50 A 10,10 0 0,1 400,60 L 400,94 L 15,94 L 15,60 A 10,10 0 0,1 25,50 Z" fill="#ff4444" fill-opacity="0.15"/>
<text x="207" y="77" font-size="15" fill="#ff6b6b" font-weight="bold" text-anchor="middle">&#x2717;&#x2003;WITHOUT pbi-cli</text>
<!-- Pain point 2 -->
<text x="50" y="155" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#ff6b6b">&#x2717;</text>
<text x="70" y="155" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">No version control for models</text>
<!-- RIGHT panel -->
<rect x="450" y="50" width="385" height="285" rx="10" fill="#0a1612" stroke="#06d6a0" stroke-width="1.5"/>
<!-- Pain point 3 -->
<text x="50" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#ff6b6b">&#x2717;</text>
<text x="70" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Copy-paste DAX from documentation</text>
<!-- RIGHT panel header (top-rounded only via path) -->
<path d="M 460,50 L 825,50 A 10,10 0 0,1 835,60 L 835,94 L 450,94 L 450,60 A 10,10 0 0,1 460,50 Z" fill="#06d6a0" fill-opacity="0.12"/>
<text x="642" y="77" font-size="15" fill="#06d6a0" font-weight="bold" text-anchor="middle">&#x2713;&#x2003;WITH pbi-cli</text>
<!-- Pain point 4 -->
<text x="50" y="225" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#ff6b6b">&#x2717;</text>
<text x="70" y="225" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">MCP tools cost 4,000+ tokens each</text>
<!-- LEFT token metric card -->
<rect x="35" y="102" width="345" height="76" rx="8" fill="#ff4444" fill-opacity="0.07" stroke="#ff4444" stroke-opacity="0.25" stroke-width="1"/>
<text x="207" y="149" font-size="44" fill="#ff4444" font-weight="bold" text-anchor="middle">4,000+</text>
<text x="207" y="168" font-size="10" fill="#8b949e" text-anchor="middle" letter-spacing="2">TOKENS PER OPERATION</text>
<!-- Pain point 5 -->
<text x="50" y="260" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#ff6b6b">&#x2717;</text>
<text x="70" y="260" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">No scripting or CI/CD integration</text>
<!-- RIGHT token metric card -->
<rect x="470" y="102" width="345" height="76" rx="8" fill="#06d6a0" fill-opacity="0.07" stroke="#06d6a0" stroke-opacity="0.25" stroke-width="1"/>
<text x="642" y="149" font-size="44" fill="#06d6a0" font-weight="bold" text-anchor="middle">~30</text>
<text x="642" y="168" font-size="10" fill="#8b949e" text-anchor="middle" letter-spacing="2">TOKENS PER OPERATION</text>
<!-- Pain point 6 -->
<text x="50" y="295" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#ff6b6b">&#x2717;</text>
<text x="70" y="295" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Context switching between tools</text>
<!-- LEFT token comparison bar (full width = expensive) -->
<rect x="35" y="186" width="345" height="7" rx="3.5" fill="#ff4444" fill-opacity="0.6"/>
<!-- Left footer -->
<text x="217" y="340" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#ff6b6b" text-anchor="middle" opacity="0.6">Slow, manual, error-prone</text>
<!-- RIGHT token comparison bars -->
<rect x="470" y="186" width="345" height="7" rx="3.5" fill="#06d6a0" fill-opacity="0.1"/>
<rect x="470" y="186" width="26" height="7" rx="3.5" fill="#06d6a0" fill-opacity="0.8"/>
<!-- Right Panel: WITH -->
<rect x="435" y="55" width="395" height="305" rx="8" fill="#161b22" stroke="#06d6a0" stroke-width="2"/>
<text x="632" y="85" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#06d6a0" text-anchor="middle" font-weight="bold">WITH pbi-cli</text>
<!-- ======================== -->
<!-- LEFT feature rows -->
<!-- ======================== -->
<!-- Solution 1 -->
<text x="465" y="120" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#06d6a0">&#x2713;</text>
<text x="485" y="120" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9">CLI commands from any terminal</text>
<!-- Row 1 -->
<rect x="35" y="202" width="345" height="40" rx="6" fill="#ff4444" fill-opacity="0.05"/>
<g transform="translate(52,211)">
<rect x="0" y="0" width="24" height="20" rx="2" fill="none" stroke="#ff6b6b" stroke-width="1.5"/>
<line x1="0" y1="7" x2="24" y2="7" stroke="#ff6b6b" stroke-width="1.5"/>
<circle cx="4" cy="3.5" r="1.5" fill="#ff6b6b" opacity="0.7"/>
<circle cx="9" cy="3.5" r="1.5" fill="#ff6b6b" opacity="0.7"/>
<circle cx="14" cy="3.5" r="1.5" fill="#ff6b6b" opacity="0.7"/>
</g>
<text x="90" y="218" font-size="12" fill="#c9d1d9" font-weight="bold">GUI-only workflow</text>
<text x="90" y="234" font-size="10.5" fill="#8b949e">Click through every change manually</text>
<!-- Solution 2 -->
<text x="465" y="155" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#06d6a0">&#x2713;</text>
<text x="485" y="155" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9">TMDL export for full Git support</text>
<!-- Row 2 -->
<rect x="35" y="247" width="345" height="40" rx="6" fill="#ff4444" fill-opacity="0.05"/>
<g transform="translate(52,256)">
<circle cx="12" cy="11" r="11" fill="none" stroke="#ff6b6b" stroke-width="1.5"/>
<line x1="3.2" y1="3.2" x2="20.8" y2="18.8" stroke="#ff6b6b" stroke-width="1.5"/>
</g>
<text x="90" y="263" font-size="12" fill="#c9d1d9" font-weight="bold">No version control</text>
<text x="90" y="279" font-size="10.5" fill="#8b949e">Reports and models untrackable</text>
<!-- Solution 3 -->
<text x="465" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#06d6a0">&#x2713;</text>
<text x="485" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9">Claude writes and validates DAX</text>
<!-- Row 3 -->
<rect x="35" y="292" width="345" height="40" rx="6" fill="#ff4444" fill-opacity="0.05"/>
<g transform="translate(52,301)">
<circle cx="12" cy="11" r="11" fill="none" stroke="#ff6b6b" stroke-width="1.5"/>
<line x1="3.2" y1="3.2" x2="20.8" y2="18.8" stroke="#ff6b6b" stroke-width="1.5"/>
</g>
<text x="90" y="308" font-size="12" fill="#c9d1d9" font-weight="bold">No AI automation</text>
<text x="90" y="324" font-size="10.5" fill="#8b949e">Cannot script or automate workflows</text>
<!-- Solution 4 -->
<text x="465" y="225" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#06d6a0">&#x2713;</text>
<text x="485" y="225" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9">CLI commands cost ~30 tokens each</text>
<!-- ======================== -->
<!-- RIGHT feature rows -->
<!-- ======================== -->
<!-- Solution 5 -->
<text x="465" y="260" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#06d6a0">&#x2713;</text>
<text x="485" y="260" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9">Script everything, automate pipelines</text>
<!-- Row 1 -->
<rect x="470" y="202" width="345" height="40" rx="6" fill="#06d6a0" fill-opacity="0.05"/>
<g transform="translate(487,211)">
<rect x="0" y="0" width="28" height="20" rx="3" fill="#06d6a0" fill-opacity="0.1" stroke="#06d6a0" stroke-width="1.5"/>
<text x="4" y="14" font-family="'Courier New', monospace" font-size="9" fill="#06d6a0">$ _</text>
</g>
<text x="528" y="218" font-size="12" fill="#c9d1d9" font-weight="bold">CLI-native commands</text>
<text x="528" y="234" font-size="10.5" fill="#8b949e">Automate from any terminal or script</text>
<!-- Solution 6 -->
<text x="465" y="295" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#06d6a0">&#x2713;</text>
<text x="485" y="295" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9">One tool, one workflow, one context</text>
<!-- Row 2 -->
<rect x="470" y="247" width="345" height="40" rx="6" fill="#06d6a0" fill-opacity="0.05"/>
<g transform="translate(487,253)">
<circle cx="5" cy="5" r="4" fill="none" stroke="#06d6a0" stroke-width="1.5"/>
<circle cx="5" cy="23" r="4" fill="none" stroke="#06d6a0" stroke-width="1.5"/>
<circle cx="19" cy="10" r="4" fill="none" stroke="#06d6a0" stroke-width="1.5"/>
<line x1="5" y1="9" x2="5" y2="19" stroke="#06d6a0" stroke-width="1.5"/>
<path d="M 5,9 C 5,12 19,10 19,10" fill="none" stroke="#06d6a0" stroke-width="1.5"/>
</g>
<text x="528" y="263" font-size="12" fill="#c9d1d9" font-weight="bold">Full Git support</text>
<text x="528" y="279" font-size="10.5" fill="#8b949e">TMDL + PBIR diff, branch, review</text>
<!-- Row 3 -->
<rect x="470" y="292" width="345" height="40" rx="6" fill="#06d6a0" fill-opacity="0.05"/>
<g transform="translate(487,300)">
<path d="M 12,0 L 14,10 L 24,12 L 14,14 L 12,24 L 10,14 L 0,12 L 10,10 Z" fill="#06d6a0" fill-opacity="0.85"/>
</g>
<text x="528" y="308" font-size="12" fill="#c9d1d9" font-weight="bold">Claude AI builds everything</text>
<text x="528" y="324" font-size="10.5" fill="#8b949e">Models, measures, and report layouts</text>
<!-- VS badge (drawn last so it sits on top of both panels) -->
<circle cx="425" cy="215" r="28" fill="#0d1117" stroke="#F2C811" stroke-width="2"/>
<text x="425" y="221" font-size="15" fill="#F2C811" font-weight="bold" text-anchor="middle">VS</text>
<!-- Right footer -->
<text x="632" y="340" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#06d6a0" text-anchor="middle" opacity="0.6">Fast, scriptable, AI-native</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View file

@ -3,7 +3,7 @@
<!-- Title -->
<text x="425" y="32" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">One Prompt, Five Measures</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Stop clicking through the GUI for every single measure</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Stop clicking through the GUI for every single measure</text>
<!-- Chat container -->
<rect x="40" y="65" width="770" height="380" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
@ -14,7 +14,7 @@
<circle cx="62" cy="80" r="5" fill="#ff5f57"/>
<circle cx="78" cy="80" r="5" fill="#febc2e"/>
<circle cx="94" cy="80" r="5" fill="#28c840"/>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#d97757" font-weight="bold">Claude Code</text>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#d97757" font-weight="bold">Claude Code</text>
<!-- User message -->
<rect x="220" y="106" width="570" height="44" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -24,47 +24,47 @@
<!-- Claude response -->
<rect x="60" y="162" width="620" height="268" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">Creating 5 measures in Sales table...</text>
<text x="78" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Creating 5 measures in Sales table...</text>
<!-- Measure 1 -->
<text x="78" y="206" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713;</text>
<text x="95" y="206" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9" font-weight="bold">YTD Revenue</text>
<text x="200" y="206" font-family="'Courier New', Courier, monospace" font-size="10" fill="#d97757">TOTALYTD ( SUM ( Sales[Amount] ), Calendar[Date] )</text>
<text x="95" y="206" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9" font-weight="bold">YTD Revenue</text>
<text x="200" y="206" font-family="'Courier New', Courier, monospace" font-size="12" fill="#d97757">TOTALYTD ( SUM ( Sales[Amount] ), Calendar[Date] )</text>
<!-- Measure 2 -->
<text x="78" y="232" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713;</text>
<text x="95" y="232" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9" font-weight="bold">QTD Revenue</text>
<text x="200" y="232" font-family="'Courier New', Courier, monospace" font-size="10" fill="#d97757">TOTALQTD ( SUM ( Sales[Amount] ), Calendar[Date] )</text>
<text x="95" y="232" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9" font-weight="bold">QTD Revenue</text>
<text x="200" y="232" font-family="'Courier New', Courier, monospace" font-size="12" fill="#d97757">TOTALQTD ( SUM ( Sales[Amount] ), Calendar[Date] )</text>
<!-- Measure 3 -->
<text x="78" y="258" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713;</text>
<text x="95" y="258" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9" font-weight="bold">MTD Revenue</text>
<text x="200" y="258" font-family="'Courier New', Courier, monospace" font-size="10" fill="#d97757">TOTALMTD ( SUM ( Sales[Amount] ), Calendar[Date] )</text>
<text x="95" y="258" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9" font-weight="bold">MTD Revenue</text>
<text x="200" y="258" font-family="'Courier New', Courier, monospace" font-size="12" fill="#d97757">TOTALMTD ( SUM ( Sales[Amount] ), Calendar[Date] )</text>
<!-- Measure 4 -->
<text x="78" y="284" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713;</text>
<text x="95" y="284" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9" font-weight="bold">PY Revenue</text>
<text x="200" y="284" font-family="'Courier New', Courier, monospace" font-size="10" fill="#d97757">CALCULATE ( SUM ( Sales[Amount] ), SAMEPERIODLASTYEAR (... ) )</text>
<text x="95" y="284" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9" font-weight="bold">PY Revenue</text>
<text x="200" y="284" font-family="'Courier New', Courier, monospace" font-size="12" fill="#d97757">CALCULATE ( SUM ( Sales[Amount] ), SAMEPERIODLASTYEAR (... ) )</text>
<!-- Measure 5 -->
<text x="78" y="310" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713;</text>
<text x="95" y="310" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9" font-weight="bold">YoY Growth %</text>
<text x="200" y="310" font-family="'Courier New', Courier, monospace" font-size="10" fill="#d97757">DIVIDE ( [YTD Revenue] - [PY Revenue], [PY Revenue] )</text>
<text x="95" y="310" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9" font-weight="bold">YoY Growth %</text>
<text x="200" y="310" font-family="'Courier New', Courier, monospace" font-size="12" fill="#d97757">DIVIDE ( [YTD Revenue] - [PY Revenue], [PY Revenue] )</text>
<!-- Summary line -->
<line x1="78" y1="325" x2="650" y2="325" stroke="#30363d" stroke-width="1"/>
<text x="78" y="345" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#F2C811" font-weight="bold">5 measures created in 3.2 seconds</text>
<text x="78" y="363" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">All formatted, described, and assigned to Sales table display folder</text>
<text x="78" y="363" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">All formatted, described, and assigned to Sales table display folder</text>
<!-- Comparison callout -->
<rect x="78" y="378" width="200" height="38" rx="6" fill="#161b22" stroke="#ff6b6b" stroke-opacity="0.4"/>
<text x="178" y="397" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#ff6b6b" text-anchor="middle">GUI: ~10 min, 50+ clicks</text>
<text x="178" y="411" font-family="'Segoe UI', Arial, sans-serif" font-size="9" fill="#8b949e" text-anchor="middle">per measure, repeated 5x</text>
<text x="178" y="397" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#ff6b6b" text-anchor="middle">GUI: ~10 min, 50+ clicks</text>
<text x="178" y="411" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">per measure, repeated 5x</text>
<rect x="298" y="378" width="200" height="38" rx="6" fill="#161b22" stroke="#06d6a0" stroke-opacity="0.4"/>
<text x="398" y="397" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#06d6a0" text-anchor="middle">pbi-cli: 3 seconds, 1 prompt</text>
<text x="398" y="411" font-family="'Segoe UI', Arial, sans-serif" font-size="9" fill="#8b949e" text-anchor="middle">all five at once</text>
<text x="398" y="397" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0" text-anchor="middle">pbi-cli: 3 seconds, 1 prompt</text>
<text x="398" y="411" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">all five at once</text>
<!-- Footer -->
<text x="425" y="470" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Batch operations that would take hours in the GUI, done in seconds</text>
<text x="425" y="470" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Batch operations that would take hours in the GUI, done in seconds</text>
</svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -0,0 +1,59 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="520" viewBox="0 0 850 520">
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="32" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#06d6a0" text-anchor="middle" font-weight="bold">Just Ask Claude -- Report Layer</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Describe the report you want, Claude builds it</text>
<!-- Chat container -->
<rect x="40" y="65" width="770" height="420" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
<!-- Header bar -->
<rect x="40" y="65" width="770" height="30" rx="8" fill="#21262d"/>
<rect x="40" y="87" width="770" height="8" fill="#21262d"/>
<!-- Traffic lights -->
<circle cx="62" cy="80" r="5" fill="#ff5f57"/>
<circle cx="78" cy="80" r="5" fill="#febc2e"/>
<circle cx="94" cy="80" r="5" fill="#28c840"/>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#d97757" font-weight="bold">Claude Code</text>
<text x="205" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">pbi-cli report session</text>
<!-- User message 1 -->
<rect x="240" y="110" width="550" height="48" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
<text x="260" y="131" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#e2e8f0">Add a bar chart showing revenue by region to the overview</text>
<text x="260" y="148" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#e2e8f0">page, and apply our corporate brand theme</text>
<!-- Claude response 1: creates visual -->
<rect x="60" y="170" width="520" height="50" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="189" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="189" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi visual add --page overview --type bar --name revenue_chart</text>
<text x="78" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713; Created barChart visual on page overview</text>
<!-- Claude response 2: binds data -->
<rect x="60" y="230" width="580" height="50" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="249" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="249" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi visual bind revenue_chart --category "Geo[Region]" --value "Sales[Revenue]"</text>
<text x="78" y="270" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713; Bound Category=Geo[Region], Y=Sales[Revenue]</text>
<!-- Claude response 3: applies theme -->
<rect x="60" y="290" width="460" height="50" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="309" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="309" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi report set-theme --file corporate-brand.json</text>
<text x="78" y="330" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713; Theme applied: Corporate Brand</text>
<!-- Desktop sync message -->
<rect x="60" y="350" width="400" height="28" rx="8" fill="#06d6a0" fill-opacity="0.06" stroke="#06d6a0" stroke-width="1" stroke-opacity="0.3"/>
<text x="78" y="369" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x21BB; Desktop synced: Power BI reopened with changes</text>
<!-- User message 2 -->
<rect x="380" y="390" width="410" height="32" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
<text x="400" y="411" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#e2e8f0">Now filter it to show only the top 10 regions</text>
<!-- Claude response 4: adds filter -->
<rect x="60" y="432" width="540" height="32" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="453" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="453" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi filters add-topn --page overview --n 10 --order-by Revenue</text>
<!-- Footer -->
<text x="425" y="508" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">No drag-and-drop. No manual binding. Just describe what you want on the page.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -3,7 +3,7 @@
<!-- Title -->
<text x="425" y="32" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">Just Ask Claude</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Plain English in, Power BI changes out</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Plain English in, Power BI changes out</text>
<!-- Chat container -->
<rect x="40" y="65" width="770" height="420" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
@ -15,8 +15,8 @@
<circle cx="62" cy="80" r="5" fill="#ff5f57"/>
<circle cx="78" cy="80" r="5" fill="#febc2e"/>
<circle cx="94" cy="80" r="5" fill="#28c840"/>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#d97757" font-weight="bold">Claude Code</text>
<text x="205" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">pbi-cli session</text>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#d97757" font-weight="bold">Claude Code</text>
<text x="205" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">pbi-cli session</text>
<!-- User message 1 -->
<rect x="350" y="110" width="440" height="32" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -24,12 +24,12 @@
<!-- Claude response 1 -->
<rect x="60" y="152" width="480" height="68" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="171" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">Running:</text>
<text x="130" y="171" font-family="'Courier New', Courier, monospace" font-size="11" fill="#58a6ff">pbi connect</text>
<text x="225" y="171" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">+</text>
<text x="237" y="171" font-family="'Courier New', Courier, monospace" font-size="11" fill="#58a6ff">pbi model stats</text>
<text x="78" y="171" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="171" font-family="'Courier New', Courier, monospace" font-size="13" fill="#58a6ff">pbi connect</text>
<text x="225" y="171" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">+</text>
<text x="237" y="171" font-family="'Courier New', Courier, monospace" font-size="13" fill="#58a6ff">pbi model stats</text>
<text x="78" y="192" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713; Connected to Adventure Works</text>
<text x="78" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9">Found 12 tables, 34 measures, 87 columns, 11 relationships</text>
<text x="78" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9">Found 12 tables, 34 measures, 87 columns, 11 relationships</text>
<!-- User message 2 -->
<rect x="280" y="232" width="510" height="32" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -37,10 +37,10 @@
<!-- Claude response 2 -->
<rect x="60" y="274" width="530" height="68" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="293" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">Running:</text>
<text x="130" y="293" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi measure create "YTD Revenue" -t Sales</text>
<text x="78" y="293" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="293" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi measure create "YTD Revenue" -t Sales</text>
<text x="78" y="312" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713; Created measure YTD Revenue in Sales</text>
<text x="78" y="330" font-family="'Courier New', Courier, monospace" font-size="10" fill="#d97757">TOTALYTD ( SUM ( Sales[Amount] ), Calendar[Date] )</text>
<text x="78" y="330" font-family="'Courier New', Courier, monospace" font-size="12" fill="#d97757">TOTALYTD ( SUM ( Sales[Amount] ), Calendar[Date] )</text>
<!-- User message 3 -->
<rect x="430" y="354" width="360" height="32" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -48,14 +48,14 @@
<!-- Claude response 3 -->
<rect x="60" y="396" width="400" height="50" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="415" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">Running:</text>
<text x="130" y="415" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi dax execute "EVALUATE ROW(..."</text>
<text x="78" y="415" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="415" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi dax execute "EVALUATE ROW(..."</text>
<text x="78" y="436" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">YTD Revenue: $1,245,890</text>
<!-- Input bar at bottom -->
<rect x="60" y="454" width="730" height="26" rx="13" fill="#0d1117" stroke="#484f58" stroke-width="1"/>
<text x="82" y="472" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#484f58">Ask Claude to modify your Power BI model...</text>
<text x="82" y="472" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#484f58">Ask Claude to modify your Power BI model...</text>
<!-- Footer -->
<text x="425" y="508" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">No DAX syntax to memorize. No GUI menus to navigate. Just describe what you need.</text>
<text x="425" y="508" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">No DAX syntax to memorize. No GUI menus to navigate. Just describe what you need.</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -3,7 +3,7 @@
<!-- Title -->
<text x="425" y="32" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">Debug DAX Like Never Before</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Describe the problem in plain English, Claude finds and fixes it</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Describe the problem in plain English, Claude finds and fixes it</text>
<!-- Chat container -->
<rect x="40" y="65" width="770" height="420" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
@ -14,7 +14,7 @@
<circle cx="62" cy="80" r="5" fill="#ff5f57"/>
<circle cx="78" cy="80" r="5" fill="#febc2e"/>
<circle cx="94" cy="80" r="5" fill="#28c840"/>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#d97757" font-weight="bold">Claude Code</text>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#d97757" font-weight="bold">Claude Code</text>
<!-- User message 1 -->
<rect x="280" y="106" width="510" height="44" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -23,29 +23,29 @@
<!-- Claude response 1: Investigation -->
<rect x="60" y="162" width="620" height="120" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9" font-weight="bold">Investigating...</text>
<text x="78" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9" font-weight="bold">Investigating...</text>
<text x="78" y="202" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">1.</text>
<text x="92" y="202" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi measure list -t Sales</text>
<text x="310" y="202" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#06d6a0">Found YTD Revenue</text>
<text x="78" y="202" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">1.</text>
<text x="92" y="202" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi measure list -t Sales</text>
<text x="310" y="202" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">Found YTD Revenue</text>
<text x="78" y="222" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">2.</text>
<text x="92" y="222" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi measure get "YTD Revenue"</text>
<text x="330" y="222" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#06d6a0">DAX syntax OK</text>
<text x="78" y="222" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">2.</text>
<text x="92" y="222" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi measure get "YTD Revenue"</text>
<text x="330" y="222" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">DAX syntax OK</text>
<text x="78" y="242" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">3.</text>
<text x="92" y="242" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi table list</text>
<text x="220" y="242" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#febc2e">&#x26A0; Calendar table exists but...</text>
<text x="78" y="242" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">3.</text>
<text x="92" y="242" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi table list</text>
<text x="220" y="242" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#febc2e">&#x26A0; Calendar table exists but...</text>
<text x="78" y="262" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">4.</text>
<text x="92" y="262" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi calendar check</text>
<text x="245" y="262" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#ff6b6b">&#x2716; Calendar NOT marked as date table</text>
<text x="78" y="262" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">4.</text>
<text x="92" y="262" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi calendar check</text>
<text x="245" y="262" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#ff6b6b">&#x2716; Calendar NOT marked as date table</text>
<!-- Claude diagnosis -->
<rect x="60" y="292" width="620" height="68" rx="12" fill="#1c1c1c" stroke="#ff6b6b" stroke-width="1" stroke-opacity="0.4"/>
<text x="78" y="312" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#ff6b6b" font-weight="bold">Root cause found</text>
<text x="78" y="330" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9">Your Calendar table lost its "Mark as Date Table" setting. TOTALYTD needs</text>
<text x="78" y="346" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9">this to work. This can happen after a refresh or schema change.</text>
<text x="78" y="330" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9">Your Calendar table lost its "Mark as Date Table" setting. TOTALYTD needs</text>
<text x="78" y="346" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9">this to work. This can happen after a refresh or schema change.</text>
<!-- User message 2 -->
<rect x="570" y="372" width="220" height="28" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -53,11 +53,11 @@
<!-- Claude response 2: Fix -->
<rect x="60" y="410" width="540" height="62" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="430" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">Running:</text>
<text x="130" y="430" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi calendar mark-as-date-table -t Calendar -c Date</text>
<text x="78" y="430" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="430" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi calendar mark-as-date-table -t Calendar -c Date</text>
<text x="78" y="450" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713; Calendar marked as date table. YTD Revenue should work now.</text>
<text x="78" y="466" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Verified: YTD Revenue = $1,245,890 (no longer blank)</text>
<text x="78" y="466" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Verified: YTD Revenue = $1,245,890 (no longer blank)</text>
<!-- Footer -->
<text x="425" y="510" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Stop guessing why your DAX is broken. Let Claude trace the problem.</text>
<text x="425" y="510" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Stop guessing why your DAX is broken. Let Claude trace the problem.</text>
</svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="340" viewBox="0 0 850 340">
<defs>
<linearGradient id="areaFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#58a6ff" stop-opacity="0.45"/>
<stop offset="100%" stop-color="#58a6ff" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="33" font-family="'Segoe UI', Arial, sans-serif" font-size="17" fill="#F2C811" text-anchor="middle" font-weight="bold">pbi-cli Downloads Over Time</text>
<text x="425" y="52" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Cumulative installs from PyPI • mirrors excluded • source: pypistats.org</text>
<!-- Gridlines & y-labels -->
<line x1="70" y1="280.0" x2="810" y2="280.0" stroke="#21262d" stroke-width="1" stroke-dasharray="2,3"/><line x1="70" y1="239.6" x2="810" y2="239.6" stroke="#21262d" stroke-width="1" stroke-dasharray="2,3"/><line x1="70" y1="199.2" x2="810" y2="199.2" stroke="#21262d" stroke-width="1" stroke-dasharray="2,3"/><line x1="70" y1="158.8" x2="810" y2="158.8" stroke="#21262d" stroke-width="1" stroke-dasharray="2,3"/><line x1="70" y1="118.4" x2="810" y2="118.4" stroke="#21262d" stroke-width="1" stroke-dasharray="2,3"/><line x1="70" y1="78.0" x2="810" y2="78.0" stroke="#21262d" stroke-width="1" stroke-dasharray="2,3"/>
<text x="62" y="284.0" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="end">0</text><text x="62" y="243.6" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="end">1,000</text><text x="62" y="203.2" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="end">2,000</text><text x="62" y="162.8" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="end">3,000</text><text x="62" y="122.4" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="end">4,000</text><text x="62" y="82.0" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="end">5,000</text>
<!-- Area fill -->
<path d="M 70.00,280 L 70.00,255.48 L 99.60,244.77 L 129.20,243.48 L 158.80,243.24 L 188.40,241.90 L 218.00,241.22 L 247.60,239.92 L 277.20,236.81 L 306.80,230.35 L 336.40,226.75 L 366.00,215.04 L 395.60,206.84 L 425.20,182.51 L 454.80,172.66 L 484.40,166.76 L 514.00,160.94 L 543.60,159.45 L 573.20,155.89 L 602.80,150.28 L 632.40,141.15 L 662.00,137.87 L 691.60,133.11 L 721.20,128.38 L 750.80,126.80 L 780.40,125.03 L 810.00,119.05 L 810.00,280 Z" fill="url(#areaFill)"/>
<!-- Line -->
<path d="M 70.00,255.48 L 99.60,244.77 L 129.20,243.48 L 158.80,243.24 L 188.40,241.90 L 218.00,241.22 L 247.60,239.92 L 277.20,236.81 L 306.80,230.35 L 336.40,226.75 L 366.00,215.04 L 395.60,206.84 L 425.20,182.51 L 454.80,172.66 L 484.40,166.76 L 514.00,160.94 L 543.60,159.45 L 573.20,155.89 L 602.80,150.28 L 632.40,141.15 L 662.00,137.87 L 691.60,133.11 L 721.20,128.38 L 750.80,126.80 L 780.40,125.03 L 810.00,119.05" fill="none" stroke="#58a6ff" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
<!-- Data points -->
<circle cx="70.00" cy="255.48" r="2.5" fill="#58a6ff"/><circle cx="99.60" cy="244.77" r="2.5" fill="#58a6ff"/><circle cx="129.20" cy="243.48" r="2.5" fill="#58a6ff"/><circle cx="158.80" cy="243.24" r="2.5" fill="#58a6ff"/><circle cx="188.40" cy="241.90" r="2.5" fill="#58a6ff"/><circle cx="218.00" cy="241.22" r="2.5" fill="#58a6ff"/><circle cx="247.60" cy="239.92" r="2.5" fill="#58a6ff"/><circle cx="277.20" cy="236.81" r="2.5" fill="#58a6ff"/><circle cx="306.80" cy="230.35" r="2.5" fill="#58a6ff"/><circle cx="336.40" cy="226.75" r="2.5" fill="#58a6ff"/><circle cx="366.00" cy="215.04" r="2.5" fill="#58a6ff"/><circle cx="395.60" cy="206.84" r="2.5" fill="#58a6ff"/><circle cx="425.20" cy="182.51" r="2.5" fill="#58a6ff"/><circle cx="454.80" cy="172.66" r="2.5" fill="#58a6ff"/><circle cx="484.40" cy="166.76" r="2.5" fill="#58a6ff"/><circle cx="514.00" cy="160.94" r="2.5" fill="#58a6ff"/><circle cx="543.60" cy="159.45" r="2.5" fill="#58a6ff"/><circle cx="573.20" cy="155.89" r="2.5" fill="#58a6ff"/><circle cx="602.80" cy="150.28" r="2.5" fill="#58a6ff"/><circle cx="632.40" cy="141.15" r="2.5" fill="#58a6ff"/><circle cx="662.00" cy="137.87" r="2.5" fill="#58a6ff"/><circle cx="691.60" cy="133.11" r="2.5" fill="#58a6ff"/><circle cx="721.20" cy="128.38" r="2.5" fill="#58a6ff"/><circle cx="750.80" cy="126.80" r="2.5" fill="#58a6ff"/><circle cx="780.40" cy="125.03" r="2.5" fill="#58a6ff"/><circle cx="810.00" cy="119.05" r="4" fill="#F2C811"/>
<!-- Last-value callout -->
<rect x="720.0" y="92.0" width="82" height="24" rx="4" fill="#0d1a2a" stroke="#F2C811" stroke-width="1"/>
<text x="761.0" y="108.0" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#F2C811" text-anchor="middle" font-weight="bold">3,984 total</text>
<!-- X-axis labels -->
<text x="70.0" y="298" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Mar 26</text><text x="247.6" y="298" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Apr 01</text><text x="454.8" y="298" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Apr 08</text><text x="632.4" y="298" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Apr 14</text><text x="810.0" y="298" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Apr 20</text>
<!-- Footer: date range -->
<text x="70" y="328" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Mar 26, 2026 → Apr 20, 2026</text>
<text x="810" y="328" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="end">26 days of data</text>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View file

@ -1,113 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="520" viewBox="0 0 850 520">
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="35" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">22 Command Groups</text>
<text x="425" y="52" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Covering the full Tabular Object Model</text>
<!-- Row 1 -->
<rect x="22" y="68" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="32" y="90" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">connect</text>
<text x="32" y="118" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Link to Power BI Desktop</text>
<rect x="227" y="68" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="237" y="90" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">dax</text>
<text x="237" y="118" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Execute and validate DAX queries</text>
<rect x="432" y="68" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="442" y="90" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">measure</text>
<text x="442" y="118" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Create, update, delete measures</text>
<rect x="637" y="68" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="647" y="90" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">table</text>
<text x="647" y="118" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Manage tables and schemas</text>
<!-- Row 2 -->
<rect x="22" y="140" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="32" y="162" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">column</text>
<text x="32" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Add, rename, hide columns</text>
<rect x="227" y="140" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="237" y="162" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">relationship</text>
<text x="237" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Define table joins and cardinality</text>
<rect x="432" y="140" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="442" y="162" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">model</text>
<text x="442" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Model metadata and statistics</text>
<rect x="637" y="140" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="647" y="162" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">database</text>
<text x="647" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">TMDL/TMSL import and export</text>
<!-- Row 3 -->
<rect x="22" y="212" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="32" y="234" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">security-role</text>
<text x="32" y="262" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Row-level security roles</text>
<rect x="227" y="212" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="237" y="234" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">calc-group</text>
<text x="237" y="262" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Calculation groups and items</text>
<rect x="432" y="212" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="442" y="234" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">partition</text>
<text x="442" y="262" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Table partitions and refresh</text>
<rect x="637" y="212" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="647" y="234" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">perspective</text>
<text x="647" y="262" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">User perspectives and views</text>
<!-- Row 4 -->
<rect x="22" y="284" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="32" y="306" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">hierarchy</text>
<text x="32" y="334" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Column hierarchies and levels</text>
<rect x="227" y="284" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="237" y="306" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">expression</text>
<text x="237" y="334" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Named M/Power Query sources</text>
<rect x="432" y="284" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="442" y="306" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">calendar</text>
<text x="442" y="334" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Date table configuration</text>
<rect x="637" y="284" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="647" y="306" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">trace</text>
<text x="647" y="334" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Query tracing and profiling</text>
<!-- Row 5 -->
<rect x="22" y="356" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="32" y="378" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">transaction</text>
<text x="32" y="406" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Atomic multi-step changes</text>
<rect x="227" y="356" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="237" y="378" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">advanced</text>
<text x="237" y="406" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Culture and locale settings</text>
<rect x="432" y="356" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="442" y="378" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">repl</text>
<text x="442" y="406" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Interactive DAX shell</text>
<rect x="637" y="356" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="647" y="378" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">skills</text>
<text x="647" y="406" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Claude Code AI skill packs</text>
<!-- Row 6 -->
<rect x="22" y="428" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="32" y="450" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">disconnect</text>
<text x="32" y="478" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Close active connection</text>
<rect x="227" y="428" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="237" y="450" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">connections</text>
<text x="237" y="478" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Manage saved sessions</text>
<rect x="432" y="428" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.3"/>
<text x="442" y="450" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#F2C811" font-weight="bold">setup</text>
<text x="442" y="478" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Environment and DLL check</text>
<!-- Summary cell -->
<rect x="637" y="428" width="195" height="62" rx="6" fill="#161b22" stroke="#F2C811" stroke-opacity="0.8"/>
<text x="734" y="456" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#F2C811" text-anchor="middle" font-weight="bold">100+ Subcommands</text>
<text x="734" y="478" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Full TOM coverage</text>
<!-- Footer -->
<text x="425" y="510" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff" text-anchor="middle" font-weight="bold">pbi &lt;group&gt; &lt;command&gt; [options]</text>
</svg>

Before

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -1,19 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="475" height="135" viewBox="0 0 475 135">
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<text font-family="'Courier New', Courier, monospace" font-size="14.5" fill="#7A6508" xml:space="preserve">
<tspan x="21" y="29">██████╗ ██████╗ ██╗ ██████╗ ██╗ ██╗</tspan>
<tspan x="21" y="47">██╔══██╗ ██╔══██╗ ██║ ██╔════╝ ██║ ██║</tspan>
<tspan x="21" y="65">██████╔╝ ██████╔╝ ██║ ███╗ ██║ ██║ ██║</tspan>
<tspan x="21" y="83">██╔═══╝ ██╔══██╗ ██║ ╚══╝ ██║ ██║ ██║</tspan>
<tspan x="21" y="101">██║ ██████╔╝ ██║ ╚██████╗ ███████╗ ██║</tspan>
<tspan x="21" y="119">╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝</tspan>
</text>
<text font-family="'Courier New', Courier, monospace" font-size="14.5" fill="#F2C811" xml:space="preserve">
<tspan x="20" y="28">██████╗ ██████╗ ██╗ ██████╗ ██╗ ██╗</tspan>
<tspan x="20" y="46">██╔══██╗ ██╔══██╗ ██║ ██╔════╝ ██║ ██║</tspan>
<tspan x="20" y="64">██████╔╝ ██████╔╝ ██║ ███╗ ██║ ██║ ██║</tspan>
<tspan x="20" y="82">██╔═══╝ ██╔══██╗ ██║ ╚══╝ ██║ ██║ ██║</tspan>
<tspan x="20" y="100">██║ ██████╔╝ ██║ ╚██████╗ ███████╗ ██║</tspan>
<tspan x="20" y="118">╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝</tspan>
</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

159
assets/layers.svg Normal file
View file

@ -0,0 +1,159 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="440" viewBox="0 0 850 440">
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="32" font-family="'Segoe UI', Arial, sans-serif" font-size="16" fill="#F2C811" text-anchor="middle" font-weight="bold">Dual-Layer Architecture</text>
<!-- ===== MODELING BAND y=48-188 ===== -->
<rect x="15" y="48" width="820" height="140" rx="10" fill="#58a6ff" fill-opacity="0.05" stroke="#58a6ff" stroke-width="1.5"/>
<rect x="15" y="48" width="6" height="140" rx="3" fill="#58a6ff" fill-opacity="0.85"/>
<!-- Database cylinder icon (centered at x=85, top of band) -->
<g transform="translate(59, 58)">
<ellipse cx="26" cy="10" rx="24" ry="10" fill="#58a6ff" fill-opacity="0.18" stroke="#58a6ff" stroke-width="2"/>
<line x1="2" y1="10" x2="2" y2="46" stroke="#58a6ff" stroke-width="2"/>
<line x1="50" y1="10" x2="50" y2="46" stroke="#58a6ff" stroke-width="2"/>
<ellipse cx="26" cy="46" rx="24" ry="10" fill="#58a6ff" fill-opacity="0.1" stroke="#58a6ff" stroke-width="2"/>
<path d="M2,22 Q26,32 50,22" fill="none" stroke="#58a6ff" stroke-width="1" opacity="0.35"/>
<path d="M2,33 Q26,43 50,33" fill="none" stroke="#58a6ff" stroke-width="1" opacity="0.35"/>
<circle cx="14" cy="26" r="2" fill="#58a6ff" fill-opacity="0.5"/>
<circle cx="26" cy="28" r="2" fill="#58a6ff" fill-opacity="0.4"/>
<circle cx="38" cy="26" r="2" fill="#58a6ff" fill-opacity="0.5"/>
<circle cx="20" cy="38" r="2" fill="#58a6ff" fill-opacity="0.3"/>
<circle cx="32" cy="38" r="2" fill="#58a6ff" fill-opacity="0.3"/>
</g>
<!-- MODELING label BELOW the cylinder icon -->
<text x="85" y="138" font-family="'Segoe UI', Arial, sans-serif" font-size="16" fill="#58a6ff" font-weight="bold" text-anchor="middle">MODELING</text>
<line x1="155" y1="54" x2="155" y2="182" stroke="#58a6ff" stroke-opacity="0.2" stroke-width="1"/>
<!-- ===== MODELING TILE 1: Export / Import ===== -->
<rect x="168" y="60" width="195" height="120" rx="8" fill="#58a6ff" fill-opacity="0.08" stroke="#58a6ff" stroke-opacity="0.3" stroke-width="1"/>
<g transform="translate(240, 68)">
<line x1="14" y1="46" x2="14" y2="8" stroke="#58a6ff" stroke-width="3" stroke-linecap="round"/>
<polygon points="14,3 8,14 20,14" fill="#58a6ff"/>
<line x1="32" y1="8" x2="32" y2="46" stroke="#58a6ff" stroke-width="3" stroke-linecap="round"/>
<polygon points="32,52 26,40 38,40" fill="#58a6ff"/>
<rect x="0" y="58" width="46" height="14" rx="7" fill="#58a6ff" fill-opacity="0.2" stroke="#58a6ff" stroke-width="0.8"/>
<text x="23" y="68" font-family="'Segoe UI', Arial, sans-serif" font-size="9" fill="#a5d6ff" text-anchor="middle" font-weight="600">TMDL</text>
</g>
<text x="265" y="158" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#a5d6ff" text-anchor="middle" font-weight="600">Export / Import</text>
<!-- ===== MODELING TILE 2: DAX Engine ===== -->
<rect x="373" y="60" width="195" height="120" rx="8" fill="#58a6ff" fill-opacity="0.08" stroke="#58a6ff" stroke-opacity="0.3" stroke-width="1"/>
<g transform="translate(446, 68)">
<circle cx="24" cy="24" r="24" fill="#F2C811" fill-opacity="0.08" stroke="#F2C811" stroke-width="2"/>
<circle cx="24" cy="24" r="16" fill="#F2C811" fill-opacity="0.06"/>
<polygon points="18,13 18,35 38,24" fill="#F2C811" fill-opacity="0.95"/>
<rect x="5" y="54" width="38" height="14" rx="7" fill="#F2C811" fill-opacity="0.2" stroke="#F2C811" stroke-width="0.8"/>
<text x="24" y="64" font-family="'Segoe UI', Arial, sans-serif" font-size="9" fill="#F2C811" text-anchor="middle" font-weight="600">DAX</text>
</g>
<text x="470" y="158" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#a5d6ff" text-anchor="middle" font-weight="600">DAX Engine</text>
<!-- ===== MODELING TILE 3: Schema Control ===== -->
<rect x="578" y="60" width="195" height="120" rx="8" fill="#58a6ff" fill-opacity="0.08" stroke="#58a6ff" stroke-opacity="0.3" stroke-width="1"/>
<g transform="translate(647, 68)">
<rect x="0" y="0" width="48" height="40" rx="3" fill="none" stroke="#58a6ff" stroke-width="2"/>
<line x1="0" y1="12" x2="48" y2="12" stroke="#58a6ff" stroke-width="1.5" opacity="0.7"/>
<line x1="16" y1="0" x2="16" y2="40" stroke="#58a6ff" stroke-width="1.5" opacity="0.5"/>
<rect x="2" y="2" width="12" height="8" rx="1" fill="#58a6ff" fill-opacity="0.4"/>
<rect x="18" y="2" width="28" height="8" rx="1" fill="#58a6ff" fill-opacity="0.4"/>
<rect x="2" y="14" width="12" height="7" rx="1" fill="#58a6ff" fill-opacity="0.3"/>
<rect x="18" y="14" width="28" height="7" rx="1" fill="#58a6ff" fill-opacity="0.3"/>
<rect x="2" y="23" width="12" height="7" rx="1" fill="#58a6ff" fill-opacity="0.2"/>
<rect x="18" y="23" width="28" height="7" rx="1" fill="#58a6ff" fill-opacity="0.2"/>
<rect x="2" y="32" width="12" height="6" rx="1" fill="#58a6ff" fill-opacity="0.1"/>
<rect x="18" y="32" width="28" height="6" rx="1" fill="#58a6ff" fill-opacity="0.1"/>
<rect x="0" y="46" width="48" height="14" rx="7" fill="#58a6ff" fill-opacity="0.2" stroke="#58a6ff" stroke-width="0.8"/>
<text x="24" y="56" font-family="'Segoe UI', Arial, sans-serif" font-size="9" fill="#a5d6ff" text-anchor="middle" font-weight="600">SCHEMA</text>
</g>
<text x="675" y="158" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#a5d6ff" text-anchor="middle" font-weight="600">Schema Control</text>
<!-- ===== CONNECTOR: PBI-CLI Block Art ===== -->
<line x1="15" y1="232" x2="310" y2="232" stroke="#F2C811" stroke-opacity="0.2" stroke-width="1" stroke-dasharray="4,4"/>
<line x1="540" y1="232" x2="835" y2="232" stroke="#F2C811" stroke-opacity="0.2" stroke-width="1" stroke-dasharray="4,4"/>
<!-- PBI-CLI block art (shadow) -->
<text font-family="'Courier New', Courier, monospace" font-size="7" fill="#7A6508" xml:space="preserve">
<tspan x="321" y="210">██████╗ ██████╗ ██╗ ██████╗ ██╗ ██╗</tspan>
<tspan x="321" y="219">██╔══██╗ ██╔══██╗ ██║ ██╔════╝ ██║ ██║</tspan>
<tspan x="321" y="228">██████╔╝ ██████╔╝ ██║ ███╗ ██║ ██║ ██║</tspan>
<tspan x="321" y="237">██╔═══╝ ██╔══██╗ ██║ ╚══╝ ██║ ██║ ██║</tspan>
<tspan x="321" y="246">██║ ██████╔╝ ██║ ╚██████╗ ███████╗ ██║</tspan>
<tspan x="321" y="255">╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝</tspan>
</text>
<!-- PBI-CLI block art (main) -->
<text font-family="'Courier New', Courier, monospace" font-size="7" fill="#F2C811" xml:space="preserve">
<tspan x="320" y="209">██████╗ ██████╗ ██╗ ██████╗ ██╗ ██╗</tspan>
<tspan x="320" y="218">██╔══██╗ ██╔══██╗ ██║ ██╔════╝ ██║ ██║</tspan>
<tspan x="320" y="227">██████╔╝ ██████╔╝ ██║ ███╗ ██║ ██║ ██║</tspan>
<tspan x="320" y="236">██╔═══╝ ██╔══██╗ ██║ ╚══╝ ██║ ██║ ██║</tspan>
<tspan x="320" y="245">██║ ██████╔╝ ██║ ╚██████╗ ███████╗ ██║</tspan>
<tspan x="320" y="254">╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝</tspan>
</text>
<!-- ===== REPORTING BAND y=278-418 ===== -->
<rect x="15" y="278" width="820" height="140" rx="10" fill="#06d6a0" fill-opacity="0.05" stroke="#06d6a0" stroke-width="1.5"/>
<rect x="15" y="278" width="6" height="140" rx="3" fill="#06d6a0" fill-opacity="0.85"/>
<!-- Bar chart icon (centered at x=85, top of band) -->
<g transform="translate(56, 288)">
<rect x="0" y="44" width="12" height="20" rx="2" fill="none" stroke="#06d6a0" stroke-width="2"/>
<rect x="15" y="30" width="12" height="34" rx="2" fill="#06d6a0" fill-opacity="0.25" stroke="#06d6a0" stroke-width="2"/>
<rect x="30" y="14" width="12" height="50" rx="2" fill="#06d6a0" fill-opacity="0.45" stroke="#06d6a0" stroke-width="2"/>
<rect x="45" y="24" width="12" height="40" rx="2" fill="#06d6a0" fill-opacity="0.35" stroke="#06d6a0" stroke-width="2"/>
<line x1="0" y1="64" x2="57" y2="64" stroke="#06d6a0" stroke-width="1.5" opacity="0.5"/>
<polyline points="6,42 21,28 36,12 51,22" fill="none" stroke="#7ee787" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
</g>
<!-- REPORTING label BELOW the bar chart icon -->
<text x="85" y="372" font-family="'Segoe UI', Arial, sans-serif" font-size="16" fill="#06d6a0" font-weight="bold" text-anchor="middle">REPORTING</text>
<line x1="155" y1="284" x2="155" y2="412" stroke="#06d6a0" stroke-opacity="0.2" stroke-width="1"/>
<!-- ===== REPORTING TILE 1: Visual Builder ===== -->
<rect x="168" y="290" width="195" height="120" rx="8" fill="#06d6a0" fill-opacity="0.08" stroke="#06d6a0" stroke-opacity="0.3" stroke-width="1"/>
<g transform="translate(228, 298)">
<rect x="0" y="0" width="60" height="44" rx="4" fill="none" stroke="#06d6a0" stroke-width="2"/>
<rect x="5" y="24" width="10" height="18" rx="1" fill="#06d6a0" fill-opacity="0.45"/>
<rect x="18" y="16" width="10" height="26" rx="1" fill="#06d6a0" fill-opacity="0.6"/>
<rect x="31" y="8" width="10" height="34" rx="1" fill="#06d6a0" fill-opacity="0.8"/>
<rect x="44" y="12" width="10" height="30" rx="1" fill="#06d6a0" fill-opacity="0.7"/>
<circle cx="54" cy="8" r="9" fill="#06d6a0" fill-opacity="0.2" stroke="#06d6a0" stroke-width="1"/>
<text x="54" y="12" font-family="'Segoe UI', Arial, sans-serif" font-size="8" fill="#06d6a0" text-anchor="middle" font-weight="bold">32</text>
<rect x="0" y="50" width="60" height="14" rx="7" fill="#06d6a0" fill-opacity="0.2" stroke="#06d6a0" stroke-width="0.8"/>
<text x="30" y="60" font-family="'Segoe UI', Arial, sans-serif" font-size="9" fill="#7ee787" text-anchor="middle" font-weight="600">VISUALS</text>
</g>
<text x="265" y="388" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#7ee787" text-anchor="middle" font-weight="600">Visual Builder</text>
<!-- ===== REPORTING TILE 2: Data Binding ===== -->
<rect x="373" y="290" width="195" height="120" rx="8" fill="#06d6a0" fill-opacity="0.08" stroke="#06d6a0" stroke-opacity="0.3" stroke-width="1"/>
<g transform="translate(430, 298)">
<circle cx="15" cy="22" r="15" fill="none" stroke="#06d6a0" stroke-width="2"/>
<circle cx="55" cy="22" r="15" fill="none" stroke="#06d6a0" stroke-width="2"/>
<rect x="24" y="16" width="12" height="12" rx="3" fill="#06d6a0" fill-opacity="0.4" stroke="#06d6a0" stroke-width="1.5"/>
<rect x="34" y="16" width="12" height="12" rx="3" fill="#06d6a0" fill-opacity="0.4" stroke="#06d6a0" stroke-width="1.5"/>
<circle cx="15" cy="22" r="5" fill="#06d6a0" fill-opacity="0.5"/>
<circle cx="55" cy="22" r="5" fill="#06d6a0" fill-opacity="0.5"/>
<rect x="10" y="44" width="50" height="14" rx="7" fill="#06d6a0" fill-opacity="0.2" stroke="#06d6a0" stroke-width="0.8"/>
<text x="35" y="54" font-family="'Segoe UI', Arial, sans-serif" font-size="9" fill="#7ee787" text-anchor="middle" font-weight="600">BIND</text>
</g>
<text x="470" y="388" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#7ee787" text-anchor="middle" font-weight="600">Data Binding</text>
<!-- ===== REPORTING TILE 3: Theme Engine ===== -->
<rect x="578" y="290" width="195" height="120" rx="8" fill="#06d6a0" fill-opacity="0.08" stroke="#06d6a0" stroke-opacity="0.3" stroke-width="1"/>
<g transform="translate(647, 298)">
<circle cx="26" cy="22" r="22" fill="none" stroke="#06d6a0" stroke-width="2"/>
<circle cx="26" cy="28" r="7" fill="#0d1117" stroke="#06d6a0" stroke-width="1.5"/>
<circle cx="12" cy="9" r="6" fill="#06d6a0" fill-opacity="0.9"/>
<circle cx="26" cy="3" r="6" fill="#F2C811" fill-opacity="0.9"/>
<circle cx="40" cy="9" r="6" fill="#58a6ff" fill-opacity="0.9"/>
<circle cx="44" cy="22" r="6" fill="#c084fc" fill-opacity="0.9"/>
<circle cx="8" cy="22" r="6" fill="#d97757" fill-opacity="0.9"/>
<rect x="3" y="50" width="46" height="14" rx="7" fill="#06d6a0" fill-opacity="0.2" stroke="#06d6a0" stroke-width="0.8"/>
<text x="26" y="60" font-family="'Segoe UI', Arial, sans-serif" font-size="9" fill="#7ee787" text-anchor="middle" font-weight="600">THEME</text>
</g>
<text x="675" y="388" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#7ee787" text-anchor="middle" font-weight="600">Theme Engine</text>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -3,7 +3,7 @@
<!-- Title -->
<text x="425" y="32" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">Model Health Check</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Ask Claude to audit your semantic model in seconds</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Ask Claude to audit your semantic model in seconds</text>
<!-- Chat container -->
<rect x="40" y="65" width="770" height="340" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
@ -14,7 +14,7 @@
<circle cx="62" cy="80" r="5" fill="#ff5f57"/>
<circle cx="78" cy="80" r="5" fill="#febc2e"/>
<circle cx="94" cy="80" r="5" fill="#28c840"/>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#d97757" font-weight="bold">Claude Code</text>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#d97757" font-weight="bold">Claude Code</text>
<!-- User message -->
<rect x="380" y="106" width="410" height="28" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -24,56 +24,56 @@
<rect x="60" y="146" width="640" height="250" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="168" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9" font-weight="bold">Model Audit Report</text>
<text x="250" y="168" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Adventure Works (12 tables, 34 measures)</text>
<text x="250" y="168" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Adventure Works (12 tables, 34 measures)</text>
<line x1="78" y1="178" x2="680" y2="178" stroke="#30363d" stroke-width="1"/>
<!-- Finding 1: Warning -->
<text x="78" y="198" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#febc2e">&#x26A0;</text>
<text x="98" y="198" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#febc2e" font-weight="bold">3 unused measures</text>
<text x="240" y="198" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Test_Calc, Old_Revenue, Copy_of_Sales not referenced anywhere</text>
<text x="98" y="198" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#febc2e" font-weight="bold">3 unused measures</text>
<text x="240" y="198" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Test_Calc, Old_Revenue, Copy_of_Sales not referenced anywhere</text>
<!-- Finding 2: Warning -->
<text x="78" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#febc2e">&#x26A0;</text>
<text x="98" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#febc2e" font-weight="bold">Wide table detected</text>
<text x="250" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Product has 42 columns, consider splitting or hiding unused</text>
<text x="98" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#febc2e" font-weight="bold">Wide table detected</text>
<text x="250" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Product has 42 columns, consider splitting or hiding unused</text>
<!-- Finding 3: Error -->
<text x="78" y="250" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#ff6b6b">&#x2716;</text>
<text x="98" y="250" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#ff6b6b" font-weight="bold">Missing relationship</text>
<text x="260" y="250" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Budget table has no join to Calendar (Date column exists)</text>
<text x="98" y="250" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#ff6b6b" font-weight="bold">Missing relationship</text>
<text x="260" y="250" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Budget table has no join to Calendar (Date column exists)</text>
<!-- Finding 4: Error -->
<text x="78" y="276" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#ff6b6b">&#x2716;</text>
<text x="98" y="276" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#ff6b6b" font-weight="bold">Bidirectional filter</text>
<text x="262" y="276" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Sales to Product is bidirectional, may cause ambiguity</text>
<text x="98" y="276" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#ff6b6b" font-weight="bold">Bidirectional filter</text>
<text x="262" y="276" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Sales to Product is bidirectional, may cause ambiguity</text>
<!-- Finding 5: OK -->
<text x="78" y="302" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713;</text>
<text x="98" y="302" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#06d6a0" font-weight="bold">Date table configured</text>
<text x="270" y="302" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Calendar marked as date table with proper hierarchy</text>
<text x="98" y="302" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#06d6a0" font-weight="bold">Date table configured</text>
<text x="270" y="302" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Calendar marked as date table with proper hierarchy</text>
<!-- Finding 6: OK -->
<text x="78" y="328" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713;</text>
<text x="98" y="328" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#06d6a0" font-weight="bold">No circular dependencies</text>
<text x="278" y="328" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">All measure references resolve correctly</text>
<text x="98" y="328" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#06d6a0" font-weight="bold">No circular dependencies</text>
<text x="278" y="328" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">All measure references resolve correctly</text>
<!-- Summary -->
<line x1="78" y1="342" x2="680" y2="342" stroke="#30363d" stroke-width="1"/>
<text x="78" y="362" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#c9d1d9">Summary:</text>
<text x="142" y="362" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#ff6b6b">2 errors</text>
<text x="205" y="362" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#febc2e">2 warnings</text>
<text x="290" y="362" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#06d6a0">2 passed</text>
<text x="78" y="362" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9">Summary:</text>
<text x="142" y="362" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#ff6b6b">2 errors</text>
<text x="205" y="362" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#febc2e">2 warnings</text>
<text x="290" y="362" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#06d6a0">2 passed</text>
<text x="78" y="382" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">Shall I fix the missing relationship and remove the unused measures?</text>
<text x="78" y="382" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Shall I fix the missing relationship and remove the unused measures?</text>
<!-- Comparison (outside chat container, centered) -->
<rect x="225" y="420" width="190" height="32" rx="16" fill="#161b22" stroke="#ff6b6b" stroke-opacity="0.4"/>
<text x="320" y="441" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#ff6b6b" text-anchor="middle">Manual review: 30+ minutes</text>
<text x="320" y="441" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#ff6b6b" text-anchor="middle">Manual review: 30+ minutes</text>
<rect x="435" y="420" width="190" height="32" rx="16" fill="#161b22" stroke="#06d6a0" stroke-opacity="0.4"/>
<text x="530" y="441" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#06d6a0" text-anchor="middle">pbi-cli: instant audit</text>
<text x="530" y="441" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0" text-anchor="middle">pbi-cli: instant audit</text>
<!-- Footer -->
<text x="425" y="480" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Catch model issues before they reach production</text>
<text x="425" y="480" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Catch model issues before they reach production</text>
</svg>

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

126
assets/report-layer.svg Normal file
View file

@ -0,0 +1,126 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="350" viewBox="0 0 850 350">
<defs>
<linearGradient id="rl-glow" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#06d6a0" stop-opacity="0"/>
<stop offset="50%" stop-color="#06d6a0" stop-opacity="0.12"/>
<stop offset="100%" stop-color="#06d6a0" stop-opacity="0"/>
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="30" font-family="'Segoe UI', Arial, sans-serif" font-size="20" fill="#06d6a0" text-anchor="middle" font-weight="bold">Report Layer</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Build entire Power BI reports from the command line</text>
<!-- Glow line -->
<rect x="100" y="60" width="650" height="1" fill="url(#rl-glow)"/>
<!-- ============ 4 visual cards with large icons ============ -->
<!-- Card 1: Visuals -->
<rect x="30" y="78" width="185" height="200" rx="10" fill="#161b22" stroke="#d97757" stroke-width="1.5"/>
<!-- Large bar chart icon -->
<g transform="translate(62, 95)">
<rect x="0" y="50" width="20" height="35" rx="3" fill="#d97757" opacity="0.4"/>
<rect x="28" y="30" width="20" height="55" rx="3" fill="#d97757" opacity="0.6"/>
<rect x="56" y="10" width="20" height="75" rx="3" fill="#d97757" opacity="0.8"/>
<rect x="84" y="0" width="20" height="85" rx="3" fill="#d97757"/>
<!-- Axis lines -->
<line x1="-5" y1="86" x2="110" y2="86" stroke="#d97757" stroke-width="1" stroke-opacity="0.3"/>
<line x1="-5" y1="0" x2="-5" y2="86" stroke="#d97757" stroke-width="1" stroke-opacity="0.3"/>
</g>
<text x="122" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="16" fill="#d97757" text-anchor="middle" font-weight="bold">32 Visual Types</text>
<text x="122" y="228" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Charts, cards, KPIs,</text>
<text x="122" y="243" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">tables, slicers, maps</text>
<text x="122" y="265" font-family="'Courier New', Courier, monospace" font-size="11" fill="#58a6ff" text-anchor="middle">pbi visual add</text>
<!-- Card 2: Data Binding -->
<rect x="232" y="78" width="185" height="200" rx="10" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
<!-- Binding icon: table arrow to chart -->
<g transform="translate(267, 100)">
<!-- Table icon -->
<rect x="0" y="10" width="40" height="50" rx="4" fill="none" stroke="#58a6ff" stroke-width="1.5"/>
<line x1="0" y1="24" x2="40" y2="24" stroke="#58a6ff" stroke-width="1" stroke-opacity="0.5"/>
<line x1="0" y1="38" x2="40" y2="38" stroke="#58a6ff" stroke-width="1" stroke-opacity="0.5"/>
<line x1="16" y1="10" x2="16" y2="60" stroke="#58a6ff" stroke-width="1" stroke-opacity="0.3"/>
<text x="20" y="8" font-family="'Segoe UI', Arial, sans-serif" font-size="8" fill="#58a6ff" text-anchor="middle">Table[Col]</text>
<!-- Arrow -->
<line x1="48" y1="35" x2="68" y2="35" stroke="#F2C811" stroke-width="2" stroke-dasharray="3,2"/>
<polygon points="72,35 66,30 66,40" fill="#F2C811"/>
<!-- Chart icon -->
<rect x="76" y="10" width="40" height="50" rx="4" fill="none" stroke="#d97757" stroke-width="1.5"/>
<rect x="82" y="36" width="8" height="18" rx="1" fill="#d97757" opacity="0.5"/>
<rect x="92" y="26" width="8" height="28" rx="1" fill="#d97757" opacity="0.7"/>
<rect x="102" y="18" width="8" height="36" rx="1" fill="#d97757"/>
</g>
<text x="324" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="16" fill="#58a6ff" text-anchor="middle" font-weight="bold">Data Binding</text>
<text x="324" y="228" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Connect model fields</text>
<text x="324" y="243" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">to visual roles</text>
<text x="324" y="265" font-family="'Courier New', Courier, monospace" font-size="11" fill="#58a6ff" text-anchor="middle">pbi visual bind</text>
<!-- Card 3: Themes -->
<rect x="434" y="78" width="185" height="200" rx="10" fill="#161b22" stroke="#F2C811" stroke-width="1.5"/>
<!-- Large colour palette icon -->
<g transform="translate(472, 100)">
<!-- Palette shape -->
<ellipse cx="55" cy="35" rx="50" ry="35" fill="none" stroke="#F2C811" stroke-width="1.2" stroke-opacity="0.3"/>
<!-- Colour dots -->
<circle cx="25" cy="20" r="10" fill="#0078D4"/>
<circle cx="50" cy="14" r="10" fill="#00BCF2"/>
<circle cx="75" cy="18" r="10" fill="#FFB900"/>
<circle cx="85" cy="40" r="10" fill="#D83B01"/>
<circle cx="68" cy="52" r="10" fill="#8661C5"/>
<circle cx="38" cy="52" r="10" fill="#00B294"/>
</g>
<text x="526" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="16" fill="#F2C811" text-anchor="middle" font-weight="bold">Themes</text>
<text x="526" y="228" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Brand colours, formatting</text>
<text x="526" y="243" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">rules, diff before apply</text>
<text x="526" y="265" font-family="'Courier New', Courier, monospace" font-size="11" fill="#58a6ff" text-anchor="middle">pbi report set-theme</text>
<!-- Card 4: Filters -->
<rect x="636" y="78" width="185" height="200" rx="10" fill="#161b22" stroke="#ff6b6b" stroke-width="1.5"/>
<!-- Large funnel icon -->
<g transform="translate(680, 96)">
<polygon points="0,0 90,0 68,28 22,28" fill="none" stroke="#ff6b6b" stroke-width="1.8" opacity="0.4"/>
<polygon points="22,28 68,28 58,50 32,50" fill="none" stroke="#ff6b6b" stroke-width="1.8" opacity="0.7"/>
<polygon points="32,50 58,50 52,68 38,68" fill="none" stroke="#ff6b6b" stroke-width="1.8"/>
<!-- Data dots being filtered -->
<circle cx="15" cy="-8" r="3" fill="#ff6b6b" opacity="0.3"/>
<circle cx="30" cy="-10" r="3" fill="#ff6b6b" opacity="0.4"/>
<circle cx="45" cy="-7" r="3" fill="#ff6b6b" opacity="0.5"/>
<circle cx="60" cy="-9" r="3" fill="#ff6b6b" opacity="0.4"/>
<circle cx="75" cy="-8" r="3" fill="#ff6b6b" opacity="0.3"/>
<!-- Output dot -->
<circle cx="45" cy="78" r="4" fill="#06d6a0"/>
</g>
<text x="728" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="16" fill="#ff6b6b" text-anchor="middle" font-weight="bold">Filters</text>
<text x="728" y="228" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">TopN, date range,</text>
<text x="728" y="243" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">categorical filters</text>
<text x="728" y="265" font-family="'Courier New', Courier, monospace" font-size="11" fill="#58a6ff" text-anchor="middle">pbi filters add-topn</text>
<!-- ============ Bottom bar: key capabilities ============ -->
<rect x="30" y="295" width="790" height="40" rx="8" fill="#161b22" stroke="#06d6a0" stroke-width="1" stroke-opacity="0.3"/>
<circle cx="70" cy="315" r="4" fill="#06d6a0"/>
<text x="82" y="319" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#e6edf3">No connection needed</text>
<circle cx="260" cy="315" r="4" fill="#06d6a0"/>
<text x="272" y="319" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#e6edf3">Auto-syncs with Desktop</text>
<circle cx="470" cy="315" r="4" fill="#06d6a0"/>
<text x="482" y="319" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#e6edf3">Bulk operations</text>
<circle cx="630" cy="315" r="4" fill="#06d6a0"/>
<text x="642" y="319" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#e6edf3">Git-friendly PBIR</text>
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

124
assets/report-workflow.svg Normal file
View file

@ -0,0 +1,124 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="280" viewBox="0 0 850 280">
<defs>
<linearGradient id="rw-glow" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#06d6a0" stop-opacity="0"/>
<stop offset="50%" stop-color="#06d6a0" stop-opacity="0.12"/>
<stop offset="100%" stop-color="#06d6a0" stop-opacity="0"/>
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="28" font-family="'Segoe UI', Arial, sans-serif" font-size="20" fill="#06d6a0" text-anchor="middle" font-weight="bold">Build a Report in 6 Steps</text>
<text x="425" y="48" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">From empty folder to a complete, themed report with visuals and filters</text>
<rect x="60" y="58" width="730" height="1" fill="url(#rw-glow)"/>
<!-- 6 steps in a horizontal flow -->
<!-- Step width=110, arrow=22, total=6*110+5*22=770, start=(850-770)/2=40 -->
<!-- Step 1: Scaffold -->
<rect x="40" y="75" width="110" height="120" rx="8" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
<circle cx="95" cy="92" r="11" fill="#58a6ff"/>
<text x="95" y="97" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#0d1117" text-anchor="middle" font-weight="bold">1</text>
<text x="95" y="118" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#58a6ff" text-anchor="middle" font-weight="bold">Scaffold</text>
<!-- Folder icon -->
<g transform="translate(72, 128)">
<rect x="0" y="6" width="40" height="28" rx="3" fill="none" stroke="#58a6ff" stroke-width="1.2"/>
<rect x="0" y="2" width="18" height="8" rx="2" fill="none" stroke="#58a6ff" stroke-width="1"/>
</g>
<text x="95" y="175" font-family="'Courier New', Courier, monospace" font-size="9" fill="#8b949e" text-anchor="middle">report create</text>
<!-- Arrow -->
<line x1="154" y1="135" x2="168" y2="135" stroke="#F2C811" stroke-width="2"/>
<polygon points="172,135 166,130 166,140" fill="#F2C811"/>
<!-- Step 2: Add Pages -->
<rect x="176" y="75" width="110" height="120" rx="8" fill="#161b22" stroke="#06d6a0" stroke-width="1.5"/>
<circle cx="231" cy="92" r="11" fill="#06d6a0"/>
<text x="231" y="97" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#0d1117" text-anchor="middle" font-weight="bold">2</text>
<text x="231" y="118" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0" text-anchor="middle" font-weight="bold">Add Pages</text>
<!-- Pages icon -->
<g transform="translate(210, 126)">
<rect x="6" y="0" width="30" height="22" rx="2" fill="none" stroke="#06d6a0" stroke-width="1" opacity="0.4"/>
<rect x="3" y="4" width="30" height="22" rx="2" fill="none" stroke="#06d6a0" stroke-width="1" opacity="0.7"/>
<rect x="0" y="8" width="30" height="22" rx="2" fill="none" stroke="#06d6a0" stroke-width="1.2"/>
</g>
<text x="231" y="175" font-family="'Courier New', Courier, monospace" font-size="9" fill="#8b949e" text-anchor="middle">report add-page</text>
<!-- Arrow -->
<line x1="290" y1="135" x2="304" y2="135" stroke="#F2C811" stroke-width="2"/>
<polygon points="308,135 302,130 302,140" fill="#F2C811"/>
<!-- Step 3: Add Visuals -->
<rect x="312" y="75" width="110" height="120" rx="8" fill="#161b22" stroke="#d97757" stroke-width="1.5"/>
<circle cx="367" cy="92" r="11" fill="#d97757"/>
<text x="367" y="97" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#0d1117" text-anchor="middle" font-weight="bold">3</text>
<text x="367" y="118" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#d97757" text-anchor="middle" font-weight="bold">Add Visuals</text>
<!-- Chart icon -->
<g transform="translate(348, 128)">
<rect x="0" y="18" width="8" height="16" rx="1" fill="#d97757" opacity="0.5"/>
<rect x="12" y="8" width="8" height="26" rx="1" fill="#d97757" opacity="0.7"/>
<rect x="24" y="0" width="8" height="34" rx="1" fill="#d97757"/>
</g>
<text x="367" y="175" font-family="'Courier New', Courier, monospace" font-size="9" fill="#8b949e" text-anchor="middle">visual add</text>
<!-- Arrow -->
<line x1="426" y1="135" x2="440" y2="135" stroke="#F2C811" stroke-width="2"/>
<polygon points="444,135 438,130 438,140" fill="#F2C811"/>
<!-- Step 4: Bind Data -->
<rect x="448" y="75" width="110" height="120" rx="8" fill="#161b22" stroke="#F2C811" stroke-width="1.5"/>
<circle cx="503" cy="92" r="11" fill="#F2C811"/>
<text x="503" y="97" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#0d1117" text-anchor="middle" font-weight="bold">4</text>
<text x="503" y="118" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#F2C811" text-anchor="middle" font-weight="bold">Bind Data</text>
<!-- Link/bind icon -->
<g transform="translate(484, 128)">
<rect x="0" y="6" width="16" height="20" rx="2" fill="none" stroke="#8b949e" stroke-width="1"/>
<line x1="20" y1="16" x2="30" y2="16" stroke="#F2C811" stroke-width="2"/>
<polygon points="32,16 28,12 28,20" fill="#F2C811"/>
<rect x="34" y="4" width="8" height="24" rx="1" fill="#d97757" opacity="0.6"/>
</g>
<text x="503" y="175" font-family="'Courier New', Courier, monospace" font-size="9" fill="#8b949e" text-anchor="middle">visual bind</text>
<!-- Arrow -->
<line x1="562" y1="135" x2="576" y2="135" stroke="#F2C811" stroke-width="2"/>
<polygon points="580,135 574,130 574,140" fill="#F2C811"/>
<!-- Step 5: Theme -->
<rect x="584" y="75" width="110" height="120" rx="8" fill="#161b22" stroke="#7b61ff" stroke-width="1.5"/>
<circle cx="639" cy="92" r="11" fill="#7b61ff"/>
<text x="639" y="97" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#0d1117" text-anchor="middle" font-weight="bold">5</text>
<text x="639" y="118" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#7b61ff" text-anchor="middle" font-weight="bold">Apply Theme</text>
<!-- Palette dots -->
<g transform="translate(616, 132)">
<circle cx="8" cy="10" r="6" fill="#0078D4"/>
<circle cx="24" cy="10" r="6" fill="#FFB900"/>
<circle cx="40" cy="10" r="6" fill="#D83B01"/>
</g>
<text x="639" y="175" font-family="'Courier New', Courier, monospace" font-size="9" fill="#8b949e" text-anchor="middle">set-theme</text>
<!-- Arrow -->
<line x1="698" y1="135" x2="712" y2="135" stroke="#F2C811" stroke-width="2"/>
<polygon points="716,135 710,130 710,140" fill="#F2C811"/>
<!-- Step 6: Validate -->
<rect x="720" y="75" width="110" height="120" rx="8" fill="#161b22" stroke="#06d6a0" stroke-width="1.5"/>
<circle cx="775" cy="92" r="11" fill="#06d6a0"/>
<text x="775" y="97" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#0d1117" text-anchor="middle" font-weight="bold">6</text>
<text x="775" y="118" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0" text-anchor="middle" font-weight="bold">Validate</text>
<!-- Checkmark icon -->
<g transform="translate(757, 128)">
<circle cx="18" cy="16" r="16" fill="none" stroke="#06d6a0" stroke-width="1.5"/>
<polyline points="8,16 15,24 28,10" fill="none" stroke="#06d6a0" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<text x="775" y="175" font-family="'Courier New', Courier, monospace" font-size="9" fill="#8b949e" text-anchor="middle">validate</text>
<!-- Bottom: full command example -->
<rect x="40" y="210" width="770" height="50" rx="8" fill="#161b22" stroke="#06d6a0" stroke-width="1" stroke-opacity="0.3"/>
<text x="425" y="233" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff" text-anchor="middle">pbi report create . --name "Sales" &amp;&amp; pbi report add-page --display-name "Overview"</text>
<text x="425" y="250" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff" text-anchor="middle">pbi visual add --type bar --page overview &amp;&amp; pbi visual bind --category --value</text>
<text x="425" y="275" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">All steps work offline on PBIR files. Desktop auto-syncs when it's open.</text>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -3,7 +3,7 @@
<!-- Title -->
<text x="425" y="32" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">Test Row-Level Security in Seconds</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Create, configure, and validate RLS roles without touching the GUI</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Create, configure, and validate RLS roles without touching the GUI</text>
<!-- Chat container -->
<rect x="40" y="65" width="770" height="360" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
@ -14,7 +14,7 @@
<circle cx="62" cy="80" r="5" fill="#ff5f57"/>
<circle cx="78" cy="80" r="5" fill="#febc2e"/>
<circle cx="94" cy="80" r="5" fill="#28c840"/>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#d97757" font-weight="bold">Claude Code</text>
<text x="116" y="84" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#d97757" font-weight="bold">Claude Code</text>
<!-- User message 1 -->
<rect x="250" y="106" width="540" height="44" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -23,13 +23,13 @@
<!-- Claude response 1 -->
<rect x="60" y="162" width="560" height="80" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">Running:</text>
<text x="130" y="182" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi security-role create "Europe Sales"</text>
<text x="78" y="200" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">Running:</text>
<text x="130" y="200" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi security-role add-filter "Europe Sales" -t Region</text>
<text x="78" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="182" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi security-role create "Europe Sales"</text>
<text x="78" y="200" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running:</text>
<text x="130" y="200" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi security-role add-filter "Europe Sales" -t Region</text>
<text x="78" y="220" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">&#x2713; Role created with filter:</text>
<text x="270" y="220" font-family="'Courier New', Courier, monospace" font-size="10" fill="#d97757">[Region][Continent] = "Europe"</text>
<text x="78" y="236" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e">Applied to Region table with DAX filter expression</text>
<text x="270" y="220" font-family="'Courier New', Courier, monospace" font-size="12" fill="#d97757">[Region][Continent] = "Europe"</text>
<text x="78" y="236" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">Applied to Region table with DAX filter expression</text>
<!-- User message 2 -->
<rect x="370" y="254" width="420" height="28" rx="12" fill="#1a3a5c" stroke="#264d73" stroke-width="1"/>
@ -37,15 +37,15 @@
<!-- Claude response 2 -->
<rect x="60" y="294" width="560" height="118" rx="12" fill="#1c1c1c" stroke="#30363d" stroke-width="1"/>
<text x="78" y="314" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e">Running as role "Europe Sales":</text>
<text x="78" y="332" font-family="'Courier New', Courier, monospace" font-size="10" fill="#58a6ff">pbi dax execute "EVALUATE ROW(\"Sales\", [Total Sales], \"Regions\", COUNTROWS(Region))"</text>
<text x="78" y="314" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e">Running as role "Europe Sales":</text>
<text x="78" y="332" font-family="'Courier New', Courier, monospace" font-size="12" fill="#58a6ff">pbi dax execute "EVALUATE ROW(\"Sales\", [Total Sales], \"Regions\", COUNTROWS(Region))"</text>
<!-- Result table -->
<line x1="78" y1="342" x2="590" y2="342" stroke="#30363d" stroke-width="1"/>
<text x="78" y="360" font-family="'Courier New', Courier, monospace" font-size="11" fill="#F2C811">| Total Sales | Regions |</text>
<text x="78" y="378" font-family="'Courier New', Courier, monospace" font-size="11" fill="#c9d1d9">| $3,842,150 | 12 |</text>
<text x="78" y="396" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#06d6a0">&#x2713; Filter working: 12 of 48 regions visible (Europe only)</text>
<text x="78" y="360" font-family="'Courier New', Courier, monospace" font-size="13" fill="#F2C811">| Total Sales | Regions |</text>
<text x="78" y="378" font-family="'Courier New', Courier, monospace" font-size="13" fill="#c9d1d9">| $3,842,150 | 12 |</text>
<text x="78" y="396" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#06d6a0">&#x2713; Filter working: 12 of 48 regions visible (Europe only)</text>
<!-- Footer -->
<text x="425" y="450" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Build and validate security roles entirely from conversation. No Security tab needed.</text>
<text x="425" y="450" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Build and validate security roles entirely from conversation. No Security tab needed.</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -1,72 +1,140 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="320" viewBox="0 0 850 320">
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="480" viewBox="0 0 850 480">
<defs>
<linearGradient id="glow-line" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#F2C811" stop-opacity="0"/>
<stop offset="50%" stop-color="#F2C811" stop-opacity="0.2"/>
<stop offset="100%" stop-color="#F2C811" stop-opacity="0"/>
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="32" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">7 Skills That Claude Discovers Automatically</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Install once, Claude uses the right skill for every task</text>
<text x="425" y="32" font-family="'Segoe UI', Arial, sans-serif" font-size="20" fill="#F2C811" text-anchor="middle" font-weight="bold">12 Skills That Claude Discovers Automatically</text>
<text x="425" y="54" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">Two layers, one CLI. Claude picks the right skill for every task.</text>
<!-- Root node: PBI-CLI -->
<rect x="350" y="70" width="150" height="40" rx="8" fill="#161b22" stroke="#F2C811" stroke-width="2"/>
<text x="425" y="95" font-family="'Segoe UI', Arial, sans-serif" font-size="15" fill="#F2C811" text-anchor="middle" font-weight="bold">pbi-cli</text>
<!-- ======================== SEMANTIC MODEL LAYER ======================== -->
<!-- Vertical trunk line -->
<line x1="425" y1="110" x2="425" y2="140" stroke="#F2C811" stroke-width="2" stroke-opacity="0.3"/>
<!-- Layer header -->
<rect x="30" y="78" width="790" height="32" rx="6" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
<text x="425" y="100" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#58a6ff" text-anchor="middle" font-weight="bold">Semantic Model Layer</text>
<text x="740" y="100" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">requires pbi connect</text>
<!-- Horizontal branch line -->
<line x1="75" y1="140" x2="775" y2="140" stroke="#F2C811" stroke-width="2" stroke-opacity="0.3"/>
<!-- Branch drops -->
<line x1="75" y1="140" x2="75" y2="160" stroke="#58a6ff" stroke-width="1.5" stroke-opacity="0.4"/>
<line x1="192" y1="140" x2="192" y2="160" stroke="#F2C811" stroke-width="1.5" stroke-opacity="0.4"/>
<line x1="308" y1="140" x2="308" y2="160" stroke="#7b61ff" stroke-width="1.5" stroke-opacity="0.4"/>
<line x1="425" y1="140" x2="425" y2="160" stroke="#06d6a0" stroke-width="1.5" stroke-opacity="0.4"/>
<line x1="542" y1="140" x2="542" y2="160" stroke="#ff6b6b" stroke-width="1.5" stroke-opacity="0.4"/>
<line x1="658" y1="140" x2="658" y2="160" stroke="#a0c4ff" stroke-width="1.5" stroke-opacity="0.4"/>
<line x1="775" y1="140" x2="775" y2="160" stroke="#ffd166" stroke-width="1.5" stroke-opacity="0.4"/>
<!-- 7 skill cards - evenly spaced across 850px -->
<!-- Card width=100, gap=15, total=7*100+6*15=790, start x=30 -->
<!-- Skill 1: DAX -->
<rect x="25" y="160" width="100" height="34" rx="8" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
<text x="75" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#58a6ff" text-anchor="middle" font-weight="bold">DAX</text>
<text x="75" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Queries &amp;</text>
<text x="75" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Calculations</text>
<rect x="30" y="125" width="100" height="44" rx="8" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
<text x="80" y="144" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#58a6ff" text-anchor="middle" font-weight="bold">DAX</text>
<text x="80" y="160" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Queries</text>
<text x="80" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Execute &amp;</text>
<text x="80" y="203" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Validate</text>
<!-- Skill 2: Modeling -->
<rect x="142" y="160" width="100" height="34" rx="8" fill="#161b22" stroke="#F2C811" stroke-width="1.5"/>
<text x="192" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#F2C811" text-anchor="middle" font-weight="bold">Modeling</text>
<text x="192" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Tables &amp;</text>
<text x="192" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Relationships</text>
<rect x="145" y="125" width="100" height="44" rx="8" fill="#161b22" stroke="#F2C811" stroke-width="1.5"/>
<text x="195" y="144" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#F2C811" text-anchor="middle" font-weight="bold">Modeling</text>
<text x="195" y="160" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Tables</text>
<text x="195" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Tables &amp;</text>
<text x="195" y="203" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Relationships</text>
<!-- Skill 3: Deployment -->
<rect x="258" y="160" width="100" height="34" rx="8" fill="#161b22" stroke="#7b61ff" stroke-width="1.5"/>
<text x="308" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#7b61ff" text-anchor="middle" font-weight="bold">Deployment</text>
<text x="308" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">TMDL &amp;</text>
<text x="308" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Transactions</text>
<rect x="260" y="125" width="100" height="44" rx="8" fill="#161b22" stroke="#7b61ff" stroke-width="1.5"/>
<text x="310" y="144" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#7b61ff" text-anchor="middle" font-weight="bold">Deployment</text>
<text x="310" y="160" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">TMDL</text>
<text x="310" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Export, Diff</text>
<text x="310" y="203" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">&amp; Transactions</text>
<!-- Skill 4: Security -->
<rect x="375" y="160" width="100" height="34" rx="8" fill="#161b22" stroke="#06d6a0" stroke-width="1.5"/>
<text x="425" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0" text-anchor="middle" font-weight="bold">Security</text>
<text x="425" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">RLS &amp;</text>
<text x="425" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Perspectives</text>
<rect x="375" y="125" width="100" height="44" rx="8" fill="#161b22" stroke="#06d6a0" stroke-width="1.5"/>
<text x="425" y="144" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#06d6a0" text-anchor="middle" font-weight="bold">Security</text>
<text x="425" y="160" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">RLS</text>
<text x="425" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Roles &amp;</text>
<text x="425" y="203" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Perspectives</text>
<!-- Skill 5: Docs -->
<rect x="492" y="160" width="100" height="34" rx="8" fill="#161b22" stroke="#ff6b6b" stroke-width="1.5"/>
<text x="542" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#ff6b6b" text-anchor="middle" font-weight="bold">Docs</text>
<text x="542" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Data</text>
<text x="542" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Dictionary</text>
<rect x="490" y="125" width="100" height="44" rx="8" fill="#161b22" stroke="#ff6b6b" stroke-width="1.5"/>
<text x="540" y="144" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#ff6b6b" text-anchor="middle" font-weight="bold">Docs</text>
<text x="540" y="160" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Dictionary</text>
<text x="540" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Data</text>
<text x="540" y="203" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Dictionary</text>
<!-- Skill 6: Partitions -->
<rect x="608" y="160" width="100" height="34" rx="8" fill="#161b22" stroke="#a0c4ff" stroke-width="1.5"/>
<text x="658" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#a0c4ff" text-anchor="middle" font-weight="bold">Partitions</text>
<text x="658" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Sources &amp;</text>
<text x="658" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">M Queries</text>
<rect x="605" y="125" width="100" height="44" rx="8" fill="#161b22" stroke="#a0c4ff" stroke-width="1.5"/>
<text x="655" y="144" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#a0c4ff" text-anchor="middle" font-weight="bold">Partitions</text>
<text x="655" y="160" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Sources</text>
<text x="655" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">M Queries</text>
<text x="655" y="203" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">&amp; Expressions</text>
<!-- Skill 7: Diagnostics -->
<rect x="725" y="160" width="100" height="34" rx="8" fill="#161b22" stroke="#ffd166" stroke-width="1.5"/>
<text x="775" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#ffd166" text-anchor="middle" font-weight="bold">Diagnostics</text>
<text x="775" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Tracing &amp;</text>
<text x="775" y="224" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Health</text>
<rect x="720" y="125" width="100" height="44" rx="8" fill="#161b22" stroke="#ffd166" stroke-width="1.5"/>
<text x="770" y="144" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#ffd166" text-anchor="middle" font-weight="bold">Diagnostics</text>
<text x="770" y="160" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Tracing</text>
<text x="770" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Tracing &amp;</text>
<text x="770" y="203" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Health Check</text>
<!-- ======================== REPORT LAYER ======================== -->
<!-- Layer header -->
<rect x="30" y="225" width="790" height="32" rx="6" fill="#161b22" stroke="#06d6a0" stroke-width="1.5"/>
<text x="425" y="247" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#06d6a0" text-anchor="middle" font-weight="bold">Report Layer</text>
<text x="680" y="247" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">no connection needed</text>
<!-- NEW badge -->
<rect x="760" y="233" width="38" height="18" rx="9" fill="#06d6a0"/>
<text x="779" y="246" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#0d1117" text-anchor="middle" font-weight="bold">NEW</text>
<!-- 5 skill cards - evenly spaced, wider cards -->
<!-- Card width=140, gap=17, total=5*140+4*17=768, start x=41 -->
<!-- Skill 8: Report -->
<rect x="41" y="272" width="140" height="44" rx="8" fill="#161b22" stroke="#06d6a0" stroke-width="1.5"/>
<text x="111" y="290" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#06d6a0" text-anchor="middle" font-weight="bold">Report</text>
<text x="111" y="306" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Scaffold &amp; Validate</text>
<text x="111" y="336" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Create, validate,</text>
<text x="111" y="349" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">preview reports</text>
<!-- Skill 9: Visuals -->
<rect x="198" y="272" width="140" height="44" rx="8" fill="#161b22" stroke="#d97757" stroke-width="1.5"/>
<text x="268" y="290" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#d97757" text-anchor="middle" font-weight="bold">Visuals</text>
<text x="268" y="306" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">32 Types</text>
<text x="268" y="336" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Add, bind, bulk-</text>
<text x="268" y="349" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">manage visuals</text>
<!-- Skill 10: Pages -->
<rect x="355" y="272" width="140" height="44" rx="8" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
<text x="425" y="290" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#58a6ff" text-anchor="middle" font-weight="bold">Pages</text>
<text x="425" y="306" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Layout &amp; Navigation</text>
<text x="425" y="336" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Pages, bookmarks,</text>
<text x="425" y="349" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">drillthrough</text>
<!-- Skill 11: Themes -->
<rect x="512" y="272" width="140" height="44" rx="8" fill="#161b22" stroke="#F2C811" stroke-width="1.5"/>
<text x="582" y="290" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#F2C811" text-anchor="middle" font-weight="bold">Themes</text>
<text x="582" y="306" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Styling</text>
<text x="582" y="336" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Branding &amp;</text>
<text x="582" y="349" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">formatting rules</text>
<!-- Skill 12: Filters -->
<rect x="669" y="272" width="140" height="44" rx="8" fill="#161b22" stroke="#ff6b6b" stroke-width="1.5"/>
<text x="739" y="290" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#ff6b6b" text-anchor="middle" font-weight="bold">Filters</text>
<text x="739" y="306" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">TopN &amp; Date</text>
<text x="739" y="336" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Page &amp; visual</text>
<text x="739" y="349" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">filters</text>
<!-- ======================== STATS BAR ======================== -->
<rect x="60" y="375" width="730" height="1" fill="url(#glow-line)"/>
<text x="130" y="412" font-family="'Segoe UI', Arial, sans-serif" font-size="30" fill="#F2C811" text-anchor="middle" font-weight="bold">27</text>
<text x="130" y="432" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">Command Groups</text>
<text x="310" y="412" font-family="'Segoe UI', Arial, sans-serif" font-size="30" fill="#58a6ff" text-anchor="middle" font-weight="bold">125+</text>
<text x="310" y="432" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">Subcommands</text>
<text x="490" y="412" font-family="'Segoe UI', Arial, sans-serif" font-size="30" fill="#06d6a0" text-anchor="middle" font-weight="bold">32</text>
<text x="490" y="432" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">Visual Types</text>
<text x="670" y="412" font-family="'Segoe UI', Arial, sans-serif" font-size="30" fill="#d97757" text-anchor="middle" font-weight="bold">12</text>
<text x="670" y="432" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">Claude Skills</text>
<!-- Footer -->
<text x="425" y="265" font-family="'Courier New', Courier, monospace" font-size="14" fill="#58a6ff" text-anchor="middle" font-weight="bold">pbi skills list</text>
<text x="425" y="290" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e" text-anchor="middle">Claude reads the skill matching your task and knows exactly which commands to use</text>
<text x="425" y="465" font-family="'Courier New', Courier, monospace" font-size="15" fill="#58a6ff" text-anchor="middle" font-weight="bold">pbi skills list</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

87
assets/stats.svg Normal file
View file

@ -0,0 +1,87 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="300" viewBox="0 0 850 300">
<!-- BACKGROUND -->
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- TITLE -->
<text x="425" y="33" font-family="'Segoe UI', Arial, sans-serif" font-size="17" fill="#F2C811" text-anchor="middle" font-weight="bold">pbi-cli at a Glance</text>
<!-- ===================== CELL 1: 30+ CLI Commands (blue) top-left ===================== -->
<rect x="18" y="48" width="387" height="110" rx="10" fill="#0d1a2a" stroke="#58a6ff" stroke-width="1.5"/>
<rect x="18" y="48" width="5" height="110" rx="2" fill="#58a6ff" fill-opacity="0.8"/>
<!-- Big number -->
<text x="211" y="103" font-family="'Segoe UI', Arial, sans-serif" font-size="58" fill="#58a6ff" text-anchor="middle" font-weight="bold">30+</text>
<!-- Label -->
<text x="211" y="126" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9" text-anchor="middle" font-weight="600">CLI Commands</text>
<!-- Description -->
<text x="211" y="143" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">across Modeling + Reporting layers</text>
<!-- Decorative accent: row of circles -->
<g opacity="0.4">
<circle cx="330" cy="68" r="4" fill="#58a6ff"/>
<circle cx="345" cy="68" r="4" fill="#58a6ff"/>
<circle cx="360" cy="68" r="4" fill="#58a6ff"/>
<circle cx="375" cy="68" r="4" fill="#58a6ff"/>
<circle cx="390" cy="68" r="4" fill="#58a6ff" fill-opacity="0.3"/>
<circle cx="394" cy="68" r="4" fill="#58a6ff" fill-opacity="0.3"/>
</g>
<!-- ===================== CELL 2: 32 Visual Types (green) top-right ===================== -->
<rect x="445" y="48" width="387" height="110" rx="10" fill="#0a1a14" stroke="#06d6a0" stroke-width="1.5"/>
<rect x="445" y="48" width="5" height="110" rx="2" fill="#06d6a0" fill-opacity="0.8"/>
<!-- Big number -->
<text x="638" y="103" font-family="'Segoe UI', Arial, sans-serif" font-size="58" fill="#06d6a0" text-anchor="middle" font-weight="bold">32</text>
<!-- Label -->
<text x="638" y="126" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9" text-anchor="middle" font-weight="600">Visual Types</text>
<!-- Description -->
<text x="638" y="143" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Power BI visuals with full data binding</text>
<!-- Decorative accent: mini bar chart -->
<rect x="772" y="70" width="8" height="20" rx="2" fill="#06d6a0" fill-opacity="0.5"/>
<rect x="783" y="62" width="8" height="28" rx="2" fill="#06d6a0" fill-opacity="0.5"/>
<rect x="794" y="58" width="8" height="32" rx="2" fill="#06d6a0" fill-opacity="0.7"/>
<!-- ===================== CELL 3: ~30 Tokens/Call (gold) bottom-left ===================== -->
<rect x="18" y="170" width="387" height="110" rx="10" fill="#1a1600" stroke="#F2C811" stroke-width="1.5"/>
<rect x="18" y="170" width="5" height="110" rx="2" fill="#F2C811" fill-opacity="0.8"/>
<!-- Big number -->
<text x="211" y="225" font-family="'Segoe UI', Arial, sans-serif" font-size="58" fill="#F2C811" text-anchor="middle" font-weight="bold">~30</text>
<!-- Label -->
<text x="211" y="248" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9" text-anchor="middle" font-weight="600">Tokens per Call</text>
<!-- Description -->
<text x="211" y="265" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">vs 4,000+ tokens with raw MCP tools</text>
<!-- Decorative accent: comparison bar -->
<rect x="310" y="183" width="80" height="8" rx="4" fill="#ff4444" fill-opacity="0.4"/>
<rect x="310" y="183" width="6" height="8" rx="4" fill="#F2C811" fill-opacity="0.9"/>
<text x="350" y="203" font-family="'Segoe UI', Arial, sans-serif" font-size="8" fill="#F2C811" fill-opacity="0.7" text-anchor="middle">133x less</text>
<!-- ===================== CELL 4: 2 Integrated Layers (coral) bottom-right ===================== -->
<rect x="445" y="170" width="387" height="110" rx="10" fill="#1a0d08" stroke="#d97757" stroke-width="1.5"/>
<rect x="445" y="170" width="5" height="110" rx="2" fill="#d97757" fill-opacity="0.8"/>
<!-- Big number -->
<text x="638" y="225" font-family="'Segoe UI', Arial, sans-serif" font-size="58" fill="#d97757" text-anchor="middle" font-weight="bold">2</text>
<!-- Label -->
<text x="638" y="248" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#c9d1d9" text-anchor="middle" font-weight="600">Integrated Layers</text>
<!-- Description -->
<text x="638" y="265" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">Modeling + Reporting, one unified CLI</text>
<!-- Decorative accent: two stacked layer bars -->
<rect x="760" y="183" width="55" height="12" rx="3" fill="#58a6ff" fill-opacity="0.5"/>
<text x="787" y="193" font-family="'Segoe UI', Arial, sans-serif" font-size="7" fill="#58a6ff" text-anchor="middle">MODELING</text>
<rect x="760" y="199" width="55" height="12" rx="3" fill="#06d6a0" fill-opacity="0.5"/>
<text x="787" y="209" font-family="'Segoe UI', Arial, sans-serif" font-size="7" fill="#06d6a0" text-anchor="middle">REPORTING</text>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

@ -1,69 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="420" viewBox="0 0 850 420">
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="32" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#F2C811" text-anchor="middle" font-weight="bold">100x More Efficient Than MCP</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Token cost per operation: MCP tool call vs CLI command</text>
<!-- Chart area -->
<rect x="40" y="65" width="770" height="300" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
<!-- Row 1: connect -->
<text x="120" y="100" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9" text-anchor="end">connect</text>
<!-- MCP bar -->
<rect x="135" y="86" width="600" height="14" rx="3" fill="#ff6b6b" fill-opacity="0.7"/>
<text x="742" y="98" font-family="'Courier New', Courier, monospace" font-size="11" fill="#ff6b6b">4,800</text>
<!-- CLI bar -->
<rect x="135" y="104" width="4" height="14" rx="2" fill="#06d6a0"/>
<text x="146" y="116" font-family="'Courier New', Courier, monospace" font-size="11" fill="#06d6a0">~30</text>
<!-- Row 2: dax query -->
<text x="120" y="155" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9" text-anchor="end">dax query</text>
<!-- MCP bar -->
<rect x="135" y="141" width="575" height="14" rx="3" fill="#ff6b6b" fill-opacity="0.7"/>
<text x="717" y="153" font-family="'Courier New', Courier, monospace" font-size="11" fill="#ff6b6b">4,600</text>
<!-- CLI bar -->
<rect x="135" y="159" width="5" height="14" rx="2" fill="#06d6a0"/>
<text x="147" y="171" font-family="'Courier New', Courier, monospace" font-size="11" fill="#06d6a0">~35</text>
<!-- Row 3: measure -->
<text x="120" y="210" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9" text-anchor="end">measure</text>
<!-- MCP bar -->
<rect x="135" y="196" width="550" height="14" rx="3" fill="#ff6b6b" fill-opacity="0.7"/>
<text x="692" y="208" font-family="'Courier New', Courier, monospace" font-size="11" fill="#ff6b6b">4,400</text>
<!-- CLI bar -->
<rect x="135" y="214" width="4" height="14" rx="2" fill="#06d6a0"/>
<text x="146" y="226" font-family="'Courier New', Courier, monospace" font-size="11" fill="#06d6a0">~28</text>
<!-- Row 4: model stats -->
<text x="120" y="265" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9" text-anchor="end">model</text>
<!-- MCP bar -->
<rect x="135" y="251" width="525" height="14" rx="3" fill="#ff6b6b" fill-opacity="0.7"/>
<text x="667" y="263" font-family="'Courier New', Courier, monospace" font-size="11" fill="#ff6b6b">4,200</text>
<!-- CLI bar -->
<rect x="135" y="269" width="4" height="14" rx="2" fill="#06d6a0"/>
<text x="146" y="281" font-family="'Courier New', Courier, monospace" font-size="11" fill="#06d6a0">~25</text>
<!-- Row 5: export -->
<text x="120" y="320" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#c9d1d9" text-anchor="end">export</text>
<!-- MCP bar -->
<rect x="135" y="306" width="563" height="14" rx="3" fill="#ff6b6b" fill-opacity="0.7"/>
<text x="705" y="318" font-family="'Courier New', Courier, monospace" font-size="11" fill="#ff6b6b">4,500</text>
<!-- CLI bar -->
<rect x="135" y="324" width="4" height="14" rx="2" fill="#06d6a0"/>
<text x="146" y="336" font-family="'Courier New', Courier, monospace" font-size="11" fill="#06d6a0">~32</text>
<!-- Legend -->
<rect x="230" y="378" width="14" height="14" rx="2" fill="#ff6b6b" fill-opacity="0.7"/>
<text x="250" y="390" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#ff6b6b">MCP tool call</text>
<rect x="380" y="378" width="14" height="14" rx="2" fill="#06d6a0"/>
<text x="400" y="390" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0">CLI command</text>
<text x="530" y="390" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#8b949e">|</text>
<text x="550" y="390" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#F2C811" font-weight="bold">~130x savings</text>
<!-- Footer -->
<text x="425" y="414" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#8b949e" text-anchor="middle">Same capabilities, fraction of the cost. Your AI budget goes 100x further.</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

245
assets/visual-types.svg Normal file
View file

@ -0,0 +1,245 @@
<svg xmlns="http://www.w3.org/2000/svg" width="850" height="440" viewBox="0 0 850 440">
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="425" y="30" font-family="'Segoe UI', Arial, sans-serif" font-size="20" fill="#F2C811" text-anchor="middle" font-weight="bold">32 Visual Types, All from CLI</text>
<text x="425" y="50" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">pbi visual add --type &lt;alias&gt; -- use friendly names, pbi-cli handles the rest</text>
<!-- Row 1: Charts (8) -->
<text x="30" y="80" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#d97757" font-weight="bold">CHARTS</text>
<!-- bar -->
<rect x="30" y="90" width="90" height="70" rx="6" fill="#161b22" stroke="#d97757" stroke-width="1"/>
<g transform="translate(45, 100)">
<rect x="0" y="22" width="12" height="18" rx="2" fill="#d97757" opacity="0.5"/>
<rect x="16" y="12" width="12" height="28" rx="2" fill="#d97757" opacity="0.7"/>
<rect x="32" y="2" width="12" height="38" rx="2" fill="#d97757"/>
</g>
<text x="75" y="152" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">bar</text>
<!-- line -->
<rect x="130" y="90" width="90" height="70" rx="6" fill="#161b22" stroke="#d97757" stroke-width="1"/>
<g transform="translate(145, 105)">
<polyline points="0,28 15,18 30,22 45,5" fill="none" stroke="#d97757" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="0" cy="28" r="3" fill="#d97757"/>
<circle cx="15" cy="18" r="3" fill="#d97757"/>
<circle cx="30" cy="22" r="3" fill="#d97757"/>
<circle cx="45" cy="5" r="3" fill="#d97757"/>
</g>
<text x="175" y="152" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">line</text>
<!-- column -->
<rect x="230" y="90" width="90" height="70" rx="6" fill="#161b22" stroke="#d97757" stroke-width="1"/>
<g transform="translate(247, 98)">
<rect x="0" y="20" width="14" height="22" rx="2" fill="#d97757" opacity="0.5"/>
<rect x="18" y="8" width="14" height="34" rx="2" fill="#d97757" opacity="0.7"/>
<rect x="36" y="0" width="14" height="42" rx="2" fill="#d97757"/>
</g>
<text x="275" y="152" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">column</text>
<!-- area -->
<rect x="330" y="90" width="90" height="70" rx="6" fill="#161b22" stroke="#d97757" stroke-width="1"/>
<g transform="translate(345, 105)">
<polygon points="0,30 15,18 30,22 50,5 50,30" fill="#d97757" fill-opacity="0.2" stroke="#d97757" stroke-width="1.5"/>
</g>
<text x="375" y="152" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">area</text>
<!-- scatter -->
<rect x="430" y="90" width="90" height="70" rx="6" fill="#161b22" stroke="#d97757" stroke-width="1"/>
<g transform="translate(448, 100)">
<circle cx="8" cy="30" r="4" fill="#d97757" opacity="0.4"/>
<circle cx="20" cy="15" r="5" fill="#d97757" opacity="0.6"/>
<circle cx="35" cy="22" r="3" fill="#d97757" opacity="0.8"/>
<circle cx="48" cy="8" r="6" fill="#d97757"/>
<circle cx="30" cy="35" r="3" fill="#d97757" opacity="0.5"/>
</g>
<text x="475" y="152" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">scatter</text>
<!-- combo -->
<rect x="530" y="90" width="90" height="70" rx="6" fill="#161b22" stroke="#d97757" stroke-width="1"/>
<g transform="translate(545, 100)">
<rect x="0" y="20" width="10" height="20" rx="1" fill="#d97757" opacity="0.5"/>
<rect x="14" y="10" width="10" height="30" rx="1" fill="#d97757" opacity="0.7"/>
<rect x="28" y="15" width="10" height="25" rx="1" fill="#d97757" opacity="0.6"/>
<rect x="42" y="5" width="10" height="35" rx="1" fill="#d97757"/>
<polyline points="5,18 19,8 33,12 47,3" fill="none" stroke="#58a6ff" stroke-width="2" stroke-linecap="round"/>
</g>
<text x="575" y="152" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">combo</text>
<!-- donut -->
<rect x="630" y="90" width="90" height="70" rx="6" fill="#161b22" stroke="#d97757" stroke-width="1"/>
<g transform="translate(660, 105)">
<circle cx="15" cy="15" r="15" fill="none" stroke="#d97757" stroke-width="6" stroke-dasharray="25 70" stroke-dashoffset="0"/>
<circle cx="15" cy="15" r="15" fill="none" stroke="#F2C811" stroke-width="6" stroke-dasharray="20 70" stroke-dashoffset="-25"/>
<circle cx="15" cy="15" r="15" fill="none" stroke="#58a6ff" stroke-width="6" stroke-dasharray="15 70" stroke-dashoffset="-45"/>
</g>
<text x="675" y="152" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">donut</text>
<!-- waterfall -->
<rect x="730" y="90" width="90" height="70" rx="6" fill="#161b22" stroke="#d97757" stroke-width="1"/>
<g transform="translate(748, 100)">
<rect x="0" y="0" width="10" height="35" rx="1" fill="#06d6a0"/>
<rect x="14" y="10" width="10" height="10" rx="1" fill="#06d6a0"/>
<rect x="28" y="15" width="10" height="8" rx="1" fill="#ff6b6b"/>
<rect x="42" y="20" width="10" height="15" rx="1" fill="#06d6a0"/>
</g>
<text x="775" y="152" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">waterfall</text>
<!-- Row 2: Cards & KPIs + Tables (6) -->
<text x="30" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#06d6a0" font-weight="bold">CARDS &amp; KPIs</text>
<!-- card -->
<rect x="30" y="192" width="90" height="70" rx="6" fill="#161b22" stroke="#06d6a0" stroke-width="1"/>
<g transform="translate(48, 202)">
<rect x="0" y="0" width="52" height="35" rx="4" fill="none" stroke="#06d6a0" stroke-width="1.2"/>
<text x="26" y="24" font-family="'Segoe UI', Arial, sans-serif" font-size="18" fill="#06d6a0" text-anchor="middle" font-weight="bold">42K</text>
</g>
<text x="75" y="254" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">card</text>
<!-- cardVisual -->
<rect x="130" y="192" width="90" height="70" rx="6" fill="#161b22" stroke="#06d6a0" stroke-width="1"/>
<g transform="translate(148, 202)">
<rect x="0" y="0" width="52" height="35" rx="4" fill="#06d6a0" fill-opacity="0.08" stroke="#06d6a0" stroke-width="1.2"/>
<text x="26" y="16" font-family="'Segoe UI', Arial, sans-serif" font-size="8" fill="#8b949e" text-anchor="middle">Revenue</text>
<text x="26" y="30" font-family="'Segoe UI', Arial, sans-serif" font-size="14" fill="#06d6a0" text-anchor="middle" font-weight="bold">$1.2M</text>
</g>
<text x="175" y="254" font-family="'Courier New', Courier, monospace" font-size="9" fill="#8b949e" text-anchor="middle">card_visual</text>
<!-- kpi -->
<rect x="230" y="192" width="90" height="70" rx="6" fill="#161b22" stroke="#06d6a0" stroke-width="1"/>
<g transform="translate(250, 204)">
<text x="24" y="14" font-family="'Segoe UI', Arial, sans-serif" font-size="16" fill="#06d6a0" text-anchor="middle" font-weight="bold">85%</text>
<polyline points="0,28 10,24 20,26 30,20 40,18 50,22" fill="none" stroke="#06d6a0" stroke-width="1.5" stroke-opacity="0.5"/>
<text x="24" y="38" font-family="'Segoe UI', Arial, sans-serif" font-size="8" fill="#8b949e" text-anchor="middle">Goal: 80%</text>
</g>
<text x="275" y="254" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">kpi</text>
<!-- gauge -->
<rect x="330" y="192" width="90" height="70" rx="6" fill="#161b22" stroke="#06d6a0" stroke-width="1"/>
<g transform="translate(355, 204)">
<path d="M 5,32 A 20,20 0 0,1 45,32" fill="none" stroke="#8b949e" stroke-width="4" stroke-linecap="round"/>
<path d="M 5,32 A 20,20 0 0,1 35,16" fill="none" stroke="#06d6a0" stroke-width="4" stroke-linecap="round"/>
<text x="25" y="38" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#06d6a0" text-anchor="middle" font-weight="bold">72%</text>
</g>
<text x="375" y="254" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">gauge</text>
<text x="440" y="182" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#58a6ff" font-weight="bold">TABLES</text>
<!-- table -->
<rect x="430" y="192" width="90" height="70" rx="6" fill="#161b22" stroke="#58a6ff" stroke-width="1"/>
<g transform="translate(442, 202)">
<rect x="0" y="0" width="66" height="38" rx="3" fill="none" stroke="#58a6ff" stroke-width="1"/>
<line x1="0" y1="10" x2="66" y2="10" stroke="#58a6ff" stroke-width="0.8"/>
<line x1="0" y1="20" x2="66" y2="20" stroke="#58a6ff" stroke-width="0.5" stroke-opacity="0.4"/>
<line x1="0" y1="30" x2="66" y2="30" stroke="#58a6ff" stroke-width="0.5" stroke-opacity="0.4"/>
<line x1="22" y1="0" x2="22" y2="38" stroke="#58a6ff" stroke-width="0.5" stroke-opacity="0.3"/>
<line x1="44" y1="0" x2="44" y2="38" stroke="#58a6ff" stroke-width="0.5" stroke-opacity="0.3"/>
</g>
<text x="475" y="254" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">table</text>
<!-- matrix -->
<rect x="530" y="192" width="90" height="70" rx="6" fill="#161b22" stroke="#58a6ff" stroke-width="1"/>
<g transform="translate(542, 202)">
<rect x="0" y="0" width="66" height="38" rx="3" fill="none" stroke="#58a6ff" stroke-width="1"/>
<rect x="0" y="0" width="18" height="10" rx="2" fill="#58a6ff" fill-opacity="0.15"/>
<line x1="0" y1="10" x2="66" y2="10" stroke="#58a6ff" stroke-width="0.8"/>
<line x1="18" y1="0" x2="18" y2="38" stroke="#58a6ff" stroke-width="0.8"/>
<line x1="0" y1="20" x2="66" y2="20" stroke="#58a6ff" stroke-width="0.5" stroke-opacity="0.4"/>
<line x1="0" y1="30" x2="66" y2="30" stroke="#58a6ff" stroke-width="0.5" stroke-opacity="0.4"/>
</g>
<text x="575" y="254" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">matrix</text>
<!-- Row 3: Slicers + Maps + Decorative -->
<text x="30" y="284" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#F2C811" font-weight="bold">SLICERS</text>
<!-- slicer -->
<rect x="30" y="294" width="90" height="70" rx="6" fill="#161b22" stroke="#F2C811" stroke-width="1"/>
<g transform="translate(48, 302)">
<rect x="0" y="0" width="52" height="40" rx="3" fill="none" stroke="#F2C811" stroke-width="1"/>
<rect x="4" y="6" width="44" height="8" rx="2" fill="#F2C811" fill-opacity="0.2"/>
<rect x="4" y="18" width="44" height="8" rx="2" fill="#F2C811" fill-opacity="0.1"/>
<rect x="4" y="30" width="44" height="8" rx="2" fill="#F2C811" fill-opacity="0.1"/>
<rect x="6" y="7" width="6" height="6" rx="1" fill="#F2C811"/>
</g>
<text x="75" y="356" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">slicer</text>
<!-- text_slicer -->
<rect x="130" y="294" width="90" height="70" rx="6" fill="#161b22" stroke="#F2C811" stroke-width="1"/>
<g transform="translate(148, 306)">
<rect x="0" y="0" width="52" height="28" rx="3" fill="none" stroke="#F2C811" stroke-width="1"/>
<text x="8" y="18" font-family="'Courier New', Courier, monospace" font-size="10" fill="#F2C811">Search...</text>
</g>
<text x="175" y="356" font-family="'Courier New', Courier, monospace" font-size="9" fill="#8b949e" text-anchor="middle">text_slicer</text>
<text x="240" y="284" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#7b61ff" font-weight="bold">MAPS</text>
<!-- azureMap -->
<rect x="230" y="294" width="90" height="70" rx="6" fill="#161b22" stroke="#7b61ff" stroke-width="1"/>
<g transform="translate(252, 302)">
<circle cx="22" cy="20" r="18" fill="none" stroke="#7b61ff" stroke-width="1.2" stroke-opacity="0.3"/>
<circle cx="14" cy="15" r="4" fill="#7b61ff" opacity="0.5"/>
<circle cx="28" cy="12" r="6" fill="#7b61ff" opacity="0.7"/>
<circle cx="20" cy="28" r="3" fill="#7b61ff" opacity="0.4"/>
<circle cx="34" cy="24" r="5" fill="#7b61ff"/>
</g>
<text x="275" y="356" font-family="'Courier New', Courier, monospace" font-size="9" fill="#8b949e" text-anchor="middle">azure_map</text>
<text x="340" y="284" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#ff6b6b" font-weight="bold">MORE CHARTS</text>
<!-- funnel -->
<rect x="330" y="294" width="90" height="70" rx="6" fill="#161b22" stroke="#ff6b6b" stroke-width="1"/>
<g transform="translate(348, 302)">
<polygon points="0,0 52,0 40,14 12,14" fill="#ff6b6b" fill-opacity="0.15" stroke="#ff6b6b" stroke-width="1"/>
<polygon points="12,16 40,16 34,28 18,28" fill="#ff6b6b" fill-opacity="0.25" stroke="#ff6b6b" stroke-width="1"/>
<polygon points="18,30 34,30 30,40 22,40" fill="#ff6b6b" fill-opacity="0.4" stroke="#ff6b6b" stroke-width="1"/>
</g>
<text x="375" y="356" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">funnel</text>
<!-- treemap -->
<rect x="430" y="294" width="90" height="70" rx="6" fill="#161b22" stroke="#ff6b6b" stroke-width="1"/>
<g transform="translate(442, 302)">
<rect x="0" y="0" width="34" height="24" rx="2" fill="#ff6b6b" opacity="0.4"/>
<rect x="36" y="0" width="30" height="12" rx="2" fill="#ff6b6b" opacity="0.6"/>
<rect x="36" y="14" width="30" height="10" rx="2" fill="#ff6b6b" opacity="0.3"/>
<rect x="0" y="26" width="20" height="16" rx="2" fill="#ff6b6b" opacity="0.7"/>
<rect x="22" y="26" width="22" height="16" rx="2" fill="#ff6b6b" opacity="0.5"/>
<rect x="46" y="26" width="20" height="16" rx="2" fill="#ff6b6b" opacity="0.2"/>
</g>
<text x="475" y="356" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">treemap</text>
<!-- ribbon -->
<rect x="530" y="294" width="90" height="70" rx="6" fill="#161b22" stroke="#ff6b6b" stroke-width="1"/>
<g transform="translate(545, 305)">
<path d="M 0,25 Q 15,20 30,22 Q 45,24 60,20" fill="#ff6b6b" fill-opacity="0.3" stroke="#ff6b6b" stroke-width="1.5"/>
<path d="M 0,18 Q 15,12 30,15 Q 45,18 60,10" fill="#d97757" fill-opacity="0.3" stroke="#d97757" stroke-width="1.5"/>
<path d="M 0,10 Q 15,5 30,8 Q 45,6 60,2" fill="#F2C811" fill-opacity="0.3" stroke="#F2C811" stroke-width="1.5"/>
</g>
<text x="575" y="356" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">ribbon</text>
<text x="640" y="284" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="#a0c4ff" font-weight="bold">DECORATIVE</text>
<!-- image -->
<rect x="630" y="294" width="90" height="70" rx="6" fill="#161b22" stroke="#a0c4ff" stroke-width="1"/>
<g transform="translate(648, 304)">
<rect x="0" y="0" width="50" height="36" rx="3" fill="none" stroke="#a0c4ff" stroke-width="1"/>
<circle cx="14" cy="12" r="5" fill="#F2C811" opacity="0.5"/>
<polygon points="8,30 22,16 34,26 42,20 50,28 50,36 0,36" fill="#a0c4ff" fill-opacity="0.2"/>
</g>
<text x="675" y="356" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">image</text>
<!-- textbox -->
<rect x="730" y="294" width="90" height="70" rx="6" fill="#161b22" stroke="#a0c4ff" stroke-width="1"/>
<g transform="translate(748, 306)">
<rect x="0" y="0" width="50" height="28" rx="3" fill="none" stroke="#a0c4ff" stroke-width="1"/>
<line x1="6" y1="9" x2="44" y2="9" stroke="#a0c4ff" stroke-width="1" stroke-opacity="0.4"/>
<line x1="6" y1="17" x2="36" y2="17" stroke="#a0c4ff" stroke-width="1" stroke-opacity="0.3"/>
<line x1="6" y1="24" x2="26" y2="24" stroke="#a0c4ff" stroke-width="1" stroke-opacity="0.2"/>
</g>
<text x="775" y="356" font-family="'Courier New', Courier, monospace" font-size="10" fill="#8b949e" text-anchor="middle">textbox</text>
<!-- Footer: +14 more -->
<text x="425" y="392" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">+ stacked_bar, clustered_bar, clustered_column, list_slicer, advanced_slicer,</text>
<text x="425" y="410" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#8b949e" text-anchor="middle">card_new, multi_row_card, shape, action_button, page_navigator, and more</text>
<text x="425" y="435" font-family="'Courier New', Courier, monospace" font-size="14" fill="#58a6ff" text-anchor="middle" font-weight="bold">pbi visual add --type bar --page overview --name my_chart</text>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,13 +1,14 @@
[build-system]
requires = ["setuptools>=68.0", "wheel"]
requires = ["setuptools>=77.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pbi-cli-tool"
version = "3.10.0"
version = "3.10.10"
description = "CLI for Power BI semantic models and PBIR reports - direct .NET connection for token-efficient AI agent usage"
readme = "README.pypi.md"
license = {text = "MIT"}
license = "MIT AND LicenseRef-Microsoft-AS-Client-Libraries"
license-files = ["LICENSE", "THIRD_PARTY_LICENSES.md", "NOTICE"]
requires-python = ">=3.10"
authors = [
{name = "pbi-cli contributors"},
@ -17,7 +18,6 @@ classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
@ -37,6 +37,7 @@ dependencies = [
[project.scripts]
pbi = "pbi_cli.main:cli"
pbi-cli = "pbi_cli.main_pbi_cli:cli"
[project.urls]
Homepage = "https://github.com/MinaSaad1/pbi-cli"
@ -58,7 +59,7 @@ where = ["src"]
[tool.setuptools.package-data]
"pbi_cli.skills" = ["**/*.md"]
"pbi_cli.dlls" = ["*.dll"]
"pbi_cli.dlls" = ["*.dll", "*.json", "README.md"]
"pbi_cli.templates" = ["**/*.json"]
[tool.ruff]
@ -94,3 +95,11 @@ strict = true
[[tool.mypy.overrides]]
module = ["pythonnet", "clr", "clr_loader"]
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = ["win32gui", "win32con", "win32api", "win32process", "win32com.*"]
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = ["websockets", "websockets.*"]
ignore_missing_imports = true

329
scripts/bake_headers.py Normal file
View file

@ -0,0 +1,329 @@
"""
Bake the Vibe BI header into each feature SVG in assets/.
For each feature SVG:
1. Inject the VIBE BI branding header (ASCII art, connection flow, install cmd)
2. Shift original content down by HEADER_HEIGHT
3. Write the updated SVG back to disk
Logos are referenced via <image> to preserve original dimensions.
"""
import re
import sys
from pathlib import Path
ASSETS_DIR = Path(__file__).parent.parent / "assets"
HEADER_HEIGHT = 210
SVG_WIDTH = 850
# Feature SVGs to process (same list as generate_images.py)
FEATURE_SVGS = [
"before-after.svg",
"architecture-flow.svg",
"backup-restore.svg",
"bulk-operations.svg",
"chat-demo.svg",
"dax-debugging.svg",
"feature-grid.svg",
"model-health-check.svg",
"rls-testing.svg",
"skills-hub.svg",
"token-cost.svg",
"how-it-works.svg",
"dax-skill.svg",
"modeling-skill.svg",
"deploy-secure.svg",
"docs-diagnostics.svg",
"cta-start.svg",
"report-layer.svg",
"dual-layer.svg",
"visual-types.svg",
"report-workflow.svg",
"auto-sync.svg",
"chat-demo-report.svg",
"commands.svg",
"layers.svg",
"release-vibe-bi.svg",
"stats.svg",
"workflow.svg",
]
# The Vibe BI header SVG fragment (210px tall, 850px wide)
HEADER_DEFS = """\
<linearGradient id="hdr-bar1" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#EBBB14"/>
<stop offset="100%" stop-color="#B25400"/>
</linearGradient>
<linearGradient id="hdr-bar2" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#F9E583"/>
<stop offset="100%" stop-color="#DE9800"/>
</linearGradient>
<linearGradient id="hdr-bar3" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#F9E68B"/>
<stop offset="100%" stop-color="#F3CD32"/>
</linearGradient>
<linearGradient id="pbi-lg1" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#EBBB14"/>
<stop offset="100%" stop-color="#B25400"/>
</linearGradient>
<linearGradient id="pbi-lg2" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#F9E583"/>
<stop offset="100%" stop-color="#DE9800"/>
</linearGradient>
<linearGradient id="pbi-lg5" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#F9E68B"/>
<stop offset="100%" stop-color="#F3CD32"/>
</linearGradient>"""
HEADER_BODY = """\
<!-- ==================== VIBE BI HEADER ==================== -->
<!-- "VIBE BI" block art (shadow) -->
<text font-family="'Courier New', Courier, monospace" font-size="9" fill="#7A6508" xml:space="preserve">
<tspan x="296" y="22"> </tspan>
<tspan x="296" y="33"> </tspan>
<tspan x="296" y="44"> </tspan>
<tspan x="296" y="55"> </tspan>
<tspan x="296" y="66"> </tspan>
<tspan x="296" y="77"> </tspan>
</text>
<!-- "VIBE BI" block art (main) -->
<text font-family="'Courier New', Courier, monospace" font-size="9" fill="#F2C811" xml:space="preserve">
<tspan x="295" y="21"> </tspan>
<tspan x="295" y="32"> </tspan>
<tspan x="295" y="43"> </tspan>
<tspan x="295" y="54"> </tspan>
<tspan x="295" y="65"> </tspan>
<tspan x="295" y="76"> </tspan>
</text>
<!-- Separator -->
<line x1="60" y1="84" x2="790" y2="84" stroke="#F2C811" stroke-opacity="0.15" stroke-width="1"/>
<!-- Tagline -->
<text x="425" y="100" font-family="'Segoe UI', Arial, sans-serif" font-size="13" fill="#e6edf3" text-anchor="middle" font-weight="600">The First CLI for Both Power BI Modeling and Reporting</text>
<!-- ===== Connection Flow: Claude > PBI-CLI > Power BI ===== -->
<!-- Claude AI logo (inline, original 1200x1200, displayed as 50x50, centered at x=110) -->
<svg x="85" y="110" width="50" height="50" viewBox="0 0 1200 1200">
<path fill="#d97757" d="M 233.959793 800.214905 L 468.644287 668.536987 L 472.590637 657.100647 L 468.644287 650.738403 L 457.208069 650.738403 L 417.986633 648.322144 L 283.892639 644.69812 L 167.597321 639.865845 L 54.926208 633.825623 L 26.577238 627.785339 L 3.3e-05 592.751709 L 2.73832 575.27533 L 26.577238 559.248352 L 60.724873 562.228149 L 136.187973 567.382629 L 249.422867 575.194763 L 331.570496 580.026978 L 453.261841 592.671082 L 472.590637 592.671082 L 475.328857 584.859009 L 468.724915 580.026978 L 463.570557 575.194763 L 346.389313 495.785217 L 219.543671 411.865906 L 153.100723 363.543762 L 117.181267 339.060425 L 99.060455 316.107361 L 91.248367 266.01355 L 123.865784 230.093994 L 167.677887 233.073853 L 178.872513 236.053772 L 223.248367 270.201477 L 318.040283 343.570496 L 441.825592 434.738342 L 459.946411 449.798706 L 467.194672 444.64447 L 468.080597 441.020203 L 459.946411 427.409485 L 392.617493 305.718323 L 320.778564 181.932983 L 288.80542 130.630859 L 280.348999 99.865845 C 277.369171 87.221436 275.194641 76.590698 275.194641 63.624268 L 312.322174 13.20813 L 332.8591 6.604126 L 382.389313 13.20813 L 403.248352 31.328979 L 434.013519 101.71814 L 483.865753 212.537048 L 561.181274 363.221497 L 583.812134 407.919434 L 595.892639 449.315491 L 600.40271 461.959839 L 608.214783 461.959839 L 608.214783 454.711609 L 614.577271 369.825623 L 626.335632 265.61084 L 637.771851 131.516846 L 641.718201 93.745117 L 660.402832 48.483276 L 697.530334 24.000122 L 726.52356 37.852417 L 750.362549 72 L 747.060486 94.067139 L 732.886047 186.201416 L 705.100708 330.52356 L 686.979919 427.167847 L 697.530334 427.167847 L 709.61084 415.087341 L 758.496704 350.174561 L 840.644348 247.490051 L 876.885925 206.738342 L 919.167847 161.71814 L 946.308838 140.29541 L 997.61084 140.29541 L 1035.38269 196.429626 L 1018.469849 254.416199 L 965.637634 321.422852 L 921.825562 378.201538 L 859.006714 462.765259 L 819.785278 530.41626 L 823.409424 535.812073 L 832.75177 534.92627 L 974.657776 504.724915 L 1051.328979 490.872559 L 1142.818848 475.167786 L 1184.214844 494.496582 L 1188.724854 514.147644 L 1172.456421 554.335693 L 1074.604126 578.496765 L 959.838989 601.449829 L 788.939636 641.879272 L 786.845764 643.409485 L 789.261841 646.389343 L 866.255127 653.637634 L 899.194702 655.409424 L 979.812134 655.409424 L 1129.932861 666.604187 L 1169.154419 692.537109 L 1192.671265 724.268677 L 1188.724854 748.429688 L 1128.322144 779.194641 L 1046.818848 759.865845 L 856.590759 714.604126 L 791.355774 698.335754 L 782.335693 698.335754 L 782.335693 703.731567 L 836.69812 756.885986 L 936.322205 846.845581 L 1061.073975 962.81897 L 1067.436279 991.490112 L 1051.409424 1014.120911 L 1034.496704 1011.704712 L 924.885986 929.234924 L 882.604126 892.107544 L 786.845764 811.48999 L 780.483276 811.48999 L 780.483276 819.946289 L 802.550415 852.241699 L 919.087341 1027.409424 L 925.127625 1081.127686 L 916.671204 1098.604126 L 886.469849 1109.154419 L 853.288696 1103.114136 L 785.073914 1007.355835 L 714.684631 899.516785 L 657.906067 802.872498 L 650.979858 806.81897 L 617.476624 1167.704834 L 601.771851 1186.147705 L 565.530212 1200 L 535.328857 1177.046997 L 519.302124 1139.919556 L 535.328857 1066.550537 L 554.657776 970.792053 L 570.362488 894.68457 L 584.536926 800.134277 L 592.993347 768.724976 L 592.429626 766.630859 L 585.503479 767.516968 L 514.22821 865.369263 L 405.825531 1011.865906 L 320.053711 1103.677979 L 299.516815 1111.812256 L 263.919525 1093.369263 L 267.221497 1060.429688 L 287.114136 1031.114136 L 405.825531 880.107361 L 477.422913 786.52356 L 523.651062 732.483276 L 523.328918 724.671265 L 520.590698 724.671265 L 205.288605 929.395935 L 149.154434 936.644409 L 124.993355 914.01355 L 127.973183 876.885986 L 139.409409 864.80542 L 234.201385 799.570435 L 233.879227 799.8927 Z"/>
</svg>
<text x="110" y="175" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#d97757" text-anchor="middle" font-weight="600">Claude Code</text>
<!-- Left arrow -->
<line x1="155" y1="135" x2="290" y2="135" stroke="#F2C811" stroke-width="2" stroke-dasharray="6,4" stroke-opacity="0.5"/>
<polygon points="294,135 286,130 286,140" fill="#F2C811" fill-opacity="0.5"/>
<!-- PBI-CLI block art (shadow) -->
<text font-family="'Courier New', Courier, monospace" font-size="7" fill="#7A6508" xml:space="preserve">
<tspan x="316" y="118"> </tspan>
<tspan x="316" y="128"> </tspan>
<tspan x="316" y="138"> </tspan>
<tspan x="316" y="148"> </tspan>
<tspan x="316" y="158"> </tspan>
<tspan x="316" y="168"> </tspan>
</text>
<!-- PBI-CLI block art (main) -->
<text font-family="'Courier New', Courier, monospace" font-size="7" fill="#F2C811" xml:space="preserve">
<tspan x="315" y="117"> </tspan>
<tspan x="315" y="127"> </tspan>
<tspan x="315" y="137"> </tspan>
<tspan x="315" y="147"> </tspan>
<tspan x="315" y="157"> </tspan>
<tspan x="315" y="167"> </tspan>
</text>
<!-- Right arrow -->
<line x1="560" y1="135" x2="655" y2="135" stroke="#F2C811" stroke-width="2" stroke-dasharray="6,4" stroke-opacity="0.5"/>
<polygon points="659,135 651,130 651,140" fill="#F2C811" fill-opacity="0.5"/>
<!-- Power BI logo (inline, original 630x630, displayed as 50x50, centered at x=700) -->
<svg x="675" y="110" width="50" height="50" viewBox="0 0 630 630">
<g transform="translate(77.5, 0)">
<rect fill="url(#pbi-lg1)" x="256" y="0" width="219" height="630" rx="26"/>
<path fill="url(#pbi-lg2)" d="M346,604 L346,630 L320,630 L153,630 C138.64,630 127,618.36 127,604 L127,183 C127,168.64 138.64,157 153,157 L320,157 C334.36,157 346,168.64 346,183 L346,604 Z"/>
<path fill="url(#pbi-lg5)" d="M219,604 L219,630 L193,630 L26,630 C11.64,630 0,618.36 0,604 L0,341 C0,326.64 11.64,315 26,315 L193,315 C207.36,315 219,326.64 219,341 L219,604 Z"/>
</g>
</svg>
<text x="700" y="175" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="#F2C811" text-anchor="middle" font-weight="600">Power BI</text>
<!-- Modeling + Reporting pills -->
<rect x="308" y="176" width="100" height="20" rx="10" fill="#58a6ff" fill-opacity="0.1" stroke="#58a6ff" stroke-width="1"/>
<text x="358" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#58a6ff" text-anchor="middle" font-weight="600">Modeling</text>
<text x="420" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#8b949e" text-anchor="middle">+</text>
<rect x="432" y="176" width="100" height="20" rx="10" fill="#06d6a0" fill-opacity="0.1" stroke="#06d6a0" stroke-width="1"/>
<text x="482" y="190" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="#06d6a0" text-anchor="middle" font-weight="600">Reporting</text>
<!-- Bottom separator -->
<line x1="60" y1="200" x2="790" y2="200" stroke="#F2C811" stroke-opacity="0.25" stroke-width="2"/>
<!-- ==================== END HEADER ==================== -->"""
def get_svg_height(svg_text: str) -> int:
"""Extract height from SVG viewBox (expects '0 0 W H' format)."""
match = re.search(r'viewBox="0 0 \d+ (\d+)"', svg_text)
if match:
return int(match.group(1))
match = re.search(r'height="(\d+)"', svg_text)
if match:
return int(match.group(1))
return 400
def extract_svg_inner(svg_text: str) -> str:
"""Extract everything between <svg ...> and </svg> tags."""
inner = re.sub(r"<\?xml[^>]*\?>\s*", "", svg_text)
inner = re.sub(r"<svg[^>]*>", "", inner, count=1)
inner = re.sub(r"</svg>\s*$", "", inner)
return inner
def extract_defs_content(svg_text: str) -> str:
"""Extract the content inside <defs>...</defs> if present."""
match = re.search(r"<defs>(.*?)</defs>", svg_text, re.DOTALL)
return match.group(1) if match else ""
def remove_bg_rect(svg_inner: str) -> str:
"""Remove the first background rect (fill='#0d1117') from the SVG inner content."""
return re.sub(
r'\s*<rect[^>]*fill="#0d1117"[^/]*/>\s*',
"\n",
svg_inner,
count=1,
)
def remove_defs_block(svg_inner: str) -> str:
"""Remove the <defs>...</defs> block from SVG inner content."""
return re.sub(r"\s*<defs>.*?</defs>\s*", "\n", svg_inner, flags=re.DOTALL)
def is_already_baked(svg_text: str) -> bool:
"""Check if the SVG already contains the Vibe BI header."""
return "VIBE BI HEADER" in svg_text
def unbake_header(svg_text: str) -> str:
"""Strip the baked header and reconstruct the original feature SVG."""
# Extract the feature content from inside <g transform="translate(0, 210)">...</g>
match = re.search(
r'<g transform="translate\(0, 210\)">\s*(.*?)\s*</g>\s*</svg>',
svg_text,
re.DOTALL,
)
if not match:
return svg_text
feature_inner = match.group(1)
original_height = get_svg_height(svg_text) - HEADER_HEIGHT
# Extract feature-specific defs (skip header gradient defs)
all_defs = extract_defs_content(svg_text)
# Remove header-specific defs (hdr-bar* and pbi-lg*)
feature_defs = re.sub(
r'\s*<linearGradient id="(hdr-bar|pbi-lg)[^"]*"[^>]*>.*?</linearGradient>',
"",
all_defs,
flags=re.DOTALL,
)
feature_defs = feature_defs.strip()
defs_block = ""
if feature_defs:
defs_block = f"\n <defs>\n{feature_defs}\n </defs>\n"
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{SVG_WIDTH}" height="{original_height}" viewBox="0 0 {SVG_WIDTH} {original_height}">
{defs_block}
<rect width="100%" height="100%" fill="#0d1117" rx="8"/>
{feature_inner}
</svg>"""
def bake_header(svg_text: str) -> str:
"""Inject the Vibe BI header into a feature SVG."""
original_height = get_svg_height(svg_text)
new_height = original_height + HEADER_HEIGHT
# Extract parts
feature_inner = extract_svg_inner(svg_text)
feature_defs = extract_defs_content(svg_text)
# Clean feature inner: remove defs block and background rect
clean_inner = remove_defs_block(feature_inner)
clean_inner = remove_bg_rect(clean_inner)
# Merge defs
all_defs = HEADER_DEFS
if feature_defs.strip():
all_defs += "\n" + feature_defs
return f"""<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{SVG_WIDTH}" height="{new_height}" viewBox="0 0 {SVG_WIDTH} {new_height}">
<defs>
{all_defs}
</defs>
<!-- Full background -->
<rect width="100%" height="100%" fill="#0d1117"/>
{HEADER_BODY}
<!-- Feature content (shifted down by {HEADER_HEIGHT}px) -->
<g transform="translate(0, {HEADER_HEIGHT})">
{clean_inner}
</g>
</svg>"""
def process_file(svg_file: str, dry_run: bool = False, force: bool = False) -> None:
"""Process a single feature SVG file."""
filepath = ASSETS_DIR / svg_file
if not filepath.exists():
print(f" SKIP {svg_file} (not found)")
return
svg_text = filepath.read_text(encoding="utf-8")
if is_already_baked(svg_text):
if force:
svg_text = unbake_header(svg_text)
print(f" STRIP {svg_file} (removed old header)")
else:
print(f" SKIP {svg_file} (already has header, use --force)")
return
original_height = get_svg_height(svg_text)
result = bake_header(svg_text)
new_height = original_height + HEADER_HEIGHT
if dry_run:
print(f" DRY {svg_file}: {original_height} -> {new_height}px")
else:
filepath.write_text(result, encoding="utf-8")
print(f" OK {svg_file}: {original_height} -> {new_height}px")
def main():
dry_run = "--dry-run" in sys.argv
force = "--force" in sys.argv
targets = sys.argv[1:]
targets = [t for t in targets if not t.startswith("--")]
if not targets:
targets = FEATURE_SVGS
mode = "DRY RUN" if dry_run else ("FORCE REBAKE" if force else "BAKING")
print(f"\n{mode}: Injecting Vibe BI header into {len(targets)} SVGs\n")
for svg_file in targets:
process_file(svg_file, dry_run=dry_run, force=force)
print(f"\nDone! Processed {len(targets)} files.")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,220 @@
"""Regenerate assets/downloads-chart.svg from pypistats.org data.
Fetches daily download counts for pbi-cli-tool (mirrors excluded), computes a
cumulative series, and writes a dark-theme SVG line chart that matches the
visual style of the other assets in this repo.
Runs with stdlib only so it works in CI without extra dependencies.
Usage:
python scripts/generate_downloads_chart.py
"""
from __future__ import annotations
import json
import sys
import urllib.request
from datetime import date, datetime
from pathlib import Path
PACKAGE = "pbi-cli-tool"
API_URL = f"https://pypistats.org/api/packages/{PACKAGE}/overall?mirrors=false"
OUTPUT_PATH = Path(__file__).resolve().parent.parent / "assets" / "downloads-chart.svg"
# Chart geometry
WIDTH = 850
HEIGHT = 340
PLOT_LEFT = 70
PLOT_RIGHT = 810
PLOT_TOP = 78
PLOT_BOTTOM = 280
# Colors (match stats.svg / banner.svg palette)
BG = "#0d1117"
ACCENT_YELLOW = "#F2C811"
LINE_BLUE = "#58a6ff"
GRID = "#21262d"
TEXT_PRIMARY = "#c9d1d9"
TEXT_SECONDARY = "#8b949e"
CARD_BG = "#0d1a2a"
def fetch_downloads() -> list[tuple[date, int]]:
"""Return sorted list of (date, daily_downloads) from pypistats."""
with urllib.request.urlopen(API_URL, timeout=30) as resp:
payload = json.loads(resp.read())
rows = [
(datetime.strptime(r["date"], "%Y-%m-%d").date(), int(r["downloads"]))
for r in payload["data"]
if r["category"] == "without_mirrors"
]
rows.sort(key=lambda item: item[0])
return rows
def to_cumulative(rows: list[tuple[date, int]]) -> list[tuple[date, int]]:
total = 0
out: list[tuple[date, int]] = []
for d, n in rows:
total += n
out.append((d, total))
return out
def nice_ceiling(value: int) -> int:
"""Round up to a nice axis maximum (1-2-5 * 10^n)."""
if value <= 0:
return 10
import math
exp = math.floor(math.log10(value))
base = 10**exp
for step in (1, 2, 2.5, 5, 10):
candidate = int(step * base)
if candidate >= value:
return candidate
return int(10 * base)
def build_svg(series: list[tuple[date, int]]) -> str:
if not series:
raise RuntimeError("No download data returned from pypistats")
n = len(series)
max_val = series[-1][1]
y_max = nice_ceiling(int(max_val * 1.15))
plot_width = PLOT_RIGHT - PLOT_LEFT
plot_height = PLOT_BOTTOM - PLOT_TOP
def x_at(i: int) -> float:
if n == 1:
return PLOT_LEFT + plot_width / 2
return PLOT_LEFT + (i / (n - 1)) * plot_width
def y_at(v: int) -> float:
return PLOT_BOTTOM - (v / y_max) * plot_height
points = [(x_at(i), y_at(v)) for i, (_, v) in enumerate(series)]
line_path = "M " + " L ".join(f"{x:.2f},{y:.2f}" for x, y in points)
area_path = (
f"M {points[0][0]:.2f},{PLOT_BOTTOM} "
+ "L "
+ " L ".join(f"{x:.2f},{y:.2f}" for x, y in points)
+ f" L {points[-1][0]:.2f},{PLOT_BOTTOM} Z"
)
# Y-axis gridlines (5 steps)
gridlines = []
y_labels = []
for step in range(6):
v = y_max * step / 5
y = PLOT_BOTTOM - (v / y_max) * plot_height
gridlines.append(
f'<line x1="{PLOT_LEFT}" y1="{y:.1f}" x2="{PLOT_RIGHT}" y2="{y:.1f}" '
f'stroke="{GRID}" stroke-width="1" stroke-dasharray="2,3"/>'
)
label = f"{int(v):,}"
y_labels.append(
f'<text x="{PLOT_LEFT - 8}" y="{y + 4:.1f}" font-family="\'Segoe UI\', Arial, sans-serif" '
f'font-size="10" fill="{TEXT_SECONDARY}" text-anchor="end">{label}</text>'
)
# X-axis labels: first, last, and ~3 intermediate
label_indices = sorted({0, n - 1, n // 4, n // 2, (3 * n) // 4})
x_labels = []
for i in label_indices:
d, _ = series[i]
x = x_at(i)
x_labels.append(
f'<text x="{x:.1f}" y="{PLOT_BOTTOM + 18}" font-family="\'Segoe UI\', Arial, sans-serif" '
f'font-size="10" fill="{TEXT_SECONDARY}" text-anchor="middle">{d.strftime("%b %d")}</text>'
)
# Data point dots + highlight on last
dots = []
for i, (x, y) in enumerate(points):
is_last = i == n - 1
r = 4 if is_last else 2.5
fill = ACCENT_YELLOW if is_last else LINE_BLUE
dots.append(f'<circle cx="{x:.2f}" cy="{y:.2f}" r="{r}" fill="{fill}"/>')
# Last-value callout
last_x, last_y = points[-1]
last_val = series[-1][1]
callout_x = min(last_x + 10, PLOT_RIGHT - 90)
callout_y = max(last_y - 28, PLOT_TOP + 14)
# Summary stats
first_date = series[0][0].strftime("%b %d, %Y")
last_date = series[-1][0].strftime("%b %d, %Y")
total_str = f"{max_val:,}"
svg = f"""<svg xmlns="http://www.w3.org/2000/svg" width="{WIDTH}" height="{HEIGHT}" viewBox="0 0 {WIDTH} {HEIGHT}">
<defs>
<linearGradient id="areaFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="{LINE_BLUE}" stop-opacity="0.45"/>
<stop offset="100%" stop-color="{LINE_BLUE}" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="{BG}" rx="8"/>
<!-- Title -->
<text x="{WIDTH // 2}" y="33" font-family="'Segoe UI', Arial, sans-serif" font-size="17" fill="{ACCENT_YELLOW}" text-anchor="middle" font-weight="bold">pbi-cli Downloads Over Time</text>
<text x="{WIDTH // 2}" y="52" font-family="'Segoe UI', Arial, sans-serif" font-size="11" fill="{TEXT_SECONDARY}" text-anchor="middle">Cumulative installs from PyPI \u2022 mirrors excluded \u2022 source: pypistats.org</text>
<!-- Gridlines & y-labels -->
{"".join(gridlines)}
{"".join(y_labels)}
<!-- Area fill -->
<path d="{area_path}" fill="url(#areaFill)"/>
<!-- Line -->
<path d="{line_path}" fill="none" stroke="{LINE_BLUE}" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
<!-- Data points -->
{"".join(dots)}
<!-- Last-value callout -->
<rect x="{callout_x:.1f}" y="{callout_y:.1f}" width="82" height="24" rx="4" fill="{CARD_BG}" stroke="{ACCENT_YELLOW}" stroke-width="1"/>
<text x="{callout_x + 41:.1f}" y="{callout_y + 16:.1f}" font-family="'Segoe UI', Arial, sans-serif" font-size="12" fill="{ACCENT_YELLOW}" text-anchor="middle" font-weight="bold">{total_str} total</text>
<!-- X-axis labels -->
{"".join(x_labels)}
<!-- Footer: date range -->
<text x="{PLOT_LEFT}" y="{HEIGHT - 12}" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="{TEXT_SECONDARY}">{first_date} \u2192 {last_date}</text>
<text x="{PLOT_RIGHT}" y="{HEIGHT - 12}" font-family="'Segoe UI', Arial, sans-serif" font-size="10" fill="{TEXT_SECONDARY}" text-anchor="end">{n} days of data</text>
</svg>
"""
return svg
def main() -> int:
try:
daily = fetch_downloads()
except Exception as exc: # noqa: BLE001
print(f"Failed to fetch pypistats data: {exc}", file=sys.stderr)
return 1
if not daily:
print("pypistats returned no rows", file=sys.stderr)
return 1
cumulative = to_cumulative(daily)
svg = build_svg(cumulative)
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH.write_text(svg, encoding="utf-8")
print(f"Wrote {OUTPUT_PATH} ({len(cumulative)} days, {cumulative[-1][1]:,} total downloads)")
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,3 +1,3 @@
"""pbi-cli: CLI for Power BI semantic models via direct .NET interop."""
__version__ = "3.10.0"
__version__ = "3.10.10"

View file

@ -14,10 +14,19 @@ if TYPE_CHECKING:
from pbi_cli.main import PbiContext
# Statuses that indicate a write operation (triggers Desktop sync)
_WRITE_STATUSES = frozenset({
"created", "deleted", "updated", "applied", "added",
"cleared", "bound", "removed", "set",
})
_WRITE_STATUSES = frozenset(
{
"created",
"deleted",
"updated",
"applied",
"added",
"cleared",
"bound",
"removed",
"set",
}
)
def run_command(
@ -53,7 +62,7 @@ def run_command(
def _is_report_write(result: Any) -> bool:
"""Check if the result indicates a report-layer write."""
"""Check if the result indicates a report-layer write that should trigger sync."""
if not isinstance(result, dict):
return False
status = result.get("status", "")
@ -65,11 +74,13 @@ def _is_report_write(result: Any) -> bool:
if click_ctx is None:
return False
# Walk up to the group to find report_path
# Walk up to the group to find report_path; also check for --no-sync flag
parent = click_ctx.parent
while parent is not None:
obj = parent.obj
if isinstance(obj, dict) and "report_path" in obj:
if obj.get("no_sync", False):
return False
return True
parent = parent.parent
return False

View file

@ -15,11 +15,18 @@ from pbi_cli.main import PbiContext, pass_context
default=None,
help="Path to .Report folder (auto-detected from CWD if omitted).",
)
@click.option(
"--no-sync",
is_flag=True,
default=False,
help="Skip Desktop auto-sync after write commands. Use for scripted multi-step builds.",
)
@click.pass_context
def bookmarks(ctx: click.Context, path: str | None) -> None:
def bookmarks(ctx: click.Context, path: str | None, no_sync: bool) -> None:
"""Manage report bookmarks."""
ctx.ensure_object(dict)
ctx.obj["report_path"] = path
ctx.obj["no_sync"] = no_sync
@bookmarks.command(name="list")

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import click
from pbi_cli.commands.skills_cmd import _get_bundled_skills, _is_installed
from pbi_cli.core.connection_store import (
ConnectionInfo,
add_connection,
@ -44,8 +45,6 @@ def connect(
If --data-source is omitted, auto-detects a running Power BI Desktop instance.
"""
_ensure_ready()
if data_source is None:
data_source = _auto_discover_data_source()
@ -76,6 +75,13 @@ def connect(
)
else:
print_success(f"Connected: {effective_name} ({data_source})")
bundled = _get_bundled_skills()
any_missing = any(not _is_installed(name) for name in bundled)
if any_missing:
print_info(
"Tip: Claude Code skills not yet installed. "
"Run 'pbi-cli skills install' to register pbi-cli skills with Claude Code."
)
except Exception as e:
print_error(f"Connection failed: {e}")
raise SystemExit(1)
@ -184,30 +190,3 @@ def _auto_discover_data_source() -> str:
data_source = f"localhost:{port}"
print_info(f"Auto-detected Power BI Desktop on {data_source}")
return data_source
def _ensure_ready() -> None:
"""Auto-install skills if not already done.
Lets users go straight from install to connect in one step:
pipx install pbi-cli-tool
pbi connect -d localhost:54321
"""
from pbi_cli.commands.skills_cmd import SKILLS_TARGET_DIR, _get_bundled_skills
from pbi_cli.core.claude_integration import ensure_claude_md_snippet
bundled = _get_bundled_skills()
any_missing = any(not (SKILLS_TARGET_DIR / name / "SKILL.md").exists() for name in bundled)
if bundled and any_missing:
print_info("Installing Claude Code skills...")
for name, source in sorted(bundled.items()):
target_dir = SKILLS_TARGET_DIR / name
if (target_dir / "SKILL.md").exists():
continue
target_dir.mkdir(parents=True, exist_ok=True)
source_file = source / "SKILL.md"
target_file = target_dir / "SKILL.md"
target_file.write_text(source_file.read_text(encoding="utf-8"), encoding="utf-8")
print_info("Skills installed.")
ensure_claude_md_snippet()

View file

@ -15,11 +15,18 @@ from pbi_cli.main import PbiContext, pass_context
default=None,
help="Path to .Report folder (auto-detected from CWD if omitted).",
)
@click.option(
"--no-sync",
is_flag=True,
default=False,
help="Skip Desktop auto-sync after write commands. Use for scripted multi-step builds.",
)
@click.pass_context
def filters(ctx: click.Context, path: str | None) -> None:
def filters(ctx: click.Context, path: str | None, no_sync: bool) -> None:
"""Manage page and visual filters."""
ctx.ensure_object(dict)
ctx.obj["report_path"] = path
ctx.obj["no_sync"] = no_sync
@filters.command(name="list")

View file

@ -17,11 +17,18 @@ from pbi_cli.main import PbiContext, pass_context
default=None,
help="Path to .Report folder (auto-detected from CWD if omitted).",
)
@click.option(
"--no-sync",
is_flag=True,
default=False,
help="Skip Desktop auto-sync after write commands. Use for scripted multi-step builds.",
)
@click.pass_context
def report(ctx: click.Context, path: str | None) -> None:
def report(ctx: click.Context, path: str | None, no_sync: bool) -> None:
"""Manage Power BI PBIR reports (pages, themes, validation)."""
ctx.ensure_object(dict)
ctx.obj["report_path"] = path
ctx.obj["no_sync"] = no_sync
@report.command()
@ -46,9 +53,7 @@ def info(ctx: PbiContext, click_ctx: click.Context) -> None:
help="Relative path to semantic model folder (e.g. ../MyModel.Dataset).",
)
@pass_context
def create(
ctx: PbiContext, target_path: str, name: str, dataset_path: str | None
) -> None:
def create(ctx: PbiContext, target_path: str, name: str, dataset_path: str | None) -> None:
"""Scaffold a new PBIR report project."""
from pbi_cli.core.report_backend import report_create
@ -168,7 +173,10 @@ def get_theme(ctx: PbiContext, click_ctx: click.Context) -> None:
@report.command(name="diff-theme")
@click.option(
"--file", "-f", required=True, type=click.Path(exists=True),
"--file",
"-f",
required=True,
type=click.Path(exists=True),
help="Proposed theme JSON file.",
)
@click.pass_context
@ -194,10 +202,18 @@ def diff_theme(ctx: PbiContext, click_ctx: click.Context, file: str) -> None:
@report.command(name="set-background")
@click.argument("page_name")
@click.option("--color", "-c", required=True, help="Hex color e.g. '#F8F9FA'.")
@click.option(
"--transparency",
"-t",
default=0,
show_default=True,
type=click.IntRange(0, 100),
help="Transparency 0 (opaque) to 100 (invisible). Defaults to 0 so the color is visible.",
)
@click.pass_context
@pass_context
def set_background(
ctx: PbiContext, click_ctx: click.Context, page_name: str, color: str
ctx: PbiContext, click_ctx: click.Context, page_name: str, color: str, transparency: int
) -> None:
"""Set the background color of a page."""
from pbi_cli.core.pbir_path import resolve_report_path
@ -211,6 +227,7 @@ def set_background(
definition_path=definition_path,
page_name=page_name,
color=color,
transparency=transparency,
)
@ -223,9 +240,7 @@ def set_background(
)
@click.pass_context
@pass_context
def set_visibility(
ctx: PbiContext, click_ctx: click.Context, page_name: str, hidden: bool
) -> None:
def set_visibility(ctx: PbiContext, click_ctx: click.Context, page_name: str, hidden: bool) -> None:
"""Hide or show a page in the report navigation."""
from pbi_cli.core.pbir_path import resolve_report_path
from pbi_cli.core.report_backend import page_set_visibility

View file

@ -88,14 +88,6 @@ def _verify(json_output: bool) -> None:
print_json({"status": "error", "errors": errors})
raise SystemExit(1)
# Install skills
try:
from pbi_cli.commands.connection import _ensure_ready
_ensure_ready()
except Exception:
pass
if json_output:
print_json({"status": "ready"})
else:

View file

@ -12,8 +12,6 @@ if TYPE_CHECKING:
import click
from pbi_cli.main import pass_context
SKILLS_TARGET_DIR = Path.home() / ".claude" / "skills"
@ -38,8 +36,7 @@ def skills() -> None:
@skills.command("list")
@pass_context
def skills_list(ctx: object) -> None:
def skills_list() -> None:
"""List available and installed skills."""
bundled = _get_bundled_skills()
if not bundled:
@ -59,23 +56,32 @@ def skills_list(ctx: object) -> None:
@skills.command("install")
@click.option("--skill", "skill_name", default=None, help="Install a specific skill.")
@click.option("--force", is_flag=True, default=False, help="Overwrite existing installations.")
@pass_context
def skills_install(ctx: object, skill_name: str | None, force: bool) -> None:
"""Install skills to ~/.claude/skills/ for Claude Code discovery."""
@click.option("--yes", "-y", is_flag=True, default=False, help="Skip confirmation prompt.")
def skills_install(skill_name: str | None, force: bool, yes: bool) -> None:
"""Install Power BI skills to ~/.claude/skills/ and register with CLAUDE.md."""
bundled = _get_bundled_skills()
if not bundled:
click.echo("No bundled skills found.", err=True)
return
to_install = (
{skill_name: bundled[skill_name]} if skill_name and skill_name in bundled else bundled
)
if skill_name and skill_name not in bundled:
raise click.ClickException(
f"Unknown skill '{skill_name}'. Available: {', '.join(sorted(bundled))}"
)
to_install = (
{skill_name: bundled[skill_name]} if skill_name and skill_name in bundled else bundled
)
if not yes:
click.echo("This command will modify your global Claude Code configuration:\n")
click.echo(f" {'~/.claude/skills/power-bi-*/':<52} copy {len(to_install)} skill file(s)")
click.echo(f" {'~/.claude/CLAUDE.md':<52} append pbi-cli skill trigger block")
click.echo("\nThis affects ALL Claude Code sessions, not just Power BI work.")
if not click.confirm("\nProceed?", default=False):
click.echo("Aborted.")
return
installed_count = 0
for name, source in sorted(to_install.items()):
target_dir = SKILLS_TARGET_DIR / name
@ -87,18 +93,21 @@ def skills_install(ctx: object, skill_name: str | None, force: bool) -> None:
source_file = source / "SKILL.md"
target_file = target_dir / "SKILL.md"
# Read from importlib resource and write to target
target_file.write_text(source_file.read_text(encoding="utf-8"), encoding="utf-8")
installed_count += 1
click.echo(f" {name}: installed", err=True)
if installed_count > 0:
from pbi_cli.core.claude_integration import ensure_claude_md_snippet
ensure_claude_md_snippet()
click.echo(f"\n{installed_count} skill(s) installed to {SKILLS_TARGET_DIR}", err=True)
@skills.command("uninstall")
@click.option("--skill", "skill_name", default=None, help="Uninstall a specific skill.")
@pass_context
def skills_uninstall(ctx: object, skill_name: str | None) -> None:
def skills_uninstall(skill_name: str | None) -> None:
"""Remove installed skills from ~/.claude/skills/."""
bundled = _get_bundled_skills()
names = [skill_name] if skill_name else sorted(bundled)

View file

@ -15,17 +15,25 @@ from pbi_cli.main import PbiContext, pass_context
default=None,
help="Path to .Report folder (auto-detected from CWD if omitted).",
)
@click.option(
"--no-sync",
is_flag=True,
default=False,
help="Skip Desktop auto-sync after write commands. Use for scripted multi-step builds.",
)
@click.pass_context
def visual(ctx: click.Context, path: str | None) -> None:
def visual(ctx: click.Context, path: str | None, no_sync: bool) -> None:
"""Manage visuals in PBIR report pages."""
ctx.ensure_object(dict)
ctx.obj["report_path"] = path
ctx.obj["no_sync"] = no_sync
def _get_report_path(click_ctx: click.Context) -> str | None:
"""Extract report_path from parent context."""
if click_ctx.parent:
return click_ctx.parent.obj.get("report_path")
result: str | None = click_ctx.parent.obj.get("report_path")
return result
return None

View file

@ -0,0 +1,68 @@
"""ASCII banner displayed on bare `pbi` invocation."""
from __future__ import annotations
import os
import sys
# Power BI yellow — closest ANSI equivalent to #FFBE00
_YELLOW = "\033[93m"
_DIM = "\033[2m"
_RESET = "\033[0m"
_ART = r"""
"""
_TAGLINE = "Power BI CLI · Direct .NET interop · Built for Claude Code"
def _color_supported() -> bool:
"""Return True if the terminal supports ANSI color codes."""
if os.environ.get("NO_COLOR"):
return False
if os.environ.get("FORCE_COLOR"):
return True
return sys.stdout.isatty()
def _can_encode(text: str) -> bool:
"""Return True if stdout can encode the given text."""
enc = getattr(sys.stdout, "encoding", "utf-8") or "utf-8"
try:
text.encode(enc)
return True
except (UnicodeEncodeError, LookupError):
return False
def print_banner(version: str) -> None:
"""Print the PBI-CLI ASCII banner with version and quick-start hints."""
use_color = _color_supported()
use_art = _can_encode(_ART)
if use_art:
art = f"{_YELLOW}{_ART}{_RESET}" if use_color else _ART
else:
# Fallback for terminals without Unicode support (e.g. legacy cmd.exe)
art = "\n PBI-CLI\n"
tagline = f" {_DIM}{_TAGLINE}{_RESET}" if use_color else f" {_TAGLINE}"
ver_line = f" {_DIM}v{version}{_RESET}" if use_color else f" v{version}"
print(art)
print(tagline)
print(ver_line)
print()
print(" Quick start:")
print(" pipx install pbi-cli-tool # already done")
print(" pbi-cli skills install # register Claude Code skills")
print(" pbi connect # auto-detect Power BI Desktop")
print()
print(" Run 'pbi --help' for the full command list.")
print()

View file

@ -34,7 +34,8 @@ SCHEMA_BOOKMARK = (
def _read_json(path: Path) -> dict[str, Any]:
"""Read and parse a JSON file."""
return json.loads(path.read_text(encoding="utf-8"))
result: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
return result
def _write_json(path: Path, data: dict[str, Any]) -> None:
@ -93,11 +94,13 @@ def bookmark_list(definition_path: Path) -> list[dict[str, Any]]:
continue
bm = _read_json(bm_file)
exploration = bm.get("explorationState", {})
results.append({
"name": name,
"display_name": bm.get("displayName", ""),
"active_section": exploration.get("activeSection"),
})
results.append(
{
"name": name,
"display_name": bm.get("displayName", ""),
"active_section": exploration.get("activeSection"),
}
)
return results

View file

@ -48,8 +48,7 @@ class VisualTypeError(PbiCliError):
def __init__(self, visual_type: str) -> None:
self.visual_type = visual_type
super().__init__(
f"Unknown visual type '{visual_type}'. "
"Run 'pbi visual types' to see supported types."
f"Unknown visual type '{visual_type}'. Run 'pbi visual types' to see supported types."
)

View file

@ -25,7 +25,8 @@ from pbi_cli.core.pbir_path import get_page_dir, get_visual_dir
def _read_json(path: Path) -> dict[str, Any]:
"""Read and parse a JSON file."""
return json.loads(path.read_text(encoding="utf-8"))
result: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
return result
def _write_json(path: Path, data: dict[str, Any]) -> None:
@ -288,9 +289,7 @@ def filter_add_topn(
"Select": [
{
"Column": {
"Expression": {
"SourceRef": {"Source": cat_alias}
},
"Expression": {"SourceRef": {"Source": cat_alias}},
"Property": column,
},
"Name": "field",
@ -333,9 +332,7 @@ def filter_add_topn(
"Expressions": [
{
"Column": {
"Expression": {
"SourceRef": {"Source": cat_alias}
},
"Expression": {"SourceRef": {"Source": cat_alias}},
"Property": column,
}
}
@ -399,9 +396,7 @@ def filter_add_relative_date(
time_unit_lower = time_unit.strip().lower()
if time_unit_lower not in _RELATIVE_DATE_TIME_UNITS:
valid = ", ".join(_RELATIVE_DATE_TIME_UNITS)
raise PbiCliError(
f"time_unit must be one of {valid}, got '{time_unit}'."
)
raise PbiCliError(f"time_unit must be one of {valid}, got '{time_unit}'.")
time_unit_code = _RELATIVE_DATE_TIME_UNITS[time_unit_lower]
days_code = _RELATIVE_DATE_TIME_UNITS["days"]

View file

@ -21,7 +21,8 @@ from pbi_cli.core.pbir_path import get_visual_dir
def _read_json(path: Path) -> dict[str, Any]:
"""Read and parse a JSON file."""
return json.loads(path.read_text(encoding="utf-8"))
result: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
return result
def _write_json(path: Path, data: dict[str, Any]) -> None:
@ -42,8 +43,7 @@ def _load_visual(definition_path: Path, page_name: str, visual_name: str) -> dic
visual_path = get_visual_dir(definition_path, page_name, visual_name) / "visual.json"
if not visual_path.exists():
raise PbiCliError(
f"Visual '{visual_name}' not found on page '{page_name}'. "
f"Expected: {visual_path}"
f"Visual '{visual_name}' not found on page '{page_name}'. Expected: {visual_path}"
)
return _read_json(visual_path)
@ -176,20 +176,10 @@ def format_background_gradient(
},
"FillRule": {
"linearGradient2": {
"min": {
"color": {
"Literal": {"Value": f"'{min_color}'"}
}
},
"max": {
"color": {
"Literal": {"Value": f"'{max_color}'"}
}
},
"min": {"color": {"Literal": {"Value": f"'{min_color}'"}}},
"max": {"color": {"Literal": {"Value": f"'{max_color}'"}}},
"nullColoringStrategy": {
"strategy": {
"Literal": {"Value": "'asZero'"}
}
"strategy": {"Literal": {"Value": "'asZero'"}}
},
}
},
@ -259,9 +249,7 @@ def format_background_conditional(
comparison_lower = comparison.strip().lower()
if comparison_lower not in _COMPARISON_KINDS:
valid = ", ".join(_COMPARISON_KINDS)
raise PbiCliError(
f"comparison must be one of {valid}, got '{comparison}'."
)
raise PbiCliError(f"comparison must be one of {valid}, got '{comparison}'.")
comparison_kind = _COMPARISON_KINDS[comparison_lower]
if field_query_ref is None:
@ -302,16 +290,10 @@ def format_background_conditional(
"Function": 0,
}
},
"Right": {
"Literal": {
"Value": threshold_literal
}
},
"Right": {"Literal": {"Value": threshold_literal}},
}
},
"Value": {
"Literal": {"Value": f"'{color_hex}'"}
},
"Value": {"Literal": {"Value": f"'{color_hex}'"}},
}
]
}
@ -373,9 +355,7 @@ def format_background_measure(
"color": {
"expr": {
"Measure": {
"Expression": {
"SourceRef": {"Entity": measure_table}
},
"Expression": {"SourceRef": {"Entity": measure_table}},
"Property": measure_property,
}
}

View file

@ -39,46 +39,48 @@ SCHEMA_BOOKMARK = (
# -- Visual type identifiers ------------------------------------------------
SUPPORTED_VISUAL_TYPES: frozenset[str] = frozenset({
# Original 9
"barChart",
"lineChart",
"card",
"pivotTable",
"tableEx",
"slicer",
"kpi",
"gauge",
"donutChart",
# v3.1.0 additions
"columnChart",
"areaChart",
"ribbonChart",
"waterfallChart",
"scatterChart",
"funnelChart",
"multiRowCard",
"treemap",
"cardNew",
"stackedBarChart",
"lineStackedColumnComboChart",
# v3.4.0 additions
"cardVisual",
"actionButton",
# v3.5.0 additions (confirmed from HR Analysis Desktop export)
"clusteredColumnChart",
"clusteredBarChart",
"textSlicer",
"listSlicer",
# v3.6.0 additions (confirmed from HR Analysis Desktop export)
"image",
"shape",
"textbox",
"pageNavigator",
"advancedSlicerVisual",
# v3.8.0 additions
"azureMap",
})
SUPPORTED_VISUAL_TYPES: frozenset[str] = frozenset(
{
# Original 9
"barChart",
"lineChart",
"card",
"pivotTable",
"tableEx",
"slicer",
"kpi",
"gauge",
"donutChart",
# v3.1.0 additions
"columnChart",
"areaChart",
"ribbonChart",
"waterfallChart",
"scatterChart",
"funnelChart",
"multiRowCard",
"treemap",
"cardNew",
"stackedBarChart",
"lineStackedColumnComboChart",
# v3.4.0 additions
"cardVisual",
"actionButton",
# v3.5.0 additions (confirmed from HR Analysis Desktop export)
"clusteredColumnChart",
"clusteredBarChart",
"textSlicer",
"listSlicer",
# v3.6.0 additions (confirmed from HR Analysis Desktop export)
"image",
"shape",
"textbox",
"pageNavigator",
"advancedSlicerVisual",
# v3.8.0 additions
"azureMap",
}
)
# Mapping from user-friendly names to PBIR visualType identifiers
VISUAL_TYPE_ALIASES: dict[str, str] = {

View file

@ -114,9 +114,7 @@ def get_visuals_dir(definition_path: Path, page_name: str) -> Path:
return visuals
def get_visual_dir(
definition_path: Path, page_name: str, visual_name: str
) -> Path:
def get_visual_dir(definition_path: Path, page_name: str, visual_name: str) -> Path:
"""Return the directory for a specific visual."""
return definition_path / "pages" / page_name / "visuals" / visual_name

View file

@ -107,11 +107,13 @@ def validate_bindings_against_model(
ref = _extract_field_ref(sel, sources)
if ref and ref not in valid_fields:
rel = f"{page_dir.name}/visuals/{vdir.name}"
findings.append(ValidationResult(
"warning",
rel,
f"Field '{ref}' not found in semantic model",
))
findings.append(
ValidationResult(
"warning",
rel,
f"Field '{ref}' not found in semantic model",
)
)
except (json.JSONDecodeError, KeyError, TypeError):
continue
@ -151,20 +153,15 @@ def _validate_report_json(definition_path: Path) -> list[ValidationResult]:
findings.append(ValidationResult("warning", "report.json", "Missing $schema reference"))
if "themeCollection" not in data:
findings.append(ValidationResult(
"error", "report.json", "Missing required 'themeCollection'"
))
findings.append(
ValidationResult("error", "report.json", "Missing required 'themeCollection'")
)
else:
tc = data["themeCollection"]
if "baseTheme" not in tc:
findings.append(ValidationResult(
"warning", "report.json", "themeCollection missing 'baseTheme'"
))
if "layoutOptimization" not in data:
findings.append(ValidationResult(
"error", "report.json", "Missing required 'layoutOptimization'"
))
findings.append(
ValidationResult("warning", "report.json", "themeCollection missing 'baseTheme'")
)
return findings
@ -201,9 +198,9 @@ def _validate_pages_metadata(definition_path: Path) -> list[ValidationResult]:
page_order = data.get("pageOrder", [])
if not isinstance(page_order, list):
findings.append(ValidationResult(
"error", "pages/pages.json", "'pageOrder' must be an array"
))
findings.append(
ValidationResult("error", "pages/pages.json", "'pageOrder' must be an array")
)
return findings
@ -234,14 +231,15 @@ def _validate_all_pages(definition_path: Path) -> list[ValidationResult]:
findings.append(ValidationResult("error", rel, f"Missing required '{req}'"))
valid_options = {
"FitToPage", "FitToWidth", "ActualSize",
"ActualSizeTopLeft", "DeprecatedDynamic",
"FitToPage",
"FitToWidth",
"ActualSize",
"ActualSizeTopLeft",
"DeprecatedDynamic",
}
opt = data.get("displayOption")
if opt and opt not in valid_options:
findings.append(ValidationResult(
"warning", rel, f"Unknown displayOption '{opt}'"
))
findings.append(ValidationResult("warning", rel, f"Unknown displayOption '{opt}'"))
if opt != "DeprecatedDynamic":
if "width" not in data:
@ -251,9 +249,9 @@ def _validate_all_pages(definition_path: Path) -> list[ValidationResult]:
name = data.get("name", "")
if name and len(name) > 50:
findings.append(ValidationResult(
"warning", rel, f"Name exceeds 50 chars: '{name[:20]}...'"
))
findings.append(
ValidationResult("warning", rel, f"Name exceeds 50 chars: '{name[:20]}...'")
)
return findings
@ -294,18 +292,22 @@ def _validate_all_visuals(definition_path: Path) -> list[ValidationResult]:
pos = data["position"]
for req in ("x", "y", "width", "height"):
if req not in pos:
findings.append(ValidationResult(
"error", rel, f"Position missing required '{req}'"
))
findings.append(
ValidationResult("error", rel, f"Position missing required '{req}'")
)
visual_config = data.get("visual", {})
vtype = visual_config.get("visualType", "")
if not vtype:
# Could be a visualGroup, which is also valid
if "visualGroup" not in data:
findings.append(ValidationResult(
"warning", rel, "Missing 'visual.visualType' (not a visual group either)"
))
findings.append(
ValidationResult(
"warning",
rel,
"Missing 'visual.visualType' (not a visual group either)",
)
)
return findings
@ -331,26 +333,28 @@ def _validate_page_order_consistency(definition_path: Path) -> list[ValidationRe
pages_dir = definition_path / "pages"
actual_pages = {
d.name
for d in pages_dir.iterdir()
if d.is_dir() and (d / "page.json").exists()
d.name for d in pages_dir.iterdir() if d.is_dir() and (d / "page.json").exists()
}
for name in page_order:
if name not in actual_pages:
findings.append(ValidationResult(
"warning",
"pages/pages.json",
f"pageOrder references '{name}' but no such page folder exists",
))
findings.append(
ValidationResult(
"warning",
"pages/pages.json",
f"pageOrder references '{name}' but no such page folder exists",
)
)
unlisted = actual_pages - set(page_order)
for name in sorted(unlisted):
findings.append(ValidationResult(
"info",
"pages/pages.json",
f"Page '{name}' exists but is not listed in pageOrder",
))
findings.append(
ValidationResult(
"info",
"pages/pages.json",
f"Page '{name}' exists but is not listed in pageOrder",
)
)
return findings
@ -381,11 +385,13 @@ def _validate_visual_name_uniqueness(definition_path: Path) -> list[ValidationRe
name = data.get("name", "")
if name in names_seen:
rel = f"pages/{page_dir.name}/visuals/{vdir.name}/visual.json"
findings.append(ValidationResult(
"error",
rel,
f"Duplicate visual name '{name}' (also in {names_seen[name]})",
))
findings.append(
ValidationResult(
"error",
rel,
f"Duplicate visual name '{name}' (also in {names_seen[name]})",
)
)
else:
names_seen[name] = vdir.name
except (json.JSONDecodeError, KeyError):
@ -418,16 +424,12 @@ def _build_result(findings: list[ValidationResult]) -> dict[str, Any]:
}
def _extract_field_ref(
select_item: dict[str, Any], sources: dict[str, str]
) -> str | None:
def _extract_field_ref(select_item: dict[str, Any], sources: dict[str, str]) -> str | None:
"""Extract a Table[Column] reference from a semantic query select item."""
for kind in ("Column", "Measure"):
if kind in select_item:
item = select_item[kind]
source_name = (
item.get("Expression", {}).get("SourceRef", {}).get("Source", "")
)
source_name = item.get("Expression", {}).get("SourceRef", {}).get("Source", "")
prop = item.get("Property", "")
table = sources.get(source_name, source_name)
if table and prop:

View file

@ -34,7 +34,8 @@ from pbi_cli.core.pbir_path import (
def _read_json(path: Path) -> dict[str, Any]:
"""Read and parse a JSON file."""
return json.loads(path.read_text(encoding="utf-8"))
result: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
return result
def _write_json(path: Path, data: dict[str, Any]) -> None:
@ -76,12 +77,14 @@ def report_info(definition_path: Path) -> dict[str, Any]:
for v in visuals_dir.iterdir()
if v.is_dir() and (v / "visual.json").exists()
)
pages.append({
"name": page_data.get("name", page_dir.name),
"display_name": page_data.get("displayName", ""),
"ordinal": page_data.get("ordinal", 0),
"visual_count": visual_count,
})
pages.append(
{
"name": page_data.get("name", page_dir.name),
"display_name": page_data.get("displayName", ""),
"ordinal": page_data.get("ordinal", 0),
"visual_count": visual_count,
}
)
theme = report_data.get("themeCollection", {}).get("baseTheme", {})
@ -115,39 +118,48 @@ def report_create(
pages_dir.mkdir(parents=True, exist_ok=True)
# version.json
_write_json(definition_dir / "version.json", {
"$schema": SCHEMA_VERSION,
"version": "2.0.0",
})
_write_json(
definition_dir / "version.json",
{
"$schema": SCHEMA_VERSION,
"version": "2.0.0",
},
)
# report.json (matches Desktop defaults)
_write_json(definition_dir / "report.json", {
"$schema": SCHEMA_REPORT,
"themeCollection": {
"baseTheme": dict(DEFAULT_BASE_THEME),
_write_json(
definition_dir / "report.json",
{
"$schema": SCHEMA_REPORT,
"themeCollection": {
"baseTheme": dict(DEFAULT_BASE_THEME),
},
"layoutOptimization": "None",
"settings": {
"useStylableVisualContainerHeader": True,
"defaultDrillFilterOtherVisuals": True,
"allowChangeFilterTypes": True,
"useEnhancedTooltips": True,
"useDefaultAggregateDisplayName": True,
},
"slowDataSourceSettings": {
"isCrossHighlightingDisabled": False,
"isSlicerSelectionsButtonEnabled": False,
"isFilterSelectionsButtonEnabled": False,
"isFieldWellButtonEnabled": False,
"isApplyAllButtonEnabled": False,
},
},
"layoutOptimization": "None",
"settings": {
"useStylableVisualContainerHeader": True,
"defaultDrillFilterOtherVisuals": True,
"allowChangeFilterTypes": True,
"useEnhancedTooltips": True,
"useDefaultAggregateDisplayName": True,
},
"slowDataSourceSettings": {
"isCrossHighlightingDisabled": False,
"isSlicerSelectionsButtonEnabled": False,
"isFilterSelectionsButtonEnabled": False,
"isFieldWellButtonEnabled": False,
"isApplyAllButtonEnabled": False,
},
})
)
# pages.json (empty page order)
_write_json(definition_dir / "pages" / "pages.json", {
"$schema": SCHEMA_PAGES_METADATA,
"pageOrder": [],
})
_write_json(
definition_dir / "pages" / "pages.json",
{
"$schema": SCHEMA_PAGES_METADATA,
"pageOrder": [],
},
)
# Scaffold a blank semantic model if no dataset path provided
if not dataset_path:
@ -155,38 +167,47 @@ def report_create(
_scaffold_blank_semantic_model(target_path, name)
# definition.pbir (datasetReference is REQUIRED by Desktop)
_write_json(report_folder / "definition.pbir", {
"version": "4.0",
"datasetReference": {
"byPath": {"path": dataset_path},
_write_json(
report_folder / "definition.pbir",
{
"version": "4.0",
"datasetReference": {
"byPath": {"path": dataset_path},
},
},
})
)
# .platform file for the report
_write_json(report_folder / ".platform", {
"$schema": (
"https://developer.microsoft.com/json-schemas/"
"fabric/gitIntegration/platformProperties/2.0.0/schema.json"
),
"metadata": {
"type": "Report",
"displayName": name,
_write_json(
report_folder / ".platform",
{
"$schema": (
"https://developer.microsoft.com/json-schemas/"
"fabric/gitIntegration/platformProperties/2.0.0/schema.json"
),
"metadata": {
"type": "Report",
"displayName": name,
},
"config": {
"version": "2.0",
"logicalId": "00000000-0000-0000-0000-000000000000",
},
},
"config": {
"version": "2.0",
"logicalId": "00000000-0000-0000-0000-000000000000",
},
})
)
# .pbip project file
_write_json(target_path / f"{name}.pbip", {
"version": "1.0",
"artifacts": [
{
"report": {"path": f"{name}.Report"},
}
],
})
_write_json(
target_path / f"{name}.pbip",
{
"version": "1.0",
"artifacts": [
{
"report": {"path": f"{name}.Report"},
}
],
},
)
return {
"status": "created",
@ -219,8 +240,6 @@ def report_validate(definition_path: Path) -> dict[str, Any]:
data = _read_json(report_json)
if "themeCollection" not in data:
errors.append("report.json missing required 'themeCollection'")
if "layoutOptimization" not in data:
errors.append("report.json missing required 'layoutOptimization'")
except json.JSONDecodeError:
pass # Already caught above
@ -236,9 +255,7 @@ def report_validate(definition_path: Path) -> dict[str, Any]:
pdata = _read_json(page_json)
for req in ("name", "displayName", "displayOption"):
if req not in pdata:
errors.append(
f"Page '{page_dir.name}' missing required '{req}'"
)
errors.append(f"Page '{page_dir.name}' missing required '{req}'")
except json.JSONDecodeError:
pass
@ -281,21 +298,21 @@ def page_list(definition_path: Path) -> list[dict[str, Any]]:
visuals_dir = page_dir / "visuals"
if visuals_dir.is_dir():
visual_count = sum(
1
for v in visuals_dir.iterdir()
if v.is_dir() and (v / "visual.json").exists()
1 for v in visuals_dir.iterdir() if v.is_dir() and (v / "visual.json").exists()
)
results.append({
"name": data.get("name", page_dir.name),
"display_name": data.get("displayName", ""),
"ordinal": data.get("ordinal", 0),
"width": data.get("width", 1280),
"height": data.get("height", 720),
"display_option": data.get("displayOption", "FitToPage"),
"visual_count": visual_count,
"is_hidden": data.get("visibility") == "HiddenInViewMode",
"page_type": data.get("type", "Default"),
})
results.append(
{
"name": data.get("name", page_dir.name),
"display_name": data.get("displayName", ""),
"ordinal": data.get("ordinal", 0),
"width": data.get("width", 1280),
"height": data.get("height", 720),
"display_option": data.get("displayOption", "FitToPage"),
"visual_count": visual_count,
"is_hidden": data.get("visibility") == "HiddenInViewMode",
"page_type": data.get("type", "Default"),
}
)
# Sort by page order if available, then by ordinal
if page_order:
@ -327,14 +344,17 @@ def page_add(
(page_dir / "visuals").mkdir()
# Write page.json (no ordinal - Desktop uses pages.json pageOrder instead)
_write_json(page_dir / "page.json", {
"$schema": SCHEMA_PAGE,
"name": page_name,
"displayName": display_name,
"displayOption": display_option,
"height": height,
"width": width,
})
_write_json(
page_dir / "page.json",
{
"$schema": SCHEMA_PAGE,
"name": page_name,
"displayName": display_name,
"displayOption": display_option,
"height": height,
"width": width,
},
)
# Update pages.json
_update_page_order(definition_path, page_name, action="add")
@ -375,9 +395,7 @@ def page_get(definition_path: Path, page_name: str) -> dict[str, Any]:
visuals_dir = page_dir / "visuals"
if visuals_dir.is_dir():
visual_count = sum(
1
for v in visuals_dir.iterdir()
if v.is_dir() and (v / "visual.json").exists()
1 for v in visuals_dir.iterdir() if v.is_dir() and (v / "visual.json").exists()
)
return {
@ -400,16 +418,21 @@ def page_set_background(
definition_path: Path,
page_name: str,
color: str,
transparency: int = 0,
) -> dict[str, Any]:
"""Set the background color of a page.
Updates the ``objects.background`` property in ``page.json``.
The color must be a hex string, e.g. ``'#F8F9FA'``.
``transparency`` is 0 (fully opaque) to 100 (fully transparent). Desktop
defaults missing transparency to 100 (invisible), so this function always
writes it explicitly. Pass a value to override.
"""
if not re.fullmatch(r"#[0-9A-Fa-f]{3,8}", color):
raise PbiCliError(
f"Invalid color '{color}' -- expected hex format like '#F8F9FA'."
)
raise PbiCliError(f"Invalid color '{color}' -- expected hex format like '#F8F9FA'.")
if not 0 <= transparency <= 100:
raise PbiCliError(f"Invalid transparency '{transparency}' -- must be 0-100.")
page_dir = get_page_dir(definition_path, page_name)
page_json_path = page_dir / "page.json"
@ -419,20 +442,18 @@ def page_set_background(
page_data = _read_json(page_json_path)
background_entry = {
"properties": {
"color": {
"solid": {
"color": {
"expr": {
"Literal": {"Value": f"'{color}'"}
}
}
}
}
"color": {"solid": {"color": {"expr": {"Literal": {"Value": f"'{color}'"}}}}},
"transparency": {"expr": {"Literal": {"Value": f"{transparency}D"}}},
}
}
objects = {**page_data.get("objects", {}), "background": [background_entry]}
_write_json(page_json_path, {**page_data, "objects": objects})
return {"status": "updated", "page": page_name, "background_color": color}
return {
"status": "updated",
"page": page_name,
"background_color": color,
"transparency": transparency,
}
def page_set_visibility(
@ -464,9 +485,7 @@ def page_set_visibility(
# ---------------------------------------------------------------------------
def theme_set(
definition_path: Path, theme_path: Path
) -> dict[str, Any]:
def theme_set(definition_path: Path, theme_path: Path) -> dict[str, Any]:
"""Apply a custom theme JSON to the report."""
if not theme_path.exists():
raise PbiCliError(f"Theme file not found: {theme_path}")
@ -489,9 +508,7 @@ def theme_set(
resources_dir = report_folder / "StaticResources" / "RegisteredResources"
resources_dir.mkdir(parents=True, exist_ok=True)
theme_dest = resources_dir / theme_path.name
theme_dest.write_text(
theme_path.read_text(encoding="utf-8"), encoding="utf-8"
)
theme_dest.write_text(theme_path.read_text(encoding="utf-8"), encoding="utf-8")
# Update resource packages in report.json
resource_packages = report_data.get("resourcePackages", [])
@ -513,15 +530,19 @@ def theme_set(
break
if not found:
resource_packages.append({
"name": "RegisteredResources",
"type": "RegisteredResources",
"items": [{
"name": theme_path.name,
"type": 202,
"path": f"BaseThemes/{theme_path.name}",
}],
})
resource_packages.append(
{
"name": "RegisteredResources",
"type": "RegisteredResources",
"items": [
{
"name": theme_path.name,
"type": 202,
"path": f"BaseThemes/{theme_path.name}",
}
],
}
)
report_data["resourcePackages"] = resource_packages
_write_json(report_json_path, report_data)
@ -670,8 +691,7 @@ def report_convert(
if report_folder is None:
raise PbiCliError(
f"No .Report folder found in '{source_path}'. "
"Expected a folder ending in .Report."
f"No .Report folder found in '{source_path}'. Expected a folder ending in .Report."
)
name = report_folder.name.replace(".Report", "")
@ -680,26 +700,22 @@ def report_convert(
# Create .pbip file
pbip_path = target / f"{name}.pbip"
if pbip_path.exists() and not force:
raise PbiCliError(
f".pbip file already exists at '{pbip_path}'. Use --force to overwrite."
)
_write_json(pbip_path, {
"version": "1.0",
"artifacts": [
{"report": {"path": f"{name}.Report"}},
],
})
raise PbiCliError(f".pbip file already exists at '{pbip_path}'. Use --force to overwrite.")
_write_json(
pbip_path,
{
"version": "1.0",
"artifacts": [
{"report": {"path": f"{name}.Report"}},
],
},
)
# Create .gitignore if not present
gitignore = target / ".gitignore"
gitignore_created = not gitignore.exists()
if gitignore_created:
gitignore_content = (
"# Power BI local settings\n"
".pbi/\n"
"*.pbix\n"
"*.bak\n"
)
gitignore_content = "# Power BI local settings\n.pbi/\n*.pbix\n*.bak\n"
gitignore.write_text(gitignore_content, encoding="utf-8")
# Validate the definition.pbir exists
@ -728,38 +744,40 @@ def _scaffold_blank_semantic_model(target_path: Path, name: str) -> None:
# model.tmdl (minimal valid TMDL)
(defn_dir / "model.tmdl").write_text(
"model Model\n"
" culture: en-US\n"
" defaultPowerBIDataSourceVersion: powerBI_V3\n",
"model Model\n culture: en-US\n defaultPowerBIDataSourceVersion: powerBI_V3\n",
encoding="utf-8",
)
# .platform file (required by Desktop)
_write_json(model_dir / ".platform", {
"$schema": (
"https://developer.microsoft.com/json-schemas/"
"fabric/gitIntegration/platformProperties/2.0.0/schema.json"
),
"metadata": {
"type": "SemanticModel",
"displayName": name,
_write_json(
model_dir / ".platform",
{
"$schema": (
"https://developer.microsoft.com/json-schemas/"
"fabric/gitIntegration/platformProperties/2.0.0/schema.json"
),
"metadata": {
"type": "SemanticModel",
"displayName": name,
},
"config": {
"version": "2.0",
"logicalId": "00000000-0000-0000-0000-000000000000",
},
},
"config": {
"version": "2.0",
"logicalId": "00000000-0000-0000-0000-000000000000",
},
})
)
# definition.pbism (matches Desktop format)
_write_json(model_dir / "definition.pbism", {
"version": "4.1",
"settings": {},
})
_write_json(
model_dir / "definition.pbism",
{
"version": "4.1",
"settings": {},
},
)
def _update_page_order(
definition_path: Path, page_name: str, action: str
) -> None:
def _update_page_order(definition_path: Path, page_name: str, action: str) -> None:
"""Update pages.json with page add/remove."""
pages_meta_path = definition_path / "pages" / "pages.json"

View file

@ -128,9 +128,7 @@ def _list_tmdl_names(tables_dir: Path) -> set[str]:
return {p.stem for p in tables_dir.glob("*.tmdl")}
def _diff_table_entities(
base_text: str, head_text: str
) -> dict[str, list[str]]:
def _diff_table_entities(base_text: str, head_text: str) -> dict[str, list[str]]:
"""Compare entity blocks within two table TMDL files."""
base_entities = _parse_table_entities(base_text)
head_entities = _parse_table_entities(head_text)
@ -226,7 +224,7 @@ def _extract_entity_name(keyword: str, declaration: str) -> str:
# e.g. "measure 'Total Revenue' = ..." -> "Total Revenue"
# e.g. "column ProductID" -> "ProductID"
# e.g. "partition Sales = m" -> "Sales"
rest = declaration[len(keyword):].strip()
rest = declaration[len(keyword) :].strip()
if rest.startswith("'"):
end = rest.find("'", 1)
return rest[1:end] if end > 0 else rest[1:]

View file

@ -26,7 +26,8 @@ from pbi_cli.core.pbir_path import get_visual_dir, get_visuals_dir
def _read_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
result: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
return result
def _write_json(path: Path, data: dict[str, Any]) -> None:
@ -87,16 +88,24 @@ VISUAL_DATA_ROLES: dict[str, list[str]] = {
}
# Roles that should default to Measure references (not Column)
MEASURE_ROLES: frozenset[str] = frozenset({
"Y", "Values", "Fields", # "Fields" is used by cardNew only
"Indicator", "Goal",
# v3.1.0 additions
"ColumnY", "LineY", "X", "Size",
# v3.4.0 additions
"Data",
# v3.8.0 additions
"MaxValue",
})
MEASURE_ROLES: frozenset[str] = frozenset(
{
"Y",
"Values",
"Fields", # "Fields" is used by cardNew only
"Indicator",
"Goal",
# v3.1.0 additions
"ColumnY",
"LineY",
"X",
"Size",
# v3.4.0 additions
"Data",
# v3.8.0 additions
"MaxValue",
}
)
# User-friendly role aliases to PBIR role names
ROLE_ALIASES: dict[str, dict[str, str]] = {
@ -127,7 +136,11 @@ ROLE_ALIASES: dict[str, dict[str, str]] = {
"ribbonChart": {"category": "Category", "value": "Y", "legend": "Legend"},
"waterfallChart": {"category": "Category", "value": "Y", "breakdown": "Breakdown"},
"scatterChart": {
"x": "X", "y": "Y", "detail": "Details", "size": "Size", "legend": "Legend",
"x": "X",
"y": "Y",
"detail": "Details",
"size": "Size",
"legend": "Legend",
"value": "Y",
},
"funnelChart": {"category": "Category", "value": "Y"},
@ -192,16 +205,16 @@ def _build_visual_json(
) -> dict[str, Any]:
"""Fill placeholders in a template string and return parsed JSON."""
filled = (
template_str
.replace("__VISUAL_NAME__", name)
.replace("__X__", str(x))
.replace("__Y__", str(y))
.replace("__WIDTH__", str(width))
.replace("__HEIGHT__", str(height))
template_str.replace("__VISUAL_NAME__", name)
.replace("__X__", str(int(x)))
.replace("__Y__", str(int(y)))
.replace("__WIDTH__", str(int(width)))
.replace("__HEIGHT__", str(int(height)))
.replace("__Z__", str(z))
.replace("__TAB_ORDER__", str(tab_order))
)
return json.loads(filled)
result: dict[str, Any] = json.loads(filled)
return result
# ---------------------------------------------------------------------------
@ -255,9 +268,7 @@ DEFAULT_SIZES: dict[str, tuple[float, float]] = {
# ---------------------------------------------------------------------------
def visual_list(
definition_path: Path, page_name: str
) -> list[dict[str, Any]]:
def visual_list(definition_path: Path, page_name: str) -> list[dict[str, Any]]:
"""List all visuals on a page."""
visuals_dir = definition_path / "pages" / page_name / "visuals"
if not visuals_dir.is_dir():
@ -274,33 +285,35 @@ def visual_list(
# Group container: has "visualGroup" key instead of "visual"
if "visualGroup" in data and "visual" not in data:
results.append({
"name": data.get("name", vdir.name),
"visual_type": "group",
"x": 0,
"y": 0,
"width": 0,
"height": 0,
})
results.append(
{
"name": data.get("name", vdir.name),
"visual_type": "group",
"x": 0,
"y": 0,
"width": 0,
"height": 0,
}
)
continue
pos = data.get("position", {})
visual_config = data.get("visual", {})
results.append({
"name": data.get("name", vdir.name),
"visual_type": visual_config.get("visualType", "unknown"),
"x": pos.get("x", 0),
"y": pos.get("y", 0),
"width": pos.get("width", 0),
"height": pos.get("height", 0),
})
results.append(
{
"name": data.get("name", vdir.name),
"visual_type": visual_config.get("visualType", "unknown"),
"x": pos.get("x", 0),
"y": pos.get("y", 0),
"width": pos.get("width", 0),
"height": pos.get("height", 0),
}
)
return results
def visual_get(
definition_path: Path, page_name: str, visual_name: str
) -> dict[str, Any]:
def visual_get(definition_path: Path, page_name: str, visual_name: str) -> dict[str, Any]:
"""Get detailed information about a visual."""
visual_dir = get_visual_dir(definition_path, page_name, visual_name)
vfile = visual_dir / "visual.json"
@ -320,11 +333,13 @@ def visual_get(
for proj in projections:
field = proj.get("field", {})
query_ref = proj.get("queryRef", "")
bindings.append({
"role": role,
"query_ref": query_ref,
"field": _summarize_field(field),
})
bindings.append(
{
"role": role,
"query_ref": query_ref,
"field": _summarize_field(field),
}
)
return {
"name": data.get("name", visual_name),
@ -465,16 +480,12 @@ def visual_set_container(
visual_dir = get_visual_dir(definition_path, page_name, visual_name)
visual_json_path = visual_dir / "visual.json"
if not visual_json_path.exists():
raise PbiCliError(
f"Visual '{visual_name}' not found on page '{page_name}'."
)
raise PbiCliError(f"Visual '{visual_name}' not found on page '{page_name}'.")
data = _read_json(visual_json_path)
visual = data.get("visual")
if visual is None:
raise PbiCliError(
f"Visual '{visual_name}' has invalid JSON -- missing 'visual' key."
)
raise PbiCliError(f"Visual '{visual_name}' has invalid JSON -- missing 'visual' key.")
if border_show is None and background_show is None and title is None:
return {
@ -489,26 +500,17 @@ def visual_set_container(
vco: dict[str, Any] = dict(visual.get("visualContainerObjects", {}))
def _bool_entry(value: bool) -> list[dict[str, Any]]:
return [{
"properties": {
"show": {
"expr": {"Literal": {"Value": str(value).lower()}}
}
}
}]
return [{"properties": {"show": {"expr": {"Literal": {"Value": str(value).lower()}}}}}]
if border_show is not None:
vco = {**vco, "border": _bool_entry(border_show)}
if background_show is not None:
vco = {**vco, "background": _bool_entry(background_show)}
if title is not None:
vco = {**vco, "title": [{
"properties": {
"text": {
"expr": {"Literal": {"Value": f"'{title}'"}}
}
}
}]}
vco = {
**vco,
"title": [{"properties": {"text": {"expr": {"Literal": {"Value": f"'{title}'"}}}}}],
}
updated_visual = {**visual, "visualContainerObjects": vco}
_write_json(visual_json_path, {**data, "visual": updated_visual})
@ -523,9 +525,7 @@ def visual_set_container(
}
def visual_delete(
definition_path: Path, page_name: str, visual_name: str
) -> dict[str, Any]:
def visual_delete(definition_path: Path, page_name: str, visual_name: str) -> dict[str, Any]:
"""Delete a visual from a page."""
visual_dir = get_visual_dir(definition_path, page_name, visual_name)
@ -566,11 +566,6 @@ def visual_bind(
query = visual_config.setdefault("query", {})
query_state = query.setdefault("queryState", {})
# Collect existing Commands From/Select to merge (fix: don't overwrite)
from_entities: dict[str, dict[str, Any]] = {}
select_items: list[dict[str, Any]] = []
_collect_existing_commands(query, from_entities, select_items)
role_map = ROLE_ALIASES.get(visual_type, {})
applied: list[dict[str, str]] = []
@ -588,14 +583,6 @@ def visual_bind(
# Determine measure vs column: explicit flag, or role-based heuristic
is_measure = force_measure or pbir_role in MEASURE_ROLES
# Track source alias for Commands block (use full name to avoid collisions)
source_alias = table.replace(" ", "_").lower() if table else "t"
from_entities[source_alias] = {
"Name": source_alias,
"Entity": table,
"Type": 0,
}
# Build queryState projection (uses Entity directly, matching Desktop)
query_ref = f"{table}.{column}"
if is_measure:
@ -613,53 +600,25 @@ def visual_bind(
}
}
projection = {
projection: dict[str, Any] = {
"field": field_expr,
"queryRef": query_ref,
"nativeQueryRef": column,
}
if not is_measure:
projection["active"] = True
# Add to query state
role_state = query_state.setdefault(pbir_role, {"projections": []})
role_state["projections"].append(projection)
# Build Commands select item (uses Source alias)
if is_measure:
cmd_field_expr: dict[str, Any] = {
"Measure": {
"Expression": {"SourceRef": {"Source": source_alias}},
"Property": column,
}
applied.append(
{
"role": pbir_role,
"field": field_ref,
"query_ref": query_ref,
}
else:
cmd_field_expr = {
"Column": {
"Expression": {"SourceRef": {"Source": source_alias}},
"Property": column,
}
}
select_items.append({
**cmd_field_expr,
"Name": query_ref,
})
applied.append({
"role": pbir_role,
"field": field_ref,
"query_ref": query_ref,
})
# Set the semantic query Commands block (merges with existing)
if from_entities and select_items:
query["Commands"] = [{
"SemanticQueryDataShapeCommand": {
"Query": {
"Version": 2,
"From": list(from_entities.values()),
"Select": select_items,
}
}
}]
)
data["visual"] = visual_config
_write_json(vfile, data)
@ -690,9 +649,7 @@ def _parse_field_ref(ref: str) -> tuple[str, str]:
column = match.group(2).strip()
return table, column
raise PbiCliError(
f"Invalid field reference '{ref}'. Expected 'Table[Column]' format."
)
raise PbiCliError(f"Invalid field reference '{ref}'. Expected 'Table[Column]' format.")
def _summarize_field(field: dict[str, Any]) -> str:
@ -710,21 +667,6 @@ def _summarize_field(field: dict[str, Any]) -> str:
return str(field)
def _collect_existing_commands(
query: dict[str, Any],
from_entities: dict[str, dict[str, Any]],
select_items: list[dict[str, Any]],
) -> None:
"""Extract existing From entities and Select items from Commands block."""
for cmd in query.get("Commands", []):
sq = cmd.get("SemanticQueryDataShapeCommand", {}).get("Query", {})
for entity in sq.get("From", []):
name = entity.get("Name", "")
if name:
from_entities[name] = entity
select_items.extend(sq.get("Select", []))
def _next_y_position(definition_path: Path, page_name: str) -> float:
"""Calculate the next y position to avoid overlap with existing visuals."""
visuals_dir = definition_path / "pages" / page_name / "visuals"
@ -878,12 +820,14 @@ def visual_calc_list(
for proj in state.get("projections", []):
nvc = proj.get("field", {}).get("NativeVisualCalculation")
if nvc is not None:
results.append({
"name": nvc.get("Name", ""),
"expression": nvc.get("Expression", ""),
"role": role,
"query_ref": proj.get("queryRef", "select"),
})
results.append(
{
"name": nvc.get("Name", ""),
"expression": nvc.get("Expression", ""),
"role": role,
"query_ref": proj.get("queryRef", "select"),
}
)
return results
@ -905,15 +849,14 @@ def visual_calc_delete(
raise PbiCliError(f"Visual '{visual_name}' not found on page '{page_name}'.")
data = _read_json(vfile)
query_state = (
data.get("visual", {}).get("query", {}).get("queryState", {})
)
query_state = data.get("visual", {}).get("query", {}).get("queryState", {})
found = False
for role, state in query_state.items():
projections: list[dict[str, Any]] = state.get("projections", [])
new_projections = [
proj for proj in projections
proj
for proj in projections
if proj.get("field", {}).get("NativeVisualCalculation", {}).get("Name") != calc_name
]
if len(new_projections) < len(projections):
@ -921,9 +864,7 @@ def visual_calc_delete(
found = True
if not found:
raise PbiCliError(
f"Visual calculation '{calc_name}' not found in visual '{visual_name}'."
)
raise PbiCliError(f"Visual calculation '{calc_name}' not found in visual '{visual_name}'.")
_write_json(vfile, data)
return {"status": "deleted", "visual": visual_name, "name": calc_name}

View file

@ -0,0 +1,25 @@
# Bundled Microsoft Analysis Services Client Libraries
The `.dll` files in this directory are **not** covered by the MIT License
that applies to the rest of pbi-cli. They are unmodified Microsoft
Corporation binaries redistributed under the Microsoft Software License
Terms for Microsoft Analysis Management Objects (AMO) and Microsoft
Analysis Services - ADOMD.NET.
See the top-level [`THIRD_PARTY_LICENSES.md`](../../../THIRD_PARTY_LICENSES.md)
and [`NOTICE`](../../../NOTICE) files for the full license text and
attribution details.
Bundled files:
- `Microsoft.AnalysisServices.dll`
- `Microsoft.AnalysisServices.Core.dll`
- `Microsoft.AnalysisServices.Tabular.dll`
- `Microsoft.AnalysisServices.Tabular.Json.dll`
- `Microsoft.AnalysisServices.AdomdClient.dll`
Sourced from the official Microsoft NuGet packages
`Microsoft.AnalysisServices.NetCore.retail.amd64` and
`Microsoft.AnalysisServices.AdomdClient.retail.amd64`.
Copyright (c) Microsoft Corporation. All rights reserved.

View file

@ -24,7 +24,7 @@ class PbiContext:
pass_context = click.make_pass_decorator(PbiContext, ensure=True)
@click.group()
@click.group(invoke_without_command=True)
@click.option(
"--json",
"json_output",
@ -48,6 +48,12 @@ def cli(ctx: click.Context, json_output: bool, connection: str | None) -> None:
ctx.ensure_object(PbiContext)
ctx.obj = PbiContext(json_output=json_output, connection=connection)
if ctx.invoked_subcommand is None and not json_output:
from pbi_cli.core.banner import print_banner
print_banner(__version__)
click.echo(ctx.get_help())
def _register_commands() -> None:
"""Lazily import and register all command groups."""
@ -72,7 +78,6 @@ def _register_commands() -> None:
from pbi_cli.commands.report import report
from pbi_cli.commands.security import security_role
from pbi_cli.commands.setup_cmd import setup
from pbi_cli.commands.skills_cmd import skills
from pbi_cli.commands.table import table
from pbi_cli.commands.trace import trace
from pbi_cli.commands.transaction import transaction
@ -100,7 +105,6 @@ def _register_commands() -> None:
cli.add_command(transaction)
cli.add_command(advanced)
cli.add_command(repl)
cli.add_command(skills)
cli.add_command(report)
cli.add_command(visual)
cli.add_command(filters)

View file

@ -0,0 +1,32 @@
"""Entry point for the pbi-cli management command."""
from __future__ import annotations
import click
from pbi_cli import __version__
@click.group()
@click.version_option(version=__version__, prog_name="pbi-cli")
def cli() -> None:
"""pbi-cli: CLI management tool for pbi-cli-tool.
Use this command to set up Claude Code integration before using 'pbi'.
Typical first-time setup:
\b
pipx install pbi-cli-tool
pbi-cli skills install
pbi connect
"""
def _register_commands() -> None:
from pbi_cli.commands.skills_cmd import skills
cli.add_command(skills)
_register_commands()

View file

@ -29,12 +29,12 @@ def render_report(definition_path: Path) -> str:
for page_dir in page_dirs:
pages_html.append(_render_page(page_dir))
pages_content = "\n".join(pages_html) if pages_html else "<p class='empty'>No pages in report</p>"
return _HTML_TEMPLATE.replace("{{THEME}}", escape(theme)).replace(
"{{PAGES}}", pages_content
pages_content = (
"\n".join(pages_html) if pages_html else "<p class='empty'>No pages in report</p>"
)
return _HTML_TEMPLATE.replace("{{THEME}}", escape(theme)).replace("{{PAGES}}", pages_content)
def render_page(definition_path: Path, page_name: str) -> str:
"""Render a single page as HTML."""
@ -218,7 +218,7 @@ def _render_visual_content(vtype: str, w: float, h: float, bindings: list[str])
for i in range(num_bars):
bar_h = body_h * (0.3 + 0.5 * ((i * 37 + 13) % 7) / 7)
bars += (
f'<rect x="{i * bar_w * 2 + bar_w/2}" y="{body_h - bar_h}" '
f'<rect x="{i * bar_w * 2 + bar_w / 2}" y="{body_h - bar_h}" '
f'width="{bar_w}" height="{bar_h}" fill="#4472C4" opacity="0.7"/>'
)
return f'<svg class="chart-svg" viewBox="0 0 {w} {body_h}">{bars}</svg>'
@ -230,7 +230,9 @@ def _render_visual_content(vtype: str, w: float, h: float, bindings: list[str])
px = (w / (num_points - 1)) * i
py = body_h * (0.2 + 0.6 * ((i * 47 + 23) % 11) / 11)
points.append(f"{px},{py}")
polyline = f'<polyline points="{" ".join(points)}" fill="none" stroke="#ED7D31" stroke-width="3"/>'
polyline = (
f'<polyline points="{" ".join(points)}" fill="none" stroke="#ED7D31" stroke-width="3"/>'
)
return f'<svg class="chart-svg" viewBox="0 0 {w} {body_h}">{polyline}</svg>'
if vtype == "card":
@ -286,13 +288,15 @@ def _get_page_order(definition_path: Path) -> list[str]:
return []
try:
data = json.loads(pages_json.read_text(encoding="utf-8"))
return data.get("pageOrder", [])
order: list[str] = data.get("pageOrder", [])
return order
except (json.JSONDecodeError, KeyError):
return []
def _read_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
result: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
return result
# ---------------------------------------------------------------------------

View file

@ -27,7 +27,7 @@ def start_preview_server(
"""
# Check for websockets dependency
try:
import websockets # type: ignore[import-untyped]
import websockets
except ImportError:
return {
"status": "error",

View file

@ -12,7 +12,8 @@ Execute and validate DAX queries against connected Power BI models.
```bash
pipx install pbi-cli-tool
pbi connect # Auto-detects Power BI Desktop and installs skills
pbi-cli skills install
pbi connect
```
## Executing Queries

View file

@ -12,7 +12,8 @@ Manage model lifecycle with TMDL export/import, transactions, and version contro
```bash
pipx install pbi-cli-tool
pbi connect # Auto-detects Power BI Desktop and installs skills
pbi-cli skills install
pbi connect
```
## Connecting to Targets

View file

@ -12,7 +12,8 @@ Troubleshoot performance, trace queries, and verify the pbi-cli environment.
```bash
pipx install pbi-cli-tool
pbi connect # Auto-detects Power BI Desktop and installs skills
pbi-cli skills install
pbi connect
```
## Environment Check

View file

@ -12,7 +12,8 @@ Generate comprehensive documentation for Power BI semantic models.
```bash
pipx install pbi-cli-tool
pbi connect # Auto-detects Power BI Desktop and installs skills
pbi-cli skills install
pbi connect
```
## Quick Model Overview

View file

@ -130,6 +130,24 @@ pbi filters add-topn --page overview \
pbi filters list --page overview
```
## Suppressing Auto-Sync (--no-sync)
By default, every write command automatically syncs Power BI Desktop. When
applying filters to multiple pages or visuals in sequence, Desktop reloads
after each command.
Use `--no-sync` on the `filters` command group to batch all filter changes,
then call `pbi report reload` once at the end:
```bash
# Suppress sync while applying filters
pbi filters --no-sync add-categorical --page overview --table "Calendar Lookup" --column "Year" --values "2024"
pbi filters --no-sync add-categorical --page details --table "Product Lookup" --column "Category" --values "Bikes"
# Single reload when all filters are done
pbi report reload
```
## JSON Output
```bash

View file

@ -6,13 +6,14 @@ tools: pbi-cli
# Power BI Modeling Skill
Use pbi-cli to manage semantic model structure. Requires `pipx install pbi-cli-tool` and `pbi connect`.
Use pbi-cli to manage semantic model structure. Requires `pipx install pbi-cli-tool`, `pbi-cli skills install`, and `pbi connect`.
## Prerequisites
```bash
pipx install pbi-cli-tool
pbi connect # Auto-detects Power BI Desktop and installs skills
pbi-cli skills install
pbi connect
```
## Tables

View file

@ -142,6 +142,25 @@ Page commands inherit the report path from the parent `pbi report` group:
2. Auto-detect: walks up from CWD looking for `*.Report/definition/`
3. From `.pbip`: finds sibling `.Report` folder from `.pbip` file
## Suppressing Auto-Sync (--no-sync)
By default, every write command automatically syncs Power BI Desktop. When
setting up multiple pages in sequence, Desktop reloads after each one.
Use `--no-sync` on the `report` command group to batch all page changes, then
call `pbi report reload` once at the end:
```bash
# Suppress sync while setting up pages
pbi report --no-sync add-page --display-name "Overview" --name overview
pbi report --no-sync add-page --display-name "Details" --name details
pbi report --no-sync set-background overview --color "#F2F2F2"
pbi report --no-sync set-background details --color "#F2F2F2"
# Single reload when all page setup is done
pbi report reload
```
## JSON Output
```bash

View file

@ -12,7 +12,8 @@ Manage table partitions, named expressions (M queries), and calendar tables.
```bash
pipx install pbi-cli-tool
pbi connect # Auto-detects Power BI Desktop and installs skills
pbi-cli skills install
pbi connect
```
## Partitions

View file

@ -94,6 +94,30 @@ Power BI Desktop's Developer Mode auto-detects TMDL changes but not PBIR
changes. This command sends a keyboard shortcut to the Desktop window to
trigger a reload. Requires the `reload` optional dependency: `pip install pbi-cli-tool[reload]`
## Suppressing Auto-Sync (--no-sync)
By default, every write command (`add-page`, `delete-page`, `set-background`,
`set-theme`, etc.) automatically syncs Power BI Desktop after each operation.
When building a report in multiple steps, this causes Desktop to reload after
every single command.
Use `--no-sync` on the `report` command group to suppress per-command syncs,
then call `pbi report reload` once at the end:
```bash
# BAD: Desktop reloads after every command
pbi report add-page --display-name "Overview" --name overview
pbi report set-background overview --color "#F2F2F2"
# GOOD: suppress sync during build, reload once at the end
pbi report --no-sync add-page --display-name "Overview" --name overview
pbi report --no-sync set-background overview --color "#F2F2F2"
pbi report reload
```
`--no-sync` is available on: `report`, `visual`, `filters`, and `bookmarks`
command groups.
## Convert
```bash

View file

@ -12,7 +12,8 @@ Manage row-level security (RLS) and perspectives for Power BI models.
```bash
pipx install pbi-cli-tool
pbi connect # Auto-detects Power BI Desktop and installs skills
pbi-cli skills install
pbi connect
```
## Security Roles (RLS)

View file

@ -212,6 +212,25 @@ accidental mass operations.
| textbox | textbox | (no data binding) |
| page_navigator | pageNavigator | (no data binding) |
## Suppressing Auto-Sync (--no-sync)
By default, every write command automatically syncs Power BI Desktop. When
adding or binding many visuals in sequence, Desktop reloads after each one.
Use `--no-sync` on the `visual` command group to batch all changes, then call
`pbi report reload` once at the end:
```bash
# Suppress sync while building visuals
pbi visual --no-sync add --page overview --type card --name rev_card
pbi visual --no-sync bind rev_card --page overview --field "Sales[Total Revenue]"
pbi visual --no-sync add --page overview --type bar --name sales_bar
pbi visual --no-sync bind sales_bar --page overview --category "Product[Category]" --value "Sales[Revenue]"
# Single reload when all visuals are done
pbi report reload
```
## JSON Output
All commands support `--json` for agent consumption:

View file

@ -11,7 +11,6 @@
},
"visual": {
"visualType": "actionButton",
"objects": {},
"visualContainerObjects": {},
"drillFilterOtherVisuals": true
},

View file

@ -18,7 +18,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -21,7 +21,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -21,7 +21,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -21,7 +21,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -18,7 +18,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -18,7 +18,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -22,7 +22,6 @@
"isDefaultSort": true
}
},
"objects": {},
"visualContainerObjects": {},
"drillFilterOtherVisuals": true
}

View file

@ -18,7 +18,6 @@
"Legend": {"projections": []}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -18,7 +18,6 @@
"Legend": {"projections": []}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -21,7 +21,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -21,7 +21,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -21,7 +21,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -21,7 +21,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -11,7 +11,6 @@
},
"visual": {
"visualType": "image",
"objects": {},
"visualContainerObjects": {},
"drillFilterOtherVisuals": true
},

View file

@ -24,7 +24,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -21,7 +21,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -24,7 +24,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -16,7 +16,6 @@
"Values": {"projections": [], "active": true}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -18,7 +18,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -11,7 +11,6 @@
},
"visual": {
"visualType": "pageNavigator",
"objects": {},
"visualContainerObjects": {},
"drillFilterOtherVisuals": true
},

View file

@ -21,7 +21,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -21,7 +21,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -24,7 +24,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -11,7 +11,6 @@
},
"visual": {
"visualType": "shape",
"objects": {},
"visualContainerObjects": {},
"drillFilterOtherVisuals": true
},

View file

@ -18,7 +18,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -21,7 +21,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

View file

@ -18,7 +18,6 @@
}
}
},
"objects": {},
"drillFilterOtherVisuals": true
}
}

Some files were not shown because too many files have changed in this diff Show more