@luquimbo/bi-superpowers 3.1.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +28 -2
  3. package/.claude-plugin/skill-manifest.json +22 -6
  4. package/.plugin/plugin.json +1 -1
  5. package/AGENTS.md +52 -36
  6. package/CHANGELOG.md +295 -0
  7. package/README.md +75 -26
  8. package/bin/build-plugin.js +17 -10
  9. package/bin/cli.js +278 -322
  10. package/bin/commands/build-desktop.js +35 -16
  11. package/bin/commands/diff.js +31 -13
  12. package/bin/commands/install.js +93 -72
  13. package/bin/commands/lint.js +40 -26
  14. package/bin/commands/mcp-setup.js +3 -10
  15. package/bin/commands/update-check.js +389 -0
  16. package/bin/lib/agents.js +19 -0
  17. package/bin/lib/generators/claude-plugin.js +144 -6
  18. package/bin/lib/generators/shared.js +29 -33
  19. package/bin/lib/mcp-config.js +191 -16
  20. package/bin/lib/skills.js +115 -27
  21. package/bin/postinstall.js +4 -2
  22. package/bin/utils/mcp-detect.js +2 -2
  23. package/commands/bi-start.md +218 -0
  24. package/commands/pbi-connect.md +43 -65
  25. package/commands/project-kickoff.md +393 -673
  26. package/commands/report-design.md +403 -0
  27. package/desktop-extension/manifest.json +5 -12
  28. package/desktop-extension/server.js +34 -25
  29. package/package.json +6 -10
  30. package/skills/bi-start/SKILL.md +220 -0
  31. package/skills/bi-start/scripts/update-check.js +389 -0
  32. package/skills/pbi-connect/SKILL.md +45 -67
  33. package/skills/pbi-connect/scripts/update-check.js +389 -0
  34. package/skills/project-kickoff/SKILL.md +395 -675
  35. package/skills/project-kickoff/scripts/update-check.js +389 -0
  36. package/skills/report-design/SKILL.md +405 -0
  37. package/skills/report-design/references/cli-commands.md +184 -0
  38. package/skills/report-design/references/cli-setup.md +101 -0
  39. package/skills/report-design/references/close-write-open-pattern.md +80 -0
  40. package/skills/report-design/references/layouts/finance.md +65 -0
  41. package/skills/report-design/references/layouts/generic.md +46 -0
  42. package/skills/report-design/references/layouts/hr.md +48 -0
  43. package/skills/report-design/references/layouts/marketing.md +45 -0
  44. package/skills/report-design/references/layouts/operations.md +44 -0
  45. package/skills/report-design/references/layouts/sales.md +50 -0
  46. package/skills/report-design/references/native-visuals.md +341 -0
  47. package/skills/report-design/references/pbi-desktop-installation.md +87 -0
  48. package/skills/report-design/references/pbir-preview-activation.md +40 -0
  49. package/skills/report-design/references/slicer.md +89 -0
  50. package/skills/report-design/references/textbox.md +101 -0
  51. package/skills/report-design/references/themes/BISuperpowers.json +915 -0
  52. package/skills/report-design/references/troubleshooting.md +135 -0
  53. package/skills/report-design/references/visual-types.md +78 -0
  54. package/skills/report-design/scripts/apply-theme.js +243 -0
  55. package/skills/report-design/scripts/create-visual.js +878 -0
  56. package/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  57. package/skills/report-design/scripts/update-check.js +389 -0
  58. package/skills/report-design/scripts/validate-pbir.js +322 -0
  59. package/src/content/base.md +12 -68
  60. package/src/content/mcp-requirements.json +0 -25
  61. package/src/content/routing.md +19 -74
  62. package/src/content/skills/bi-start.md +191 -0
  63. package/src/content/skills/pbi-connect.md +22 -65
  64. package/src/content/skills/project-kickoff.md +372 -673
  65. package/src/content/skills/report-design/SKILL.md +376 -0
  66. package/src/content/skills/report-design/references/cli-commands.md +184 -0
  67. package/src/content/skills/report-design/references/cli-setup.md +101 -0
  68. package/src/content/skills/report-design/references/close-write-open-pattern.md +80 -0
  69. package/src/content/skills/report-design/references/layouts/finance.md +65 -0
  70. package/src/content/skills/report-design/references/layouts/generic.md +46 -0
  71. package/src/content/skills/report-design/references/layouts/hr.md +48 -0
  72. package/src/content/skills/report-design/references/layouts/marketing.md +45 -0
  73. package/src/content/skills/report-design/references/layouts/operations.md +44 -0
  74. package/src/content/skills/report-design/references/layouts/sales.md +50 -0
  75. package/src/content/skills/report-design/references/native-visuals.md +341 -0
  76. package/src/content/skills/report-design/references/pbi-desktop-installation.md +87 -0
  77. package/src/content/skills/report-design/references/pbir-preview-activation.md +40 -0
  78. package/src/content/skills/report-design/references/slicer.md +89 -0
  79. package/src/content/skills/report-design/references/textbox.md +101 -0
  80. package/src/content/skills/report-design/references/themes/BISuperpowers.json +915 -0
  81. package/src/content/skills/report-design/references/troubleshooting.md +135 -0
  82. package/src/content/skills/report-design/references/visual-types.md +78 -0
  83. package/src/content/skills/report-design/scripts/apply-theme.js +243 -0
  84. package/src/content/skills/report-design/scripts/create-visual.js +878 -0
  85. package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  86. package/src/content/skills/report-design/scripts/validate-pbir.js +322 -0
  87. package/bin/commands/add.js +0 -533
  88. package/bin/commands/add.test.js +0 -77
  89. package/bin/commands/changelog.js +0 -443
  90. package/bin/commands/install.test.js +0 -289
  91. package/bin/commands/lint.test.js +0 -103
  92. package/bin/commands/pull.js +0 -287
  93. package/bin/commands/pull.test.js +0 -36
  94. package/bin/commands/push.js +0 -231
  95. package/bin/commands/push.test.js +0 -14
  96. package/bin/commands/search.js +0 -344
  97. package/bin/commands/search.test.js +0 -115
  98. package/bin/commands/setup.js +0 -545
  99. package/bin/commands/setup.test.js +0 -46
  100. package/bin/commands/sync-profile.js +0 -405
  101. package/bin/commands/sync-profile.test.js +0 -14
  102. package/bin/commands/sync-source.js +0 -418
  103. package/bin/commands/sync-source.test.js +0 -14
  104. package/bin/lib/generators/claude-plugin.test.js +0 -111
  105. package/bin/lib/mcp-config.test.js +0 -310
  106. package/bin/lib/microsoft-mcp.test.js +0 -115
  107. package/bin/utils/errors.js +0 -159
  108. package/bin/utils/git.js +0 -298
  109. package/bin/utils/logger.js +0 -142
  110. package/bin/utils/mcp-detect.test.js +0 -81
  111. package/bin/utils/pbix.js +0 -305
  112. package/bin/utils/pbix.test.js +0 -37
  113. package/bin/utils/profiles.js +0 -312
  114. package/bin/utils/projects.js +0 -169
  115. package/bin/utils/readline.js +0 -206
  116. package/bin/utils/readline.test.js +0 -47
  117. package/bin/utils/tui.test.js +0 -127
  118. package/docs/openrouter-free-models.md +0 -92
  119. package/library/examples/README.md +0 -151
  120. package/library/examples/finance-reporting/README.md +0 -351
  121. package/library/examples/finance-reporting/data-model.md +0 -267
  122. package/library/examples/finance-reporting/measures.dax +0 -557
  123. package/library/examples/hr-analytics/README.md +0 -371
  124. package/library/examples/hr-analytics/data-model.md +0 -315
  125. package/library/examples/hr-analytics/measures.dax +0 -460
  126. package/library/examples/marketing-analytics/README.md +0 -37
  127. package/library/examples/marketing-analytics/data-model.md +0 -62
  128. package/library/examples/marketing-analytics/measures.dax +0 -110
  129. package/library/examples/retail-analytics/README.md +0 -439
  130. package/library/examples/retail-analytics/data-model.md +0 -288
  131. package/library/examples/retail-analytics/measures.dax +0 -481
  132. package/library/examples/supply-chain/README.md +0 -37
  133. package/library/examples/supply-chain/data-model.md +0 -69
  134. package/library/examples/supply-chain/measures.dax +0 -77
  135. package/library/examples/udf-library/README.md +0 -228
  136. package/library/examples/udf-library/functions.dax +0 -571
  137. package/library/snippets/dax/README.md +0 -292
  138. package/library/snippets/dax/business-domains.md +0 -576
  139. package/library/snippets/dax/calculate-patterns.md +0 -276
  140. package/library/snippets/dax/calculation-groups.md +0 -489
  141. package/library/snippets/dax/error-handling.md +0 -495
  142. package/library/snippets/dax/iterators-and-aggregations.md +0 -474
  143. package/library/snippets/dax/kpis-and-metrics.md +0 -293
  144. package/library/snippets/dax/rankings-and-topn.md +0 -235
  145. package/library/snippets/dax/security-patterns.md +0 -413
  146. package/library/snippets/dax/text-and-formatting.md +0 -316
  147. package/library/snippets/dax/time-intelligence.md +0 -196
  148. package/library/snippets/dax/user-defined-functions.md +0 -477
  149. package/library/snippets/dax/virtual-tables.md +0 -546
  150. package/library/snippets/excel-formulas/README.md +0 -84
  151. package/library/snippets/excel-formulas/aggregations.md +0 -330
  152. package/library/snippets/excel-formulas/dates-and-times.md +0 -361
  153. package/library/snippets/excel-formulas/dynamic-arrays.md +0 -314
  154. package/library/snippets/excel-formulas/lookups.md +0 -169
  155. package/library/snippets/excel-formulas/text-functions.md +0 -363
  156. package/library/snippets/governance/naming-conventions.md +0 -97
  157. package/library/snippets/governance/review-checklists.md +0 -107
  158. package/library/snippets/power-query/README.md +0 -389
  159. package/library/snippets/power-query/api-integration.md +0 -707
  160. package/library/snippets/power-query/connections.md +0 -434
  161. package/library/snippets/power-query/data-cleaning.md +0 -298
  162. package/library/snippets/power-query/error-handling.md +0 -526
  163. package/library/snippets/power-query/parameters.md +0 -350
  164. package/library/snippets/power-query/performance.md +0 -506
  165. package/library/snippets/power-query/transformations.md +0 -330
  166. package/library/snippets/report-design/accessibility.md +0 -78
  167. package/library/snippets/report-design/chart-selection.md +0 -54
  168. package/library/snippets/report-design/layout-patterns.md +0 -87
  169. package/library/templates/data-models/README.md +0 -93
  170. package/library/templates/data-models/finance-model.md +0 -627
  171. package/library/templates/data-models/retail-star-schema.md +0 -473
  172. package/library/templates/excel/README.md +0 -83
  173. package/library/templates/excel/budget-tracker.md +0 -432
  174. package/library/templates/excel/data-entry-form.md +0 -533
  175. package/library/templates/power-bi/README.md +0 -72
  176. package/library/templates/power-bi/finance-report.md +0 -449
  177. package/library/templates/power-bi/kpi-scorecard.md +0 -461
  178. package/library/templates/power-bi/sales-dashboard.md +0 -281
  179. package/library/themes/excel/README.md +0 -436
  180. package/library/themes/power-bi/README.md +0 -271
  181. package/library/themes/power-bi/accessible.json +0 -307
  182. package/library/themes/power-bi/bi-superpowers-default.json +0 -858
  183. package/library/themes/power-bi/corporate-blue.json +0 -291
  184. package/library/themes/power-bi/dark-mode.json +0 -291
  185. package/library/themes/power-bi/minimal.json +0 -292
  186. package/library/themes/power-bi/print-friendly.json +0 -309
