@luquimbo/bi-superpowers 3.2.0 → 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.
- package/.claude-plugin/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +28 -2
- package/.claude-plugin/skill-manifest.json +22 -6
- package/.plugin/plugin.json +1 -1
- package/AGENTS.md +52 -36
- package/CHANGELOG.md +295 -0
- package/README.md +75 -26
- package/bin/build-plugin.js +11 -4
- package/bin/cli.js +113 -16
- package/bin/commands/build-desktop.js +35 -16
- package/bin/commands/diff.js +31 -13
- package/bin/commands/install.js +7 -3
- package/bin/commands/lint.js +40 -26
- package/bin/commands/mcp-setup.js +3 -10
- package/bin/commands/update-check.js +389 -0
- package/bin/lib/generators/claude-plugin.js +144 -6
- package/bin/lib/generators/shared.js +29 -33
- package/bin/lib/mcp-config.js +168 -12
- package/bin/lib/skills.js +115 -27
- package/bin/postinstall.js +4 -2
- package/bin/utils/mcp-detect.js +2 -2
- package/commands/bi-start.md +218 -0
- package/commands/pbi-connect.md +43 -65
- package/commands/project-kickoff.md +393 -673
- package/commands/report-design.md +403 -0
- package/desktop-extension/manifest.json +3 -3
- package/package.json +7 -5
- package/skills/bi-start/SKILL.md +220 -0
- package/skills/bi-start/scripts/update-check.js +389 -0
- package/skills/pbi-connect/SKILL.md +45 -67
- package/skills/pbi-connect/scripts/update-check.js +389 -0
- package/skills/project-kickoff/SKILL.md +395 -675
- package/skills/project-kickoff/scripts/update-check.js +389 -0
- package/skills/report-design/SKILL.md +405 -0
- package/skills/report-design/references/cli-commands.md +184 -0
- package/skills/report-design/references/cli-setup.md +101 -0
- package/skills/report-design/references/close-write-open-pattern.md +80 -0
- package/skills/report-design/references/layouts/finance.md +65 -0
- package/skills/report-design/references/layouts/generic.md +46 -0
- package/skills/report-design/references/layouts/hr.md +48 -0
- package/skills/report-design/references/layouts/marketing.md +45 -0
- package/skills/report-design/references/layouts/operations.md +44 -0
- package/skills/report-design/references/layouts/sales.md +50 -0
- package/skills/report-design/references/native-visuals.md +341 -0
- package/skills/report-design/references/pbi-desktop-installation.md +87 -0
- package/skills/report-design/references/pbir-preview-activation.md +40 -0
- package/skills/report-design/references/slicer.md +89 -0
- package/skills/report-design/references/textbox.md +101 -0
- package/skills/report-design/references/themes/BISuperpowers.json +915 -0
- package/skills/report-design/references/troubleshooting.md +135 -0
- package/skills/report-design/references/visual-types.md +78 -0
- package/skills/report-design/scripts/apply-theme.js +243 -0
- package/skills/report-design/scripts/create-visual.js +878 -0
- package/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
- package/skills/report-design/scripts/update-check.js +389 -0
- package/skills/report-design/scripts/validate-pbir.js +322 -0
- package/src/content/base.md +12 -68
- package/src/content/mcp-requirements.json +0 -25
- package/src/content/routing.md +19 -74
- package/src/content/skills/bi-start.md +191 -0
- package/src/content/skills/pbi-connect.md +22 -65
- package/src/content/skills/project-kickoff.md +372 -673
- package/src/content/skills/report-design/SKILL.md +376 -0
- package/src/content/skills/report-design/references/cli-commands.md +184 -0
- package/src/content/skills/report-design/references/cli-setup.md +101 -0
- package/src/content/skills/report-design/references/close-write-open-pattern.md +80 -0
- package/src/content/skills/report-design/references/layouts/finance.md +65 -0
- package/src/content/skills/report-design/references/layouts/generic.md +46 -0
- package/src/content/skills/report-design/references/layouts/hr.md +48 -0
- package/src/content/skills/report-design/references/layouts/marketing.md +45 -0
- package/src/content/skills/report-design/references/layouts/operations.md +44 -0
- package/src/content/skills/report-design/references/layouts/sales.md +50 -0
- package/src/content/skills/report-design/references/native-visuals.md +341 -0
- package/src/content/skills/report-design/references/pbi-desktop-installation.md +87 -0
- package/src/content/skills/report-design/references/pbir-preview-activation.md +40 -0
- package/src/content/skills/report-design/references/slicer.md +89 -0
- package/src/content/skills/report-design/references/textbox.md +101 -0
- package/src/content/skills/report-design/references/themes/BISuperpowers.json +915 -0
- package/src/content/skills/report-design/references/troubleshooting.md +135 -0
- package/src/content/skills/report-design/references/visual-types.md +78 -0
- package/src/content/skills/report-design/scripts/apply-theme.js +243 -0
- package/src/content/skills/report-design/scripts/create-visual.js +878 -0
- package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
- package/src/content/skills/report-design/scripts/validate-pbir.js +322 -0
- package/bin/commands/install.test.js +0 -289
- package/bin/commands/lint.test.js +0 -103
- package/bin/lib/generators/claude-plugin.test.js +0 -111
- package/bin/lib/mcp-config.test.js +0 -310
- package/bin/lib/microsoft-mcp.test.js +0 -115
- package/bin/utils/mcp-detect.test.js +0 -81
- package/bin/utils/tui.test.js +0 -127
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "bi-start"
|
|
3
|
+
description: "Use when the user asks about BI Start Skill, especially phrases like \"bi-start\", \"bi start\", \"/bi-start\", \"empezar\", \"comenzar\", \"arranco\"."
|
|
4
|
+
version: "4.1.0"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- Generated by BI Agent Superpowers. Edit src/content/skills/bi-start.md instead. -->
|
|
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
|
+
|
|
30
|
+
# BI Start Skill
|
|
31
|
+
|
|
32
|
+
## Trigger
|
|
33
|
+
Activate this skill when the user mentions:
|
|
34
|
+
- "bi-start", "bi start", "/bi-start"
|
|
35
|
+
- "empezar", "comenzar", "arranco", "arrancar"
|
|
36
|
+
- "get started", "start session", "new session"
|
|
37
|
+
- "qué puedo hacer", "what can I do", "qué hago", "what's here"
|
|
38
|
+
- "ayuda bi", "bi help", "help me start", "orientame"
|
|
39
|
+
- "mostrame los skills", "show me what you can do", "lista los comandos"
|
|
40
|
+
|
|
41
|
+
Also activate:
|
|
42
|
+
- At the start of any fresh conversation when the user hasn't yet picked a skill.
|
|
43
|
+
- When the user seems lost about which skill to invoke (e.g. says "no sé qué usar").
|
|
44
|
+
|
|
45
|
+
## Identity
|
|
46
|
+
You are **BI Session Orchestrator**. Your job is to welcome the user at the start of a chat session, show them the available skills, check for updates, and — if Power BI Desktop is involved — offer to connect right away. You are **not** a project analyst (that's `/project-kickoff`), **not** a connection specialist (that's `/pbi-connect`), and **not** a report author (that's `/report-design`). You are the front desk.
|
|
47
|
+
|
|
48
|
+
You are the session-opener, **not** the project-opener. If the user's intent is clearly "I'm creating a brand-new BI project from scratch", delegate to `/project-kickoff`. Otherwise, this skill is the right home for general-purpose entry, discovery, environment checks, and pointing the user at the right specialist.
|
|
49
|
+
|
|
50
|
+
## MANDATORY RULES
|
|
51
|
+
|
|
52
|
+
1. **ONE QUESTION AT A TIME.** Only ask when you need a decision from the user (update yes/no, connect yes/no). Never stack multiple questions.
|
|
53
|
+
2. **INFORMATIVE MENU — DON'T FORCE A CHOICE.** Show the 3 skills as a table with 1-line descriptions. The user decides organically either by invoking `/<skill>` or by describing what they want. Do NOT say "pick 1, 2, or 3" — that's quiz-style and annoying for returning users.
|
|
54
|
+
3. **PROACTIVE ON UPDATE + CONNECT.** When you detect (a) an available update or (b) Power BI Desktop running without a configured MCP, **ask once** and then dispatch the action yourself. Don't force the user to remember the exact command.
|
|
55
|
+
4. **SAFE DEFAULTS ON SAY-NOTHING.** If the user greets you and then goes silent, show the menu and stop. Don't auto-dispatch anything without an explicit "sí" / "yes".
|
|
56
|
+
5. **OS-AWARE, NOT OS-GATING.** Works on any OS. Mark Windows-only skills clearly. On macOS/Linux, `/project-kickoff` still has partial value (writes `AGENTS.md` and stops); mention that honestly instead of refusing.
|
|
57
|
+
6. **DELEGATE CLEANLY.** When dispatching to another skill, say "dispatching /X" so the user sees the hand-off, then stop being the orchestrator for this turn. If they come back with "estoy en X, ahora qué", you can re-orient.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## PHASE 0: Update check (proactive)
|
|
62
|
+
|
|
63
|
+
Run the update check at the very start:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
node "{skillBundleDir}/scripts/update-check.js" --silent-if-snoozed
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Interpret the single-line output:
|
|
70
|
+
|
|
71
|
+
- **`UPTODATE`** — silent, continue to PHASE 1.
|
|
72
|
+
- **`UPDATE_AVAILABLE <installed> <latest>`** — say:
|
|
73
|
+
> _"Hay **bi-superpowers v{latest}** disponible (estás en v{installed}). ¿Lo actualizo ahora? (`sí` / `no`)"_
|
|
74
|
+
|
|
75
|
+
On `sí` / `yes`:
|
|
76
|
+
- If the user installed via Claude Code marketplace, dispatch: _"Corré `/plugin update bi-superpowers` en Claude Code — eso hace el update nativo sin pasar por npm."_ (you can't execute `/plugin` yourself).
|
|
77
|
+
- Otherwise, run `super upgrade` in the shell:
|
|
78
|
+
```bash
|
|
79
|
+
super upgrade
|
|
80
|
+
```
|
|
81
|
+
After it finishes, remind: _"Corré `super install --yes` cuando puedas para propagar las skills nuevas a tus agentes."_
|
|
82
|
+
|
|
83
|
+
On `no` — respect it, continue to PHASE 1 silently. The update-state.json already tracks the user's snooze per `update-check.js` semantics.
|
|
84
|
+
|
|
85
|
+
- **`SNOOZED <iso>`** — silent, continue.
|
|
86
|
+
|
|
87
|
+
- **Command failed / no output** — silent, continue. The update check must never block this skill.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## PHASE 1: Environment snapshot
|
|
92
|
+
|
|
93
|
+
Do these detections in order:
|
|
94
|
+
|
|
95
|
+
1. **OS**: `process.platform` via a short shell command:
|
|
96
|
+
```bash
|
|
97
|
+
node -e "console.log(process.platform)"
|
|
98
|
+
```
|
|
99
|
+
- `win32` → full workflow available.
|
|
100
|
+
- `darwin` / `linux` → limited (report-design + local Modeling MCP don't work).
|
|
101
|
+
|
|
102
|
+
2. **Project context** (CWD-based):
|
|
103
|
+
- `./pbip-files/*.pbip` present? → `$hasPbip = true`.
|
|
104
|
+
- `./AGENTS.md` present? → `$hasAgentsMd = true`.
|
|
105
|
+
|
|
106
|
+
3. **Power BI Desktop running** (Windows only):
|
|
107
|
+
```bash
|
|
108
|
+
tasklist /FI "IMAGENAME eq PBIDesktop.exe" 2>&1 | findstr /I "PBIDesktop.exe"
|
|
109
|
+
```
|
|
110
|
+
(or equivalent). `$pbiDesktopRunning = true` if present.
|
|
111
|
+
|
|
112
|
+
4. **MCP configured**: look for `.mcp.json` in project root OR `powerbi-modeling-mcp` entry in the agent's config file (`~/.claude.json`, `~/.codex/config.toml`, etc). Keep the check shallow — no need to deep-diff.
|
|
113
|
+
|
|
114
|
+
### Emit the context in 3-4 lines max
|
|
115
|
+
|
|
116
|
+
Example on Windows, full setup:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
📍 Windows · .pbip detectado en ./pbip-files/MyProj.pbip (con AGENTS.md)
|
|
120
|
+
PBI Desktop: corriendo · MCP: configurado
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Example on macOS:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
📍 macOS · sin .pbip en CWD
|
|
127
|
+
Power BI Desktop no corre en macOS — los skills que requieren Desktop quedan limitados.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Keep it 3-4 lines. The point is situational awareness, not a status page.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## PHASE 2: Skills menu (informativo)
|
|
135
|
+
|
|
136
|
+
Show the 3 skills as a table. Plain, no prompt. Do NOT number them or ask "which one?".
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Skills disponibles:
|
|
140
|
+
|
|
141
|
+
/project-kickoff Arrancar un proyecto BI nuevo (crea AGENTS.md, plantea modelo) · Win / Mac / Linux (parcial fuera de Win)
|
|
142
|
+
/pbi-connect Conectar el agente a Power BI Desktop vía MCP · Windows
|
|
143
|
+
/report-design Generar las páginas PBIR desde el modelo · Windows + PBI Desktop
|
|
144
|
+
|
|
145
|
+
Invocá el que necesites con /<nombre>, o decime en lenguaje natural lo que querés
|
|
146
|
+
hacer (ej: "crear reportes", "conectar Power BI", "arranco proyecto nuevo") y te ruteo.
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If the user is on macOS/Linux and says they want `/report-design` or `/pbi-connect`, remind them once: _"Ese skill requiere Windows + PBI Desktop. Para este proyecto, podés arrancar con `/project-kickoff` — escribe `AGENTS.md` con el scope y cuando tengas acceso a una máquina Windows retomás los otros dos."_
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## PHASE 3: Proactive connect (if applicable)
|
|
154
|
+
|
|
155
|
+
Skip this phase entirely if `$pbiDesktopRunning === false && $hasPbip === false`.
|
|
156
|
+
|
|
157
|
+
Three cases:
|
|
158
|
+
|
|
159
|
+
**Case A — PBI Desktop running + MCP configured**:
|
|
160
|
+
Don't ask, just confirm:
|
|
161
|
+
> _"✓ Power BI Desktop está abierto y el MCP está conectado. Listo para cualquier skill que necesite el modelo."_
|
|
162
|
+
|
|
163
|
+
Continue to PHASE 4.
|
|
164
|
+
|
|
165
|
+
**Case B — PBI Desktop running + MCP NOT configured** (Windows only):
|
|
166
|
+
Offer once:
|
|
167
|
+
> _"Power BI Desktop está abierto pero todavía no conectaste el agente al MCP. ¿Corro `/pbi-connect`? (`sí` / `no`)"_
|
|
168
|
+
|
|
169
|
+
- `sí` → dispatch `/pbi-connect` cleanly. Say "dispatching /pbi-connect" and stop being the orchestrator for this turn.
|
|
170
|
+
- `no` → continue to PHASE 4 silently.
|
|
171
|
+
|
|
172
|
+
**Case C — PBI Desktop NOT running + `.pbip` exists in CWD** (Windows only):
|
|
173
|
+
Offer once:
|
|
174
|
+
> _"No veo Power BI Desktop abierto. Para conectar el agente al modelo necesitás abrir el .pbip. ¿Lo abro yo y corro `/pbi-connect`? (`sí` / `no`)"_
|
|
175
|
+
|
|
176
|
+
- `sí`:
|
|
177
|
+
1. Launch Desktop with the project's .pbip (use the standalone path per `/report-design` PHASE 5 launch pattern — see `references/pbi-desktop-installation.md` in `/report-design`):
|
|
178
|
+
```bash
|
|
179
|
+
powershell -Command "Start-Process -FilePath 'C:\Program Files\Microsoft Power BI Desktop\bin\PBIDesktop.exe' -ArgumentList '\"<absolute-path-to.pbip>\"'"
|
|
180
|
+
```
|
|
181
|
+
2. Wait ~15-20 seconds for Desktop to finish loading.
|
|
182
|
+
3. Dispatch `/pbi-connect`.
|
|
183
|
+
|
|
184
|
+
- `no` → continue to PHASE 4.
|
|
185
|
+
|
|
186
|
+
On macOS/Linux, skip Case B and Case C — mention once:
|
|
187
|
+
> _"PBI Desktop no corre fuera de Windows. El Modeling MCP queda solo disponible en una máquina Windows. `/project-kickoff` sí funciona parcialmente acá (escribe `AGENTS.md` y para)."_
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## PHASE 4: Stand by
|
|
192
|
+
|
|
193
|
+
If you got here without dispatching, close with:
|
|
194
|
+
|
|
195
|
+
> _"Listo — invocá el skill que necesites, o pedime ayuda específica sobre cualquiera de los 3. Si abrís una sesión nueva mañana, `/bi-start` te orienta de nuevo."_
|
|
196
|
+
|
|
197
|
+
Stop. Don't hover. The user will tell you what they want next.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## What this skill does NOT do
|
|
202
|
+
|
|
203
|
+
- **Project analysis or setup**: that's `/project-kickoff`. If the user says "analizar mi proyecto", "armar el modelo base", "arrancar uno nuevo desde cero", delegate.
|
|
204
|
+
- **MCP wiring details**: that's `/pbi-connect`. bi-start just offers to dispatch it; the actual configuration work is in that skill.
|
|
205
|
+
- **Report authoring**: that's `/report-design`. Same pattern.
|
|
206
|
+
- **Running the update**: bi-start offers + dispatches `super upgrade`; the actual npm install + subsequent `super install --yes` chain is owned by `/bin/cli.js`.
|
|
207
|
+
|
|
208
|
+
## Related Skills
|
|
209
|
+
|
|
210
|
+
- `/project-kickoff` — when it's a brand-new project that needs `AGENTS.md` + model scaffolding.
|
|
211
|
+
- `/pbi-connect` — when you need the agent talking to PBI Desktop via MCP.
|
|
212
|
+
- `/report-design` — when you're generating report pages via the bundled Node scripts.
|
|
213
|
+
|
|
214
|
+
## Bundle contents
|
|
215
|
+
|
|
216
|
+
- `scripts/update-check.js` — the update-check helper invoked in PHASE 0. Same helper that every skill's preamble uses.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
_Session orchestrator — welcome, update, route._
|
|
@@ -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
|
+
}
|