baldart 3.10.0 → 3.13.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/CHANGELOG.md +82 -0
- package/README.md +34 -2
- package/VERSION +1 -1
- package/bin/baldart.js +14 -0
- package/framework/.claude/agents/REGISTRY.md +2 -2
- package/framework/.claude/agents/code-reviewer.md +47 -11
- package/framework/.claude/agents/ui-expert.md +355 -43
- package/framework/.claude/commands/design-review.md +7 -0
- package/framework/.claude/skills/design-system-init/SKILL.md +232 -0
- package/framework/.claude/skills/design-system-init/scripts/component-spec.template.md +79 -0
- package/framework/.claude/skills/frontend-design/SKILL.md +124 -0
- package/framework/.claude/skills/motion-design/reference/accessibility-and-modern-apis.md +114 -0
- package/framework/.claude/skills/ui-design/SKILL.md +43 -0
- package/framework/agents/design-review.md +51 -0
- package/framework/agents/design-system-protocol.md +363 -0
- package/framework/agents/index.md +2 -0
- package/framework/docs/UPGRADE-3.12-UI-COHERENCE.md +268 -0
- package/package.json +1 -1
- package/src/commands/configure.js +21 -0
- package/src/commands/doctor.js +54 -2
- package/src/commands/update.js +15 -1
- package/src/commands/version.js +5 -1
- package/src/utils/state.js +27 -0
- package/src/utils/update-notifier.js +181 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Upgrade to v3.12.0 — UI Excellence & Registry Coherence
|
|
2
|
+
|
|
3
|
+
> **Audience**: Claude (or a human operator) working inside a **consumer
|
|
4
|
+
> repo** (any project that installed BALDART via `npx baldart add` and just
|
|
5
|
+
> ran `npx baldart update` to v3.12.0). This document is the alignment
|
|
6
|
+
> walkthrough — it does not replace the CHANGELOG (which describes *what*
|
|
7
|
+
> changed in the framework); it tells you *what to do* in this specific
|
|
8
|
+
> project to align an existing graphic organization with the new discipline.
|
|
9
|
+
>
|
|
10
|
+
> Run this once per project after updating to v3.12.0. It is idempotent —
|
|
11
|
+
> re-running on an already-aligned project produces an empty backlog.
|
|
12
|
+
|
|
13
|
+
## What changed in one paragraph
|
|
14
|
+
|
|
15
|
+
The `ui-expert` agent received a major upgrade (UI states taxonomy,
|
|
16
|
+
Performance Gates, modern CSS, AI-era patterns, 60+ categorized red flags),
|
|
17
|
+
and the registry-first discipline gained a **post-intervention coherence
|
|
18
|
+
gate**: every UI task that introduces, modifies, or surfaces a primitive or
|
|
19
|
+
token must reconcile the registry (`INDEX.md` + `components/<Name>.md` +
|
|
20
|
+
`tokens-reference.md`) **in the same change**. Drift can no longer wait for
|
|
21
|
+
Monday's `ds-drift` routine — it must be resolved at introduction. Three
|
|
22
|
+
layers now enforce coherence: per-task (in `ui-expert` / `ui-design` /
|
|
23
|
+
`frontend-design`) → per-merge (`code-reviewer`) → weekly safety net
|
|
24
|
+
(`ds-drift`). The full text-of-record lives in
|
|
25
|
+
`.framework/framework/agents/design-system-protocol.md` § "Post-Intervention
|
|
26
|
+
Coherence Check".
|
|
27
|
+
|
|
28
|
+
## Step 0 — Should you even run this walkthrough?
|
|
29
|
+
|
|
30
|
+
Run only if all three are true. Otherwise, exit cleanly.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
[ ] baldart.config.yml has features.has_design_system: true
|
|
34
|
+
[ ] paths.design_system points to an existing directory
|
|
35
|
+
[ ] That directory contains at least an INDEX.md
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If `has_design_system: false` → there is nothing to align. The new gates
|
|
39
|
+
short-circuit. Optionally consider bootstrapping a registry via
|
|
40
|
+
`/design-system-init` (skill ships with v3.11.0+).
|
|
41
|
+
|
|
42
|
+
If `has_design_system: true` but `paths.design_system` does not exist or is
|
|
43
|
+
empty → run `/design-system-init` first; come back to this walkthrough
|
|
44
|
+
after.
|
|
45
|
+
|
|
46
|
+
## Step 1 — Surface the current state (no edits yet)
|
|
47
|
+
|
|
48
|
+
Before touching anything, build a snapshot. Do the four scans below and
|
|
49
|
+
emit one finding per item. **Do not fix anything yet** — fixing without a
|
|
50
|
+
baseline produces churn and hides the size of the gap.
|
|
51
|
+
|
|
52
|
+
### 1.1 — Silent primitives (`DS_INDEX_DRIFT` baseline)
|
|
53
|
+
|
|
54
|
+
Goal: find components that exist in the source tree but are NOT documented
|
|
55
|
+
in `INDEX.md`.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# List candidate primitives in the project (adjust glob to match the project)
|
|
59
|
+
find $(yq '.paths.components_primitives' baldart.config.yml) \
|
|
60
|
+
-maxdepth 2 -type f \( -name "*.tsx" -o -name "*.vue" -o -name "*.svelte" \)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For each filename `<Name>.tsx`, grep `INDEX.md` for `<Name>` (case-sensitive).
|
|
64
|
+
Anything not found is a silent primitive. Record name + path + estimated
|
|
65
|
+
broadness (broadly-useful vs. surface-specific). Output:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
SILENT_PRIMITIVES:
|
|
69
|
+
- Badge | components/ui/Badge.tsx | broadly-useful
|
|
70
|
+
- PriceTag | components/marketing/Price.tsx | surface-specific
|
|
71
|
+
...
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 1.2 — Stale specs (`DS_COMPONENT_STALE` baseline)
|
|
75
|
+
|
|
76
|
+
For each `${paths.design_system}/components/<Name>.md`:
|
|
77
|
+
|
|
78
|
+
1. Locate the source file for `<Name>`.
|
|
79
|
+
2. Compare the spec's documented props/variants against the source's actual
|
|
80
|
+
exported types/props. Use `codebase-architect` if the project has LSP
|
|
81
|
+
enabled — it does this cheaply via go-to-definition.
|
|
82
|
+
3. Spec older than the source's last edit in git AND with mismatch on
|
|
83
|
+
props/variants → `DS_COMPONENT_STALE`.
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
STALE_SPECS:
|
|
87
|
+
- Button.md | source mtime 2026-04-12, spec mtime 2026-01-05
|
|
88
|
+
| spec missing variants: "danger-outline", "icon-only"
|
|
89
|
+
| spec missing prop: leftIcon
|
|
90
|
+
...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 1.3 — Hardcoded tokens (`DS_TOKENS_DRIFT` baseline)
|
|
94
|
+
|
|
95
|
+
Scan the codebase for values that bypass `tokens-reference.md`:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Hardcoded hex/rgb in styled code (adjust path to actual sources)
|
|
99
|
+
grep -RInE '#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)' \
|
|
100
|
+
$(yq '.paths.components_root' baldart.config.yml) \
|
|
101
|
+
--include='*.tsx' --include='*.css' --include='*.scss'
|
|
102
|
+
|
|
103
|
+
# Hardcoded box-shadow tuples
|
|
104
|
+
grep -RInE 'box-shadow:\s*[^v;]+;' \
|
|
105
|
+
$(yq '.paths.components_root' baldart.config.yml)
|
|
106
|
+
|
|
107
|
+
# Spacing values outside the scale (heuristic: any odd or > 64 px literal)
|
|
108
|
+
grep -RInE '(padding|margin|gap|inset)[^:]*:\s*[0-9]+px' \
|
|
109
|
+
$(yq '.paths.components_root' baldart.config.yml)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For each hit, classify:
|
|
113
|
+
- **Tokenizable** — exact value matches an existing token → trivial replace
|
|
114
|
+
- **Missing token** — value would belong to the system but no token exists →
|
|
115
|
+
propose adding to `tokens-reference.md`
|
|
116
|
+
- **Justified one-off** — surface-specific value with a documented reason →
|
|
117
|
+
leave + add a code comment citing the exception
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
HARDCODED_TOKENS:
|
|
121
|
+
- tokenizable: 37 hits across 12 files
|
|
122
|
+
- missing-token: 8 hits (suggest 3 new tokens)
|
|
123
|
+
- justified-one-off: 2 hits
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 1.4 — Overlay drift (only if `.baldart/overlays/ui-expert.md` exists)
|
|
127
|
+
|
|
128
|
+
The v3.12.0 base `ui-expert.md` now defines authoritatively things the
|
|
129
|
+
overlay may have previously codified. Likely conflict points:
|
|
130
|
+
|
|
131
|
+
| Overlay section likely to conflict | Why |
|
|
132
|
+
|---|---|
|
|
133
|
+
| Custom "Red Flags" list | Base now has 60+ categorized red flags — overlay items may be duplicates or contradictions |
|
|
134
|
+
| Custom workflow steps | Base now has step 10 (Review) and step 12 (Design) as BLOCKING completion gates — overlay must not skip them |
|
|
135
|
+
| Custom token rules | Base now references the Reference Tables in `design-system-protocol.md` — overlay numeric scales should defer to the protocol |
|
|
136
|
+
| Custom motion guidance | Base now delegates to `motion-design` skill — overlay should point there, not redefine durations/easings |
|
|
137
|
+
|
|
138
|
+
Read `.baldart/overlays/ui-expert.md` and report each section that:
|
|
139
|
+
- Uses `## [OVERRIDE]` marker on a section the base now owns
|
|
140
|
+
authoritatively (red flags, workflow steps, expertise enumeration).
|
|
141
|
+
- Uses `## [APPEND]` to add red flags that are now in the base set
|
|
142
|
+
(duplicates).
|
|
143
|
+
- Hardcodes scales / thresholds / durations the new protocol tables now
|
|
144
|
+
declare canonically.
|
|
145
|
+
|
|
146
|
+
Output:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
OVERLAY_DRIFT:
|
|
150
|
+
- [OVERRIDE] Your Expertise — conflicts with expanded base (consider [APPEND] instead)
|
|
151
|
+
- [APPEND] Red Flags — 4 of 6 items now in base list (deduplicate)
|
|
152
|
+
- hardcoded motion duration 250ms — defer to motion-design timing tables
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 1.5 — Emit the baseline report
|
|
156
|
+
|
|
157
|
+
Save the four sections above as:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
${paths.references_dir:-docs/references}/ds-coherence-baseline-YYYYMMDD.md
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Present a short summary to the user (totals per finding type + estimated
|
|
164
|
+
size of the reconciliation work). **Stop here for user confirmation** before
|
|
165
|
+
proceeding to Step 2.
|
|
166
|
+
|
|
167
|
+
## Step 2 — Prioritize reconciliation
|
|
168
|
+
|
|
169
|
+
Not all findings are equal. Rank them so the user gets quick wins first.
|
|
170
|
+
|
|
171
|
+
| Priority | Criteria | Action |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| **P0** | Silent primitive used in ≥ 3 places AND broadly-useful | Add INDEX entry + scaffold full `components/<Name>.md` spec |
|
|
174
|
+
| **P0** | Hardcoded token (tokenizable) in the design-system entry-point file or in a primitive | Replace with the existing token reference inline |
|
|
175
|
+
| **P1** | Silent primitive used in 1–2 places, broadly-useful | Add INDEX entry + stub spec (`status: needs-review`) |
|
|
176
|
+
| **P1** | Stale spec on a primitive used by ≥ 3 features | Update spec to match source |
|
|
177
|
+
| **P1** | Missing-token (hardcoded value that should be a token) used > 5 times | Propose new token + update `tokens-reference.md` + replace inline |
|
|
178
|
+
| **P2** | Surface-specific silent primitive | Add INDEX entry with `scope: local` + 2-line note |
|
|
179
|
+
| **P2** | Stale spec on a rarely-used primitive | Update spec OR open follow-up card |
|
|
180
|
+
| **P3** | Justified one-off hardcoded value | Add inline code comment citing the exception |
|
|
181
|
+
| **P3** | Overlay-base conflict that does not break the new gates | Refactor overlay to use `[APPEND]` instead of `[OVERRIDE]` |
|
|
182
|
+
|
|
183
|
+
Convert the prioritized findings into the project's backlog format. If
|
|
184
|
+
`features.has_backlog: true`:
|
|
185
|
+
|
|
186
|
+
- One card per P0 item.
|
|
187
|
+
- One card per **group** of P1 items (e.g. "Reconcile 8 stale specs in
|
|
188
|
+
`components/forms/`").
|
|
189
|
+
- One catch-all card for P2/P3 batched cleanup.
|
|
190
|
+
|
|
191
|
+
If no backlog feature → emit a markdown checklist under
|
|
192
|
+
`${paths.references_dir}/ds-coherence-followups-YYYYMMDD.md` and present it
|
|
193
|
+
to the user.
|
|
194
|
+
|
|
195
|
+
## Step 3 — Quick wins (apply now, with user approval)
|
|
196
|
+
|
|
197
|
+
Apply only the items that are **truly trivial and reversible**:
|
|
198
|
+
|
|
199
|
+
- Inline replacement of `tokenizable` hardcoded values (the token already
|
|
200
|
+
exists; the change is a pure substitution).
|
|
201
|
+
- Adding INDEX entries for surface-specific silent primitives (no spec
|
|
202
|
+
needed beyond the scope: local note).
|
|
203
|
+
- Removing overlay `[APPEND]` items that exactly duplicate the new base
|
|
204
|
+
red flags.
|
|
205
|
+
|
|
206
|
+
Group them in **one commit per category** (`chore(ds): replace hardcoded
|
|
207
|
+
tokens with existing references`, `docs(ds): index surface-specific
|
|
208
|
+
primitives`, etc.) so the diff is reviewable.
|
|
209
|
+
|
|
210
|
+
Anything more substantial (new specs, new tokens, overlay refactors, stale
|
|
211
|
+
spec updates) → backlog cards, not inline edits. Resist the urge to "just
|
|
212
|
+
clean it up while you're there" — that turns the alignment into a
|
|
213
|
+
multi-day refactor.
|
|
214
|
+
|
|
215
|
+
## Step 4 — Verify the new gates fire correctly
|
|
216
|
+
|
|
217
|
+
After the quick wins land, pick **one** small UI task that touches a
|
|
218
|
+
primitive (e.g. add a variant to an existing Button) and run it end-to-end:
|
|
219
|
+
|
|
220
|
+
1. Invoke `ui-expert` or the relevant skill.
|
|
221
|
+
2. Make the code change.
|
|
222
|
+
3. Observe that the agent's final output explicitly reports the
|
|
223
|
+
Post-Intervention Coherence Check result — either "no findings" or one
|
|
224
|
+
of the `DS_*_DRIFT` codes.
|
|
225
|
+
|
|
226
|
+
If the agent silently skips this step → the upgrade didn't propagate to
|
|
227
|
+
this project. Check:
|
|
228
|
+
|
|
229
|
+
- `npx baldart status` confirms v3.12.0 installed.
|
|
230
|
+
- `.framework/framework/agents/design-system-protocol.md` contains the
|
|
231
|
+
"Post-Intervention Coherence Check" section.
|
|
232
|
+
- `.framework/framework/.claude/agents/ui-expert.md` has step 10 (Review)
|
|
233
|
+
and step 12 (Design).
|
|
234
|
+
|
|
235
|
+
If any of those is stale → `npx baldart update` again, then re-test.
|
|
236
|
+
|
|
237
|
+
## Step 5 — Establish the rhythm
|
|
238
|
+
|
|
239
|
+
Going forward, the operator (you, or future Claude instances on this
|
|
240
|
+
project):
|
|
241
|
+
|
|
242
|
+
- Treats the coherence check at end-of-task as **non-negotiable**, same
|
|
243
|
+
category as "tests pass". A UI task without the coherence-check report
|
|
244
|
+
is incomplete.
|
|
245
|
+
- Treats `DS_*_DRIFT` findings surfaced by `code-reviewer` as merge
|
|
246
|
+
blockers — they indicate the upstream check was skipped.
|
|
247
|
+
- Reads the weekly `ds-drift` routine report when it lands and treats any
|
|
248
|
+
finding there as a **process bug** (somewhere the upstream gates were
|
|
249
|
+
bypassed), not just a content fix.
|
|
250
|
+
|
|
251
|
+
## Reference points (resolved by `npx baldart update`)
|
|
252
|
+
|
|
253
|
+
After update, these files exist locally and are authoritative:
|
|
254
|
+
|
|
255
|
+
| Concern | File |
|
|
256
|
+
|---|---|
|
|
257
|
+
| What "coherence check" means + decision matrix | `.framework/framework/agents/design-system-protocol.md` § "Post-Intervention Coherence Check" |
|
|
258
|
+
| Numeric reference tables (type / contrast / spacing / density) | `.framework/framework/agents/design-system-protocol.md` § "Reference Tables (embed)" |
|
|
259
|
+
| Token cascade primitive → semantic → component | `.framework/framework/agents/design-system-protocol.md` § "Token Cascade" |
|
|
260
|
+
| Upgraded `ui-expert` (states taxonomy, red flags, gates) | `.framework/framework/.claude/agents/ui-expert.md` |
|
|
261
|
+
| Code-reviewer rule 8 (merge gate) | `.framework/framework/.claude/agents/code-reviewer.md` § "Design System Compliance" rule 8 |
|
|
262
|
+
| `ui-design` Step H (design-time gate) | `.framework/framework/.claude/skills/ui-design/SKILL.md` |
|
|
263
|
+
| Performance Gates (CWV 2026, LCP/INP/CLS) | `.framework/framework/.claude/skills/frontend-design/SKILL.md` § "Performance Gates" |
|
|
264
|
+
| Motion + reduced-motion + View Transitions | `.framework/framework/.claude/skills/motion-design/reference/accessibility-and-modern-apis.md` |
|
|
265
|
+
| Weekly safety net | `.framework/framework/routines/ds-drift.routine.yml` |
|
|
266
|
+
|
|
267
|
+
The framework files are symlinked, so reading them via the paths above
|
|
268
|
+
gets the live, current version.
|
package/package.json
CHANGED
|
@@ -522,6 +522,27 @@ async function interactivePrompts(merged, detected) {
|
|
|
522
522
|
merged.features[key] = await promptForKey(question, proposedDefault, 'confirm');
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
// ---- Design-system bootstrap hint (since v3.11.0) ---------------------
|
|
526
|
+
// If the user said YES to has_design_system but autodetect found no path
|
|
527
|
+
// (i.e. they WANT a registry but it doesn't exist yet), surface the
|
|
528
|
+
// bootstrap skill so they don't end up with a flag that points at nothing.
|
|
529
|
+
// If they said NO, plant a soft pointer for the future.
|
|
530
|
+
if (merged.features.has_design_system === true && !detected.paths.design_system) {
|
|
531
|
+
UI.info(
|
|
532
|
+
'No design-system path was detected on disk. After configure, run\n' +
|
|
533
|
+
' /design-system-init (or invoke the skill)\n' +
|
|
534
|
+
'to scaffold INDEX.md + tokens-reference.md + per-component specs at\n' +
|
|
535
|
+
'docs/design-system/. The registry-first protocol (ui-expert, ui-design,\n' +
|
|
536
|
+
'frontend-design, /design-review) needs these files to enforce coherence.'
|
|
537
|
+
);
|
|
538
|
+
} else if (merged.features.has_design_system === false) {
|
|
539
|
+
UI.info(
|
|
540
|
+
'has_design_system=false — UI work will rely only on ui_guidelines.md.\n' +
|
|
541
|
+
'When you want visual coherence + component reuse enforced project-wide,\n' +
|
|
542
|
+
'run /design-system-init to scaffold a registry from your existing code.'
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
525
546
|
// Path prompts — only ask for paths whose feature is enabled and value missing
|
|
526
547
|
UI.section('Paths (autodetected — confirm or override)');
|
|
527
548
|
const pathPrompts = [
|
package/src/commands/doctor.js
CHANGED
|
@@ -31,6 +31,8 @@ const UI = require('../utils/ui');
|
|
|
31
31
|
const State = require('../utils/state');
|
|
32
32
|
const Hooks = require('../utils/hooks');
|
|
33
33
|
const LspInstaller = require('../utils/lsp-installer');
|
|
34
|
+
const UpdateNotifier = require('../utils/update-notifier');
|
|
35
|
+
const cliPackageJson = require('../../package.json');
|
|
34
36
|
|
|
35
37
|
const FRAMEWORK_DIR = '.framework';
|
|
36
38
|
const REPO_DEFAULT = 'antbald/BALDART';
|
|
@@ -146,6 +148,8 @@ async function detectState(cwd, opts = {}) {
|
|
|
146
148
|
const git = new GitUtils(cwd);
|
|
147
149
|
const state = {
|
|
148
150
|
cwd,
|
|
151
|
+
cliVersion: cliPackageJson.version,
|
|
152
|
+
cliUpdate: UpdateNotifier.hint({ currentVersion: cliPackageJson.version }),
|
|
149
153
|
isGitRepo: await git.isGitRepo(),
|
|
150
154
|
frameworkPresent: await git.frameworkExists(),
|
|
151
155
|
legacyLayout: false,
|
|
@@ -188,6 +192,19 @@ async function detectState(cwd, opts = {}) {
|
|
|
188
192
|
? `v${ledger.last_pushed_version} (${(ledger.last_push_date || '').slice(0, 10) || '—'})`
|
|
189
193
|
: null;
|
|
190
194
|
|
|
195
|
+
// Ledger drift detection (v3.13.0+): the framework on disk says one thing,
|
|
196
|
+
// the ledger says another. Caused by interrupted updates, manual commits
|
|
197
|
+
// that excluded state.json, or filesystem write failures the old silent
|
|
198
|
+
// catch in update.js used to hide. Self-healable via `reconcile-state-ledger`.
|
|
199
|
+
state.stateLedgerDrift = null;
|
|
200
|
+
if (state.frameworkVersion && ledger.installed_version &&
|
|
201
|
+
state.frameworkVersion !== ledger.installed_version) {
|
|
202
|
+
state.stateLedgerDrift = {
|
|
203
|
+
payload: state.frameworkVersion,
|
|
204
|
+
ledger: ledger.installed_version
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
191
208
|
state.remote = await describeRemote(git, ledger.framework_repo || REPO_DEFAULT, !!opts.offline);
|
|
192
209
|
state.commitsAhead = await commitsAheadOfRemote(git);
|
|
193
210
|
state.workingTreeDirty = await localFrameworkChanges(git);
|
|
@@ -313,6 +330,28 @@ function planActions(state) {
|
|
|
313
330
|
// Drift is non-blocking — keep checking for other actions after.
|
|
314
331
|
}
|
|
315
332
|
|
|
333
|
+
if (state.stateLedgerDrift) {
|
|
334
|
+
actions.push({
|
|
335
|
+
key: 'reconcile-state-ledger',
|
|
336
|
+
label: `Reconcile state ledger (${state.stateLedgerDrift.ledger} → ${state.stateLedgerDrift.payload})`,
|
|
337
|
+
why: `.framework/VERSION is v${state.stateLedgerDrift.payload} but .baldart/state.json records installed_version=${state.stateLedgerDrift.ledger}. Typical causes: an update flow that exited mid-way (configure declined, stash conflict), a manual commit that excluded .baldart/state.json, or a write failure that the old silent catch in update.js used to hide (fixed in v3.13.0).`,
|
|
338
|
+
autoOk: true, // pure local file write, idempotent, fully reversible
|
|
339
|
+
run: () => {
|
|
340
|
+
const res = State.reconcileInstalledVersion({
|
|
341
|
+
to: state.stateLedgerDrift.payload,
|
|
342
|
+
reason: 'doctor reconcile — payload/ledger drift'
|
|
343
|
+
});
|
|
344
|
+
if (res.changed) {
|
|
345
|
+
UI.success(`Ledger updated: installed_version ${res.from} → ${res.to}`);
|
|
346
|
+
UI.info('History entry "ledger-reconcile" recorded for audit. Commit .baldart/state.json when ready.');
|
|
347
|
+
} else {
|
|
348
|
+
UI.info('No change needed — ledger already matches payload.');
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
// Drift is non-blocking — keep checking for other actions after.
|
|
353
|
+
}
|
|
354
|
+
|
|
316
355
|
if (state.editGateRegistered === false) {
|
|
317
356
|
actions.push({
|
|
318
357
|
key: 'register-edit-gate',
|
|
@@ -394,6 +433,17 @@ function renderDiagnostic(state) {
|
|
|
394
433
|
console.log(`Repository: ${state.cwd}`);
|
|
395
434
|
console.log();
|
|
396
435
|
|
|
436
|
+
// CLI version line — always shown (independent of git repo state). When a
|
|
437
|
+
// newer baldart npm package was detected on a previous run, surface it here
|
|
438
|
+
// so the doctor table is the single pane of glass for "what's stale".
|
|
439
|
+
console.log(statusLine(
|
|
440
|
+
'CLI',
|
|
441
|
+
state.cliUpdate
|
|
442
|
+
? `v${state.cliVersion} → v${state.cliUpdate.latest} available (npm i -g baldart@latest)`
|
|
443
|
+
: `v${state.cliVersion}`,
|
|
444
|
+
state.cliUpdate ? 'warn' : 'ok'
|
|
445
|
+
));
|
|
446
|
+
|
|
397
447
|
console.log(statusLine('Git repo', state.isGitRepo ? 'yes' : 'no', state.isGitRepo ? 'ok' : 'err'));
|
|
398
448
|
|
|
399
449
|
if (!state.isGitRepo) return;
|
|
@@ -429,9 +479,11 @@ function renderDiagnostic(state) {
|
|
|
429
479
|
console.log(statusLine(
|
|
430
480
|
'State ledger',
|
|
431
481
|
state.statePresent
|
|
432
|
-
?
|
|
482
|
+
? (state.stateLedgerDrift
|
|
483
|
+
? `drift: ledger=v${state.stateLedgerDrift.ledger}, payload=v${state.stateLedgerDrift.payload}`
|
|
484
|
+
: `installed v${state.stateInstalled} (${fmtDate(state.stateInstallDate)})`)
|
|
433
485
|
: 'not seeded',
|
|
434
|
-
state.statePresent ? 'ok' : 'warn'
|
|
486
|
+
state.stateLedgerDrift ? 'warn' : (state.statePresent ? 'ok' : 'warn')
|
|
435
487
|
));
|
|
436
488
|
|
|
437
489
|
if (state.stateLastUpdate) {
|
package/src/commands/update.js
CHANGED
|
@@ -246,7 +246,21 @@ async function update(options = {}) {
|
|
|
246
246
|
|
|
247
247
|
newVersion = await git.getFrameworkVersion();
|
|
248
248
|
UI.success(`New version: ${newVersion}`);
|
|
249
|
-
|
|
249
|
+
// Update the ledger and VERIFY the write landed. Prior to v3.13.0 this
|
|
250
|
+
// call used a silent catch — write failures (permissions, EBUSY) were
|
|
251
|
+
// hidden, and the user only noticed the drift weeks later via doctor.
|
|
252
|
+
// Now: visible warning + always-actionable next step.
|
|
253
|
+
try {
|
|
254
|
+
const after = State.recordUpdate({ from: currentVersion, to: newVersion });
|
|
255
|
+
if (after.installed_version !== newVersion) {
|
|
256
|
+
UI.warning(`Ledger write returned installed_version=${after.installed_version}, expected ${newVersion}.`);
|
|
257
|
+
UI.info('Run `baldart doctor` to reconcile the ledger.');
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
UI.warning(`Could not write .baldart/state.json: ${err.message}`);
|
|
261
|
+
UI.info(`Framework on disk is at v${newVersion} but the ledger still records v${currentVersion}.`);
|
|
262
|
+
UI.info('Run `baldart doctor` to reconcile, or check filesystem permissions on .baldart/state.json.');
|
|
263
|
+
}
|
|
250
264
|
|
|
251
265
|
// Re-apply the pre-update stash (if any). Conflicts mean we leave the
|
|
252
266
|
// stash intact and tell the user how to resolve — never silently drop.
|
package/src/commands/version.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const GitUtils = require('../utils/git');
|
|
2
2
|
const UI = require('../utils/ui');
|
|
3
3
|
const State = require('../utils/state');
|
|
4
|
+
const UpdateNotifier = require('../utils/update-notifier');
|
|
4
5
|
|
|
5
6
|
const REPO_DEFAULT = 'antbald/BALDART';
|
|
6
7
|
const FRAMEWORK_DIR = '.framework';
|
|
@@ -48,6 +49,7 @@ async function version(opts = {}) {
|
|
|
48
49
|
const frameworkVersion = await git.getFrameworkVersion();
|
|
49
50
|
const state = State.load();
|
|
50
51
|
const cliVersion = require('../../package.json').version;
|
|
52
|
+
const cliUpdate = UpdateNotifier.hint({ currentVersion: cliVersion });
|
|
51
53
|
|
|
52
54
|
// Optionally fetch upstream to compute drift (default: yes).
|
|
53
55
|
const offline = opts.offline === true;
|
|
@@ -75,7 +77,9 @@ async function version(opts = {}) {
|
|
|
75
77
|
`Last update: ${fmtDate(state.last_update_date)}`,
|
|
76
78
|
`Last push: v${state.last_pushed_version || '—'} ${state.last_push_date ? `(${fmtDate(state.last_push_date)})` : ''}`.trim(),
|
|
77
79
|
'',
|
|
78
|
-
|
|
80
|
+
cliUpdate
|
|
81
|
+
? `CLI: v${cliVersion} → v${cliUpdate.latest} available (npm i -g baldart@latest)`
|
|
82
|
+
: `CLI: v${cliVersion}`,
|
|
79
83
|
`Repository: https://github.com/${state.framework_repo || REPO_DEFAULT}`,
|
|
80
84
|
].filter((l) => l !== ''));
|
|
81
85
|
UI.newline();
|
package/src/utils/state.js
CHANGED
|
@@ -106,6 +106,32 @@ function recordUpdate({ from, to }, cwd = process.cwd()) {
|
|
|
106
106
|
return state;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Reconcile the ledger's installed_version with the actual framework version
|
|
111
|
+
* on disk (read from .framework/VERSION). Used by `baldart doctor` when it
|
|
112
|
+
* detects a mismatch between the payload and the ledger — typically caused by
|
|
113
|
+
* an interrupted update flow, a manual commit that excluded state.json, or a
|
|
114
|
+
* filesystem write failure that the silent catch in update.js used to hide.
|
|
115
|
+
*
|
|
116
|
+
* Idempotent: calling with the already-current version is a no-op (no history
|
|
117
|
+
* entry produced). History entry uses event "ledger-reconcile" so the source
|
|
118
|
+
* of the bump is auditable separately from real updates.
|
|
119
|
+
*/
|
|
120
|
+
function reconcileInstalledVersion({ to, reason }, cwd = process.cwd()) {
|
|
121
|
+
const state = load(cwd);
|
|
122
|
+
const from = state.installed_version;
|
|
123
|
+
if (from === to) return { state, changed: false };
|
|
124
|
+
state.installed_version = to;
|
|
125
|
+
state.history = appendHistory(state, {
|
|
126
|
+
event: 'ledger-reconcile',
|
|
127
|
+
from,
|
|
128
|
+
to,
|
|
129
|
+
note: reason || 'reconcile ledger with .framework/VERSION'
|
|
130
|
+
});
|
|
131
|
+
save(state, cwd);
|
|
132
|
+
return { state, changed: true, from, to };
|
|
133
|
+
}
|
|
134
|
+
|
|
109
135
|
function recordPush({ from, to, description }, cwd = process.cwd()) {
|
|
110
136
|
const state = load(cwd);
|
|
111
137
|
state.last_pushed_version = to;
|
|
@@ -137,6 +163,7 @@ module.exports = {
|
|
|
137
163
|
recordInstall,
|
|
138
164
|
recordUpdate,
|
|
139
165
|
recordPush,
|
|
166
|
+
reconcileInstalledVersion,
|
|
140
167
|
readFrameworkVersion,
|
|
141
168
|
STATE_FILE,
|
|
142
169
|
STATE_VERSION,
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI self-update notifier (v3.13.0+).
|
|
3
|
+
*
|
|
4
|
+
* The framework payload inside `.framework/` is updated by `baldart update`.
|
|
5
|
+
* The CLI itself (this package) is updated by `npm i -g baldart@latest`.
|
|
6
|
+
* Until now those two layers drifted silently — users on an old CLI never
|
|
7
|
+
* learned new flags, new doctor checks, new adapters had shipped.
|
|
8
|
+
*
|
|
9
|
+
* This module surfaces CLI drift WITHOUT auto-installing anything. It is
|
|
10
|
+
* deliberately non-invasive:
|
|
11
|
+
*
|
|
12
|
+
* - Best-effort HTTP check against the npm registry with a short timeout.
|
|
13
|
+
* - Result cached at `~/.baldart/cli-update-check.json` (TTL 24h).
|
|
14
|
+
* - Banner printed from the cache on the NEXT invocation (zero latency on
|
|
15
|
+
* every run). First-ever run shows nothing — that's intentional.
|
|
16
|
+
* - Suppressed in CI, non-TTY, --offline, NODE_ENV=test, or via
|
|
17
|
+
* BALDART_NO_UPDATE_CHECK=1.
|
|
18
|
+
* - Never blocks the process: the background refresh is fire-and-forget,
|
|
19
|
+
* its timer/socket are unref'd so they cannot hold the event loop open.
|
|
20
|
+
*
|
|
21
|
+
* Design rationale (see CHANGELOG v3.13.0): we explicitly DO NOT run
|
|
22
|
+
* `npm i -g baldart@latest` automatically. Auto-updates inside a per-project
|
|
23
|
+
* CLI invocation would (a) impact every other project on the user's machine,
|
|
24
|
+
* (b) potentially require sudo, (c) break determinism in CI runs. The banner
|
|
25
|
+
* gives the user the choice; the cache means they don't pay for it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const os = require('os');
|
|
31
|
+
|
|
32
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/baldart/latest';
|
|
33
|
+
const CACHE_DIR = path.join(os.homedir(), '.baldart');
|
|
34
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'cli-update-check.json');
|
|
35
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
36
|
+
const DEFAULT_TIMEOUT_MS = 1500;
|
|
37
|
+
|
|
38
|
+
function isSuppressed({ offline } = {}) {
|
|
39
|
+
if (offline) return true;
|
|
40
|
+
if (process.env.BALDART_NO_UPDATE_CHECK === '1') return true;
|
|
41
|
+
if (process.env.CI === 'true' || process.env.CI === '1') return true;
|
|
42
|
+
if (process.env.NODE_ENV === 'test') return true;
|
|
43
|
+
// `process.stdout.isTTY` is `true` when attached to a terminal and
|
|
44
|
+
// `undefined` (NOT `false`) when not — so we must test for "not strictly
|
|
45
|
+
// true" rather than "=== false", otherwise piped/redirected runs silently
|
|
46
|
+
// bypass suppression.
|
|
47
|
+
if (!(process.stdout && process.stdout.isTTY === true)) return true;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readCache() {
|
|
52
|
+
try {
|
|
53
|
+
if (!fs.existsSync(CACHE_FILE)) return null;
|
|
54
|
+
const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
|
55
|
+
if (!data || typeof data !== 'object') return null;
|
|
56
|
+
return data;
|
|
57
|
+
} catch (_) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeCache(data) {
|
|
63
|
+
try {
|
|
64
|
+
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
65
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2) + '\n');
|
|
66
|
+
} catch (_) {
|
|
67
|
+
// Best-effort: a read-only HOME or denied write is not worth surfacing.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cmpSemver(a, b) {
|
|
72
|
+
// Returns 1 if a>b, -1 if a<b, 0 if equal. Strips prerelease and build
|
|
73
|
+
// metadata; only X.Y.Z is compared. That is enough for a "is there a newer
|
|
74
|
+
// stable on npm?" prompt — anything fancier belongs in `semver`, and we are
|
|
75
|
+
// deliberately staying dep-free here.
|
|
76
|
+
if (!a || !b) return 0;
|
|
77
|
+
const norm = (v) => String(v).split(/[-+]/)[0].split('.').map((n) => Number(n) || 0);
|
|
78
|
+
const pa = norm(a);
|
|
79
|
+
const pb = norm(b);
|
|
80
|
+
for (let i = 0; i < 3; i++) {
|
|
81
|
+
const x = pa[i] || 0, y = pb[i] || 0;
|
|
82
|
+
if (x > y) return 1;
|
|
83
|
+
if (x < y) return -1;
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function fetchLatest(timeoutMs) {
|
|
89
|
+
if (typeof fetch !== 'function') return null; // Node < 18 fallback — node engine constraint covers this, but defensive.
|
|
90
|
+
const ctrl = new AbortController();
|
|
91
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
92
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(REGISTRY_URL, {
|
|
95
|
+
signal: ctrl.signal,
|
|
96
|
+
headers: { Accept: 'application/json' },
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) return null;
|
|
99
|
+
const body = await res.json();
|
|
100
|
+
return body && body.version ? String(body.version) : null;
|
|
101
|
+
} catch (_) {
|
|
102
|
+
return null;
|
|
103
|
+
} finally {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function refresh({ currentVersion, timeoutMs = DEFAULT_TIMEOUT_MS, force = false } = {}) {
|
|
109
|
+
const cached = readCache();
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
if (!force && cached && cached.checked_at) {
|
|
112
|
+
const age = now - new Date(cached.checked_at).getTime();
|
|
113
|
+
if (Number.isFinite(age) && age < DEFAULT_TTL_MS) return cached;
|
|
114
|
+
}
|
|
115
|
+
const latest = await fetchLatest(timeoutMs);
|
|
116
|
+
if (!latest) return cached; // Keep stale cache rather than zeroing it.
|
|
117
|
+
const data = {
|
|
118
|
+
checked_at: new Date().toISOString(),
|
|
119
|
+
latest,
|
|
120
|
+
current_at_check: currentVersion,
|
|
121
|
+
};
|
|
122
|
+
writeCache(data);
|
|
123
|
+
return data;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hint({ currentVersion } = {}) {
|
|
127
|
+
if (!currentVersion) return null;
|
|
128
|
+
const cached = readCache();
|
|
129
|
+
if (!cached || !cached.latest) return null;
|
|
130
|
+
if (cmpSemver(cached.latest, currentVersion) <= 0) return null;
|
|
131
|
+
return {
|
|
132
|
+
latest: cached.latest,
|
|
133
|
+
current: currentVersion,
|
|
134
|
+
checked_at: cached.checked_at,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function printBanner(info) {
|
|
139
|
+
if (!info) return;
|
|
140
|
+
const chalk = require('chalk');
|
|
141
|
+
const line1 = chalk.yellow('↑') + ' ' + chalk.bold(`baldart ${info.latest}`) +
|
|
142
|
+
chalk.gray(` available (you have ${info.current})`);
|
|
143
|
+
const line2 = chalk.gray(' Update with: ') + chalk.cyan('npm i -g baldart@latest');
|
|
144
|
+
const line3 = chalk.gray(' Suppress with: BALDART_NO_UPDATE_CHECK=1');
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(line1);
|
|
147
|
+
console.log(line2);
|
|
148
|
+
console.log(line3);
|
|
149
|
+
console.log('');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function backgroundCheck({ currentVersion, offline = false } = {}) {
|
|
153
|
+
// Fire-and-forget. Returns immediately; never throws; cannot block exit.
|
|
154
|
+
if (isSuppressed({ offline })) return;
|
|
155
|
+
refresh({ currentVersion }).catch(() => {});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Cheap entry point for bin/baldart.js: print the banner if a cached update
|
|
160
|
+
* is known, then kick off a refresh for next time. No-op when suppressed.
|
|
161
|
+
*/
|
|
162
|
+
function announceAndRefresh({ currentVersion, offline = false } = {}) {
|
|
163
|
+
if (isSuppressed({ offline })) return null;
|
|
164
|
+
const info = hint({ currentVersion });
|
|
165
|
+
if (info) printBanner(info);
|
|
166
|
+
backgroundCheck({ currentVersion, offline });
|
|
167
|
+
return info;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
isSuppressed,
|
|
172
|
+
refresh,
|
|
173
|
+
hint,
|
|
174
|
+
printBanner,
|
|
175
|
+
backgroundCheck,
|
|
176
|
+
announceAndRefresh,
|
|
177
|
+
// Exported for tests / introspection:
|
|
178
|
+
_cmpSemver: cmpSemver,
|
|
179
|
+
_CACHE_FILE: CACHE_FILE,
|
|
180
|
+
_DEFAULT_TTL_MS: DEFAULT_TTL_MS,
|
|
181
|
+
};
|