@@ -1,27 +1,48 @@
1
1
  ---
2
2
  name: "pbi-connect"
3
- description: "Use when the user asks about Power BI MCP Connection Skill, especially phrases like \"connect Power BI\", \"modeling mcp\", \"Power BI Desktop\", \"conectar Power BI\", \"can't connect to Power BI\"."
4
- version: "3.1.1"
3
+ description: "Use when the user asks about Power BI MCP Connection Skill, especially phrases like \"connect Power BI\", \"PBI connection\", \"MCP connection\", \"Power BI MCP\", \"modeling mcp\", \"Power BI Modeling MCP\"."
4
+ version: "4.1.0"
5
5
  ---
6
6
 
7
7
  <!-- Generated by BI Agent Superpowers. Edit src/content/skills/pbi-connect.md instead. -->
8
8
 
9
+ <!-- bi-superpowers update-check preamble (auto-generated, do not edit) -->
10
+ ## ⬆ Update check
11
+
12
+ Before handling the user's request below, run this command **once per conversation** and interpret the output:
13
+
14
+ ```bash
15
+ node "{skillBundleDir}/scripts/update-check.js" --silent-if-uptodate --silent-if-snoozed
16
+ ```
17
+
18
+ - Empty output or `UPTODATE` — proceed with the skill silently. No message.
19
+ - `UPDATE_AVAILABLE <installed> <latest>` — tell the user exactly once this conversation, before diving into the skill:
20
+ > "Hay **bi-superpowers v{latest}** disponible (estás en v{installed}). Actualizá con `super upgrade` (o `/plugin update bi-superpowers` en Claude Code) cuando te venga bien."
21
+
22
+ Then continue with the skill below.
23
+ - `SNOOZED <iso>` — proceed silently.
24
+
25
+ If the command fails (missing binary, permissions, offline), ignore the error and proceed with the skill. The update check must never block the user's request.
26
+
27
+ ---
28
+ <!-- /bi-superpowers update-check preamble -->
29
+
9
30
  # Power BI MCP Connection Skill
10
31
 
11
32
  ## Trigger
12
33
  Activate this skill when user mentions:
13
34
  - "connect Power BI", "PBI connection", "MCP connection", "Power BI MCP"
14
- - "modeling mcp", "remote mcp", "fabric mcp"
15
- - "Power BI Desktop", "PBIP", "semantic model", "Fabric workspace"
16
- - "conectar Power BI", "MCP de Power BI", "Fabric MCP"
35
+ - "modeling mcp", "Power BI Modeling MCP"
36
+ - "Power BI Desktop", "PBIP", "semantic model"
37
+ - "conectar Power BI", "MCP de Power BI", "modeling mcp"
17
38
  - "can't connect to Power BI", "connection error", "MCP not working"
18
39
 
19
40
  ## Identity
20
- You are a **Power BI MCP Connection Specialist**. Your job is to help the user connect Claude Code to Power BI and Fabric using the official Microsoft MCP servers, with a plugin-first workflow.
41
+ You are a **Power BI MCP Connection Specialist**. Your job is to help the user connect their AI agent to Power BI Desktop using the official Microsoft MCP servers shipped with bi-superpowers, with a plugin-first workflow.
21
42
 
22
43
  ## MANDATORY RULES
23
44
  1. **PLUGIN-FIRST.** Prefer `.mcp.json` in the Claude Code plugin root.
24
- 2. **OFFICIAL SERVERS ONLY.** Recommend `powerbi-remote`, `fabric-mcp-server`, and `powerbi-modeling-mcp`.
45
+ 2. **OFFICIAL SERVERS ONLY.** Use `powerbi-modeling-mcp` (local) and `microsoft-learn` (HTTP). Do not invent or recommend unofficial MCPs.
25
46
  3. **WINDOWS LIMITATION.** Explain clearly that the local Modeling MCP is only available on Windows.
26
47
  4. **NO PORT INVENTION.** Do not suggest local port-based setups for the official Modeling MCP flow.
27
48
  5. **ONE QUESTION AT A TIME.** Follow the wizard pattern.
@@ -36,13 +57,12 @@ Start with:
36
57
  POWER BI MCP CONNECTION
37
58
  =======================
38
59
 
39
- I'll help you connect Claude Code using the official Microsoft MCP servers.
60
+ I'll help you connect your AI agent using the official Microsoft MCP servers.
40
61
 
41
62
  What do you need?
42
63
 
43
- 1. Connect to Power BI Desktop / PBIP on this machine
44
- 2. Connect to a remote Power BI or Fabric workspace
45
- 3. Verify that my plugin `.mcp.json` is configured correctly
64
+ 1. Connect to Power BI Desktop on this machine (Windows)
65
+ 2. Verify that my plugin `.mcp.json` is configured correctly
46
66
  ```
47
67
 
48
68
  ---
@@ -83,19 +103,14 @@ If the user wants a config example, show:
83
103
 
84
104
  ```json
85
105
  {
86
- "powerbi-remote": {
87
- "type": "http",
88
- "url": "https://api.fabric.microsoft.com/v1/mcp/powerbi"
89
- },
90
- "fabric-mcp-server": {
91
- "type": "stdio",
92
- "command": "npx",
93
- "args": ["-y", "@microsoft/fabric-mcp@latest", "server", "start", "--mode", "all"]
94
- },
95
106
  "powerbi-modeling-mcp": {
96
107
  "type": "stdio",
97
108
  "command": "node",
98
109
  "args": ["${CLAUDE_PLUGIN_ROOT}/bin/mcp/powerbi-modeling-launcher.js"]
110
+ },
111
+ "microsoft-learn": {
112
+ "type": "http",
113
+ "url": "https://learn.microsoft.com/api/mcp"
99
114
  }
100
115
  }
101
116
  ```
@@ -120,51 +135,18 @@ Say:
120
135
 
121
136
  ```text
122
137
  The official local Power BI Modeling MCP is only available on Windows.
123
- You can still work with:
124
-
125
- - powerbi-remote
126
- - fabric-mcp-server
127
138
 
128
- If you need local Desktop editing, you'll need a Windows environment.
139
+ You still have `microsoft-learn` (HTTP) available on every platform for
140
+ docs, and you can work with Power BI files using the skills library.
141
+ For live editing of a local semantic model, you need a Windows environment.
129
142
  ```
130
143
 
131
144
  ---
132
145
 
133
- ## PHASE 2: Remote Power BI / Fabric
146
+ ## PHASE 2: Verify Plugin Config
134
147
 
135
148
  If the user chooses option 2:
136
149
 
137
- ```text
138
- REMOTE / FABRIC MCP
139
- ===================
140
-
141
- This path uses the Microsoft-hosted and Microsoft-published MCP servers.
142
-
143
- Default servers:
144
- - powerbi-remote
145
- - fabric-mcp-server
146
- ```
147
-
148
- Guide them to:
149
-
150
- 1. Run `bi-superpowers mcp-setup`
151
- 2. Verify `.mcp.json` contains the official endpoint and Fabric package
152
- 3. Authenticate in the environment required by their MCP client
153
- 4. Restart or refresh Claude Code
154
-
155
- If they ask what gets configured, show:
156
-
157
- ```text
158
- powerbi-remote -> https://api.fabric.microsoft.com/v1/mcp/powerbi
159
- fabric-mcp-server -> npx -y @microsoft/fabric-mcp@latest server start --mode all
160
- ```
161
-
162
- ---
163
-
164
- ## PHASE 3: Verify Plugin Config
165
-
166
- If the user chooses option 3:
167
-
168
150
  Check these files in order:
169
151
 
170
152
  1. `.claude-plugin/plugin.json`
@@ -174,9 +156,8 @@ Check these files in order:
174
156
  Confirm:
175
157
 
176
158
  - plugin name is `bi-superpowers`
177
- - `.mcp.json` includes `powerbi-remote`
178
- - `.mcp.json` includes `fabric-mcp-server`
179
159
  - `.mcp.json` includes `powerbi-modeling-mcp`
160
+ - `.mcp.json` includes `microsoft-learn`
180
161
 
181
162
  If anything is missing, recommend:
182
163
 
@@ -200,7 +181,7 @@ claude --plugin-dir .
200
181
  | Modeling MCP missing on Windows | Install the Microsoft extension in VS Code or Cursor |
201
182
  | Modeling MCP installed manually | Set `BI_SUPERPOWERS_POWERBI_MODELING_MCP_PATH` |
202
183
  | Plugin not loading MCPs | Re-run `bi-superpowers mcp-setup` and restart Claude Code |
203
- | macOS/Linux local modeling request | Redirect to `powerbi-remote` or `fabric-mcp-server` |
184
+ | macOS/Linux local modeling request | Use `microsoft-learn` for docs; live editing requires Windows |
204
185
  | User asks about Excel MCP | Explain Excel remains supported through skills and library content, not a default MCP |
205
186
 
206
187
  ---
@@ -212,7 +193,7 @@ claude --plugin-dir .
212
193
  | Recommend `uvx` for Modeling MCP | Not the official Microsoft installation path | Use the official executable via the local launcher |
213
194
  | Ask the user to find a localhost port | Not required in the new flow | Use the official Modeling MCP launcher |
214
195
  | Put plugin MCP config in `.claude/settings.json` first | Plugin-first flow uses `.mcp.json` | Prefer `.mcp.json` at the plugin root |
215
- | Block non-Windows users entirely | Remote/Fabric MCPs still work | Continue with `powerbi-remote` and `fabric-mcp-server` |
196
+ | Invent unofficial MCPs (remote, fabric, etc.) | This plugin only ships 2 official MCPs | Only use the 2 official MCPs we ship (`powerbi-modeling-mcp` and `microsoft-learn`) |
216
197
 
217
198
  ---
218
199
 
@@ -227,15 +208,12 @@ Adjust depth based on `config.json → experienceLevel`:
227
208
 
228
209
  ## Related Skills
229
210
 
230
- - `/dax` — Write DAX measures via MCP connection
231
- - `/model-documenter` — Document the connected model
232
- - `/fabric-scripts` — Fabric automation via MCP
211
+ - `/project-kickoff` — Analyze a BI project and plan next steps
233
212
 
234
213
  ---
235
214
 
236
215
  ## RELATED RESOURCES
237
216
 
238
217
  - [Power BI MCP overview](https://learn.microsoft.com/en-us/power-bi/developer/mcp/mcp-servers-overview)
239
- - [Remote Power BI MCP quickstart](https://learn.microsoft.com/en-us/power-bi/developer/mcp/remote-mcp-server-get-started)
240
- - [Power BI Modeling MCP](https://github.com/microsoft/powerbi-modeling-mcp)
241
- - [Microsoft Fabric MCP Server](https://github.com/microsoft/mcp/tree/main/servers/Fabric.Mcp.Server)
218
+ - [Power BI Modeling MCP on GitHub](https://github.com/microsoft/powerbi-modeling-mcp)
219
+ - [Microsoft Learn MCP](https://learn.microsoft.com/en-us/training/support/mcp)
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * update-check — cross-agent version-check helper for bi-superpowers.
4
+ *
5
+ * The SKILL.md preamble (see lib/generators/claude-plugin.js) invokes this
6
+ * script at the start of every skill so the agent can surface an update
7
+ * notice to the user when a newer version is on npm — without hitting the
8
+ * network on every invocation. Cache TTL is 24h; repeated calls inside
9
+ * that window are served from `~/.bi-superpowers/update-state.json`.
10
+ *
11
+ * Output (stdout, one line):
12
+ * UPTODATE when installed >= latest
13
+ * UPDATE_AVAILABLE <installed> <latest> when installed < latest
14
+ * SNOOZED <iso> when user deferred the notice
15
+ *
16
+ * Flags:
17
+ * --force bypass cache (re-fetch npm, ignore snooze TTL)
18
+ * --silent-if-uptodate suppress UPTODATE line (used by the preamble)
19
+ * --silent-if-snoozed suppress SNOOZED line (used by the preamble)
20
+ * --json emit JSON instead of text
21
+ * --snooze 24h|48h|7d|clear set (or clear) the snooze state and exit
22
+ * --reset delete the state file and exit (used post-upgrade)
23
+ * --state-dir <path> override ~/.bi-superpowers/ (for tests)
24
+ * --package-name <name> override the package name (for tests)
25
+ * -h, --help show this help
26
+ *
27
+ * Exit code is always 0 when the script itself ran — errors during the
28
+ * network fetch degrade to "no output" so the caller never blocks. A
29
+ * non-zero exit means a user error (bad flags).
30
+ *
31
+ * Pure helpers (compareVersions, isCacheFresh, isSnoozed,
32
+ * computeNextSnoozeUntil, readState, writeState, fetchLatestVersion) are
33
+ * exported so unit tests can exercise them without spawning child
34
+ * processes or hitting the network.
35
+ */
36
+
37
+ 'use strict';
38
+
39
+ const fs = require('fs');
40
+ const os = require('os');
41
+ const path = require('path');
42
+ const https = require('https');
43
+
44
+ const PACKAGE_NAME = '@luquimbo/bi-superpowers';
45
+ const CACHE_TTL_MS = 1000 * 60 * 60 * 24; // 24 hours
46
+ const HTTPS_TIMEOUT_MS = 5000;
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Argument parsing
50
+ // ---------------------------------------------------------------------------
51
+
52
+ function parseArgs(argv) {
53
+ const out = {
54
+ force: false,
55
+ silentIfUptodate: false,
56
+ silentIfSnoozed: false,
57
+ json: false,
58
+ snooze: null,
59
+ reset: false,
60
+ help: false,
61
+ stateDir: null,
62
+ packageName: null,
63
+ };
64
+ for (let i = 0; i < argv.length; i += 1) {
65
+ const a = argv[i];
66
+ if (a === '--force') out.force = true;
67
+ else if (a === '--silent-if-uptodate') out.silentIfUptodate = true;
68
+ else if (a === '--silent-if-snoozed') out.silentIfSnoozed = true;
69
+ else if (a === '--json') out.json = true;
70
+ else if (a === '--snooze') out.snooze = argv[++i];
71
+ else if (a === '--reset') out.reset = true;
72
+ else if (a === '--state-dir') out.stateDir = argv[++i];
73
+ else if (a === '--package-name') out.packageName = argv[++i];
74
+ else if (a === '-h' || a === '--help') out.help = true;
75
+ else {
76
+ process.stderr.write(`update-check: unknown flag: ${a}\n`);
77
+ process.exit(1);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+
83
+ function help() {
84
+ process.stdout.write(
85
+ [
86
+ 'Usage: update-check [options]',
87
+ '',
88
+ 'Prints one of: UPTODATE, UPDATE_AVAILABLE <installed> <latest>, SNOOZED <iso>.',
89
+ '',
90
+ 'Options:',
91
+ ' --force Bypass cache and snooze TTL',
92
+ ' --silent-if-uptodate Skip the UPTODATE line',
93
+ ' --silent-if-snoozed Skip the SNOOZED line',
94
+ ' --json Emit JSON',
95
+ ' --snooze <dur> Set snooze state (24h|48h|7d) or "clear" to reset snooze',
96
+ ' --reset Delete the state file (used after a successful upgrade)',
97
+ ' --state-dir <path> Override ~/.bi-superpowers/ (tests)',
98
+ ' --package-name <name> Override the package name (tests)',
99
+ ' -h, --help Show this help',
100
+ '',
101
+ ].join('\n')
102
+ );
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Version comparison (semver-ish: MAJOR.MINOR.PATCH with optional -prerelease)
107
+ // No deps; handles the shapes @luquimbo/bi-superpowers uses today.
108
+ // ---------------------------------------------------------------------------
109
+
110
+ /**
111
+ * Compare two semver strings.
112
+ * Returns -1 if a < b, 0 if equal, 1 if a > b.
113
+ * Pre-release tags (`-alpha.1`) sort before the release per semver.
114
+ */
115
+ function compareVersions(a, b) {
116
+ const parse = (v) => {
117
+ const [main, pre] = String(v).split('-');
118
+ const parts = main.split('.').map((n) => parseInt(n, 10) || 0);
119
+ while (parts.length < 3) parts.push(0);
120
+ return { parts, pre: pre || null };
121
+ };
122
+ const va = parse(a);
123
+ const vb = parse(b);
124
+ for (let i = 0; i < 3; i += 1) {
125
+ if (va.parts[i] !== vb.parts[i]) return va.parts[i] < vb.parts[i] ? -1 : 1;
126
+ }
127
+ // Main equal — pre-release < release.
128
+ if (va.pre && !vb.pre) return -1;
129
+ if (!va.pre && vb.pre) return 1;
130
+ if (va.pre && vb.pre) {
131
+ if (va.pre < vb.pre) return -1;
132
+ if (va.pre > vb.pre) return 1;
133
+ }
134
+ return 0;
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Cache + snooze state
139
+ // ---------------------------------------------------------------------------
140
+
141
+ function defaultStateDir() {
142
+ return path.join(os.homedir(), '.bi-superpowers');
143
+ }
144
+
145
+ function stateFilePath(stateDir) {
146
+ return path.join(stateDir, 'update-state.json');
147
+ }
148
+
149
+ function readState(stateDir) {
150
+ const filePath = stateFilePath(stateDir);
151
+ if (!fs.existsSync(filePath)) return null;
152
+ try {
153
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
154
+ } catch (_) {
155
+ // Malformed → treat as no cache.
156
+ return null;
157
+ }
158
+ }
159
+
160
+ function writeState(stateDir, state) {
161
+ fs.mkdirSync(stateDir, { recursive: true });
162
+ fs.writeFileSync(stateFilePath(stateDir), JSON.stringify(state, null, 2) + '\n');
163
+ }
164
+
165
+ function resetState(stateDir) {
166
+ const filePath = stateFilePath(stateDir);
167
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
168
+ }
169
+
170
+ function isCacheFresh(state, now, ttlMs) {
171
+ if (!state || !state.checkedAt) return false;
172
+ const checkedAt = Date.parse(state.checkedAt);
173
+ if (!Number.isFinite(checkedAt)) return false;
174
+ return now - checkedAt < ttlMs;
175
+ }
176
+
177
+ function isSnoozed(state, now) {
178
+ if (!state || !state.snoozeUntil) return false;
179
+ const until = Date.parse(state.snoozeUntil);
180
+ if (!Number.isFinite(until)) return false;
181
+ return until > now;
182
+ }
183
+
184
+ // Snooze escalation: 24h → 48h → 7d (capped).
185
+ function computeNextSnoozeUntil(currentLevel, now) {
186
+ const levels = [
187
+ 1000 * 60 * 60 * 24, // 24h
188
+ 1000 * 60 * 60 * 48, // 48h
189
+ 1000 * 60 * 60 * 24 * 7, // 7d
190
+ ];
191
+ const idx = Math.min(Math.max(currentLevel, 0), levels.length - 1);
192
+ return new Date(now + levels[idx]).toISOString();
193
+ }
194
+
195
+ function parseSnoozeArg(arg, now, currentLevel) {
196
+ if (arg === 'clear') return { clear: true };
197
+ if (arg === '24h') return { until: new Date(now + 1000 * 60 * 60 * 24).toISOString(), level: 0 };
198
+ if (arg === '48h') return { until: new Date(now + 1000 * 60 * 60 * 48).toISOString(), level: 1 };
199
+ if (arg === '7d')
200
+ return { until: new Date(now + 1000 * 60 * 60 * 24 * 7).toISOString(), level: 2 };
201
+ if (arg === 'auto')
202
+ return {
203
+ until: computeNextSnoozeUntil(currentLevel, now),
204
+ level: Math.min(currentLevel + 1, 2),
205
+ };
206
+ throw new Error(`invalid --snooze value: ${arg}. Expected 24h|48h|7d|auto|clear.`);
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // npm registry fetch
211
+ // ---------------------------------------------------------------------------
212
+
213
+ /**
214
+ * Fetch the latest published version of a package from the npm registry.
215
+ * Never rejects with a network error — resolves null on timeout / failure
216
+ * so callers always degrade gracefully.
217
+ *
218
+ * @param {string} packageName - e.g. "@luquimbo/bi-superpowers"
219
+ * @returns {Promise<string|null>}
220
+ */
221
+ function fetchLatestVersion(packageName) {
222
+ return new Promise((resolve) => {
223
+ const encoded = packageName.replace('/', '%2F');
224
+ const url = `https://registry.npmjs.org/${encoded}/latest`;
225
+
226
+ const req = https.get(
227
+ url,
228
+ { headers: { Accept: 'application/vnd.npm.install-v1+json' } },
229
+ (res) => {
230
+ if (res.statusCode !== 200) {
231
+ res.resume();
232
+ resolve(null);
233
+ return;
234
+ }
235
+ let body = '';
236
+ res.setEncoding('utf8');
237
+ res.on('data', (chunk) => (body += chunk));
238
+ res.on('end', () => {
239
+ try {
240
+ const json = JSON.parse(body);
241
+ resolve(typeof json.version === 'string' ? json.version : null);
242
+ } catch (_) {
243
+ resolve(null);
244
+ }
245
+ });
246
+ }
247
+ );
248
+ req.on('error', () => resolve(null));
249
+ req.setTimeout(HTTPS_TIMEOUT_MS, () => {
250
+ req.destroy();
251
+ resolve(null);
252
+ });
253
+ });
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Installed version — read from our own package.json
258
+ // ---------------------------------------------------------------------------
259
+
260
+ function readInstalledVersion() {
261
+ try {
262
+ return require(path.join(__dirname, '..', '..', 'package.json')).version;
263
+ } catch (_) {
264
+ return null;
265
+ }
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Emit helpers
270
+ // ---------------------------------------------------------------------------
271
+
272
+ function emit(args, kind, payload) {
273
+ if (args.json) {
274
+ process.stdout.write(JSON.stringify({ status: kind, ...payload }) + '\n');
275
+ return;
276
+ }
277
+ if (kind === 'UPTODATE' && args.silentIfUptodate) return;
278
+ if (kind === 'SNOOZED' && args.silentIfSnoozed) return;
279
+
280
+ if (kind === 'UPTODATE') process.stdout.write('UPTODATE\n');
281
+ else if (kind === 'UPDATE_AVAILABLE')
282
+ process.stdout.write(`UPDATE_AVAILABLE ${payload.installed} ${payload.latest}\n`);
283
+ else if (kind === 'SNOOZED') process.stdout.write(`SNOOZED ${payload.until}\n`);
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // main
288
+ // ---------------------------------------------------------------------------
289
+
290
+ async function main() {
291
+ const args = parseArgs(process.argv.slice(2));
292
+ if (args.help) {
293
+ help();
294
+ return;
295
+ }
296
+
297
+ const stateDir = args.stateDir || defaultStateDir();
298
+ const packageName = args.packageName || PACKAGE_NAME;
299
+
300
+ if (args.reset) {
301
+ resetState(stateDir);
302
+ return;
303
+ }
304
+
305
+ if (args.snooze) {
306
+ const now = Date.now();
307
+ const prior = readState(stateDir) || {};
308
+ const parsed = parseSnoozeArg(args.snooze, now, prior.snoozeLevel || 0);
309
+ if (parsed.clear) {
310
+ writeState(stateDir, { ...prior, snoozeUntil: null, snoozeLevel: 0 });
311
+ } else {
312
+ writeState(stateDir, {
313
+ ...prior,
314
+ snoozeUntil: parsed.until,
315
+ snoozeLevel: parsed.level,
316
+ });
317
+ }
318
+ return;
319
+ }
320
+
321
+ const installed = readInstalledVersion();
322
+ if (!installed) {
323
+ // Installed version undetermined — nothing useful to report.
324
+ return;
325
+ }
326
+
327
+ const now = Date.now();
328
+ let state = readState(stateDir);
329
+
330
+ // Snooze short-circuits everything except --force.
331
+ if (!args.force && isSnoozed(state, now)) {
332
+ emit(args, 'SNOOZED', { until: state.snoozeUntil });
333
+ return;
334
+ }
335
+
336
+ // Use cached `latest` when the cache is fresh (unless --force).
337
+ let latest = state && state.latest;
338
+ if (args.force || !isCacheFresh(state, now, CACHE_TTL_MS)) {
339
+ const fetched = await fetchLatestVersion(packageName);
340
+ if (fetched) {
341
+ latest = fetched;
342
+ const nextState = {
343
+ installed,
344
+ latest,
345
+ checkedAt: new Date(now).toISOString(),
346
+ snoozeUntil: (state && state.snoozeUntil) || null,
347
+ snoozeLevel: (state && state.snoozeLevel) || 0,
348
+ };
349
+ writeState(stateDir, nextState);
350
+ state = nextState;
351
+ }
352
+ // If fetched is null (network fail), we keep using the previous cache
353
+ // — or emit nothing if there's no cache at all.
354
+ }
355
+
356
+ if (!latest) {
357
+ // No cached value and no fetch — nothing to say.
358
+ return;
359
+ }
360
+
361
+ if (compareVersions(installed, latest) < 0) {
362
+ emit(args, 'UPDATE_AVAILABLE', { installed, latest });
363
+ } else {
364
+ emit(args, 'UPTODATE', { installed, latest });
365
+ }
366
+ }
367
+
368
+ module.exports = {
369
+ parseArgs,
370
+ compareVersions,
371
+ isCacheFresh,
372
+ isSnoozed,
373
+ computeNextSnoozeUntil,
374
+ parseSnoozeArg,
375
+ readState,
376
+ writeState,
377
+ resetState,
378
+ fetchLatestVersion,
379
+ CACHE_TTL_MS,
380
+ PACKAGE_NAME,
381
+ };
382
+
383
+ if (require.main === module) {
384
+ main().catch((err) => {
385
+ // Never throw out of the CLI — the preamble must not break skill invocation.
386
+ process.stderr.write(`update-check: ${err.message}\n`);
387
+ process.exit(0);
388
+ });
389
+ }