@magicpixelart/cli 0.3.0 → 0.4.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 ADDED
@@ -0,0 +1,415 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@magicpixelart/cli` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Fixed
11
+ - **`sync --watch 1` silently coerced to 2.** The commander validator
12
+ accepted `1–3600` but the watch loop enforced a 2s floor via
13
+ `Math.max(2, …)`, so `--watch 1` quietly ran at 2s with no warning.
14
+ Validator and loop now agree on `2–3600`; passing `1` fails fast with
15
+ `expected an integer 2–3600 (seconds)`.
16
+ - **`magicpixel start` printed "You're set up ✨" after a failed first
17
+ sync.** `syncCommand` sets `process.exitCode = 1` on download failures
18
+ without throwing — same misleading-success pattern we previously fixed
19
+ in `repair`. `start` now snapshots `process.exitCode` around the first
20
+ sync and prints a yellow "first sync completed with errors" line
21
+ (pointing at `magicpixel sync` and `magicpixel doctor`) when a fresh
22
+ non-zero exit code is set. The watch-script hint still prints either
23
+ way so the user knows how to retry.
24
+ - **Watch-tick `(other)` and `(network)` errors lost the request id.**
25
+ Friendly `ApiError` messages put `(request id: …)` on a second line, so
26
+ the `firstLine` we logged for non-auth tick failures dropped it. Both
27
+ branches now suffix the request id when the underlying error is an
28
+ `ApiError` — matching what the auth-pause branch has always done.
29
+ - **`whoami` could print a negative asset count** if a malformed server
30
+ response returned `count: -5`. Shape guard now clamps to `Math.max(0, …)`
31
+ and floors to an integer.
32
+
33
+ ### Internal
34
+ - New `reportAndExit(err, command, exitCode)` in `util/telemetry.ts`.
35
+ Collapses the duplicated lazy-import + best-effort-config ceremony that
36
+ used to live in both `wrap()` (index.ts, exit 1) and the watch
37
+ give-up branch (sync.ts, exit 2). One source of truth for the
38
+ fork-endpoint guard and any future flush-pending-spans hook.
39
+
40
+ ### Tests
41
+ - 119 → 130 passing. New coverage: `whoami` negative-count clamp, and
42
+ `parseWatchInterval` / `parseConcurrency` validators (extracted to
43
+ `util/flagValidators.ts` so they unit-test in-process without spawning
44
+ a subprocess — bare `-w` boolean form, lower/upper bounds, non-integer
45
+ and out-of-range rejection).
46
+
47
+ ## [0.4.0] — 2026-06-02
48
+
49
+ Production hardening pass. Reliability, self-healing, traceability, and
50
+ significant onboarding polish — no breaking changes for callers on 0.3.x.
51
+
52
+ ### Added
53
+ - **Opt-out CLI error telemetry.** Unexpected failures (5xx server errors and
54
+ uncaught exceptions) are reported fire-and-forget to a new
55
+ `log-cli-error` edge function so they surface on `/admin/errors` alongside
56
+ browser crashes. Authenticated by your API key, never blocking, 2s timeout,
57
+ per-message dedupe. Skipped when no key is configured, when pointed at a
58
+ non-canonical endpoint, or when `MAGICPIXEL_TELEMETRY=0`. User-fixable
59
+ errors (missing config, bad key, ENOENT/EACCES, commander arg errors,
60
+ 401/403/404/429) are filtered out client-side and never reported.
61
+ - **`magicpixel repair`** — one-shot self-healing command. Validates the API
62
+ key, quarantines `state.json`, prunes empty subdirs under `outDir`, and runs
63
+ a full sync. `--dry-run` previews; `--yes` skips the prompt. The dry-run
64
+ output lists the actual paths it would quarantine and the empty subdirs it
65
+ would remove (capped at 10 + “…and N more”). If the underlying sync exits
66
+ non-zero the final line reports “completed with errors” instead of a green
67
+ check.
68
+ - **`magicpixel doctor --json`** — stable, machine-readable diagnostic report
69
+ (no ANSI). Pipeable into `jq` or pasted verbatim into an LLM. Never includes
70
+ the API key, only its source. New `--offline` flag skips the live manifest
71
+ probe for users behind a strict proxy. The `network` field uses a
72
+ discriminated shape: `{ skipped: 'offline' | 'no-api-key' }` when no probe
73
+ ran, `{ ok, status, roundtripMs, requestId, error }` when one did — so JSON
74
+ consumers can branch on `network.skipped` instead of mis-reading a probe
75
+ skip as a network failure.
76
+ - **`magicpixel doctor` actively probes** the manifest endpoint with a 5s
77
+ timeout. Reports HTTP status, roundtrip ms, and the `X-Request-Id` the
78
+ server echoed.
79
+ - **`X-Request-Id` echo end-to-end.** The CLI mints a uuid for every request
80
+ via a shared `authHeaders` helper (one source of truth for the
81
+ `Authorization` / `User-Agent` / `X-Request-Id` triple); the edge function
82
+ echoes it on every response (success + error). 401/403 and other API
83
+ errors now suffix `(request id: …)` so support can grep edge logs in one
84
+ shot.
85
+ - **Friendly EACCES/EPERM/EROFS/EBUSY messages.** Asset writes and
86
+ `state.json` saves surface multi-line, action-oriented diagnostics (“close
87
+ OneDrive”, “chmod -R u+w …”) instead of raw libc errors.
88
+ - **Watch-loop resume message.** After an auth-pause clears (the user ran
89
+ `magicpixel login` in another terminal), the watcher logs
90
+ `✓ Key accepted again — resuming.` instead of silently picking up.
91
+ - **`sync --watch` exits with code `2`** after 5 consecutive 401/403s so
92
+ parent processes (Vite plugin, systemd, pm2) can detect a genuinely
93
+ revoked key rather than a transient blip. A successful tick resets the
94
+ counter. Non-auth failures don’t count toward the streak, so a flaky
95
+ network interleaved with a stale 401 can’t sneak past the threshold.
96
+ - **Per-command `--help` examples** via `commander`’s `addHelpText`. Each
97
+ command ships a 2-line example block ready to paste to an AI agent.
98
+ - **README “Production checklist”** pointing at `doctor`, `repair`,
99
+ `--offline`, watcher exit codes, and `X-Request-Id` grepping.
100
+ - **`AGENTS.md` is always written**, regardless of `emitIndex` — `public/`
101
+ and `static/` users now get a snippet that uses the correct absolute URL
102
+ form (`<img src="/magicpixel/items/tree.png" />`) instead of a broken
103
+ bundler-relative `import` path.
104
+
105
+ ### Fixed
106
+ - **`sync --watch` Ctrl+C waited out the full backoff before exiting.** When
107
+ a signal arrived during the idle sleep the watcher slept up to 60s (the
108
+ error-backoff ceiling) before re-checking `stopping`. The sleep is now
109
+ cancellable: `onStopSignal` calls `wakeStop()` and the loop exits within a
110
+ tick.
111
+ - **`magicpixel whoami` ignored `Retry-After` on 429.** Every other call site
112
+ passed `retryAfterMsFromResponse(res)` to `ApiError`; `whoami` dropped it,
113
+ burning through retries in ~750ms instead of waiting the server-suggested
114
+ window. Now honours the header (helper exported from `api.ts`).
115
+ - **`magicpixel init` patched `package.json` non-atomically.** A crash
116
+ between write and flush could corrupt `package.json`. Now uses the shared
117
+ `atomicWrite` (stage-tmp + rename) helper, same pattern as `state.json`
118
+ and the asset download path.
119
+ - **`[watch] stopped.` printed under `--quiet`.** The termination line now
120
+ honours the quiet contract so CI consumers capturing watcher output get a
121
+ clean stream.
122
+ - **`sync --watch` ignored SIGTERM.** Only `SIGINT` (Ctrl+C) was handled, so
123
+ `kill <pid>`, `docker stop`, systemd, and pm2 killed the watcher mid-sync
124
+ without draining in-flight work or preserving the exit code. Both signals
125
+ now share a single `onStopSignal` handler; second signal force-exits with
126
+ the conventional code (130 for SIGINT, 143 for SIGTERM).
127
+ - **`sync --watch` went silent after the first network blip.** Once backoff
128
+ doubled past 30s the offline message was suppressed entirely, so a user
129
+ returning to the terminal saw no signal at all. The message now prints
130
+ every time backoff changes (and the next successful tick prints a single
131
+ “Back online — resuming.” line, mirroring the auth-recovery pattern).
132
+ - **`magicpixel repair` could mis-report success.** The exit-code comparison
133
+ used raw inequality on `process.exitCode`, so a sync failure that re-set
134
+ it to `1` after a prior `1` looked like success. Now snapshots
135
+ `process.exitCode ?? 0` and compares `>`, honouring any fresh non-zero.
136
+ - **`assertKeyValid` read unbounded error bodies.** A misbehaving server
137
+ returning a multi-MB 500 page would buffer it entirely just to slice 120
138
+ chars. Now capped at 16 KB via `readBodyWithLimit`.
139
+ - **`magicpixel status` falsely reported “key not set”** when the user had
140
+ logged in via `magicpixel login` (file-backed). Now honors the same
141
+ precedence as `getApiKey()` and labels the source.
142
+ - **`magicpixel status` / `whoami` worked only with a project config.** Both
143
+ now fall back to defaults when `magicpixel.json` is missing, so a brand-new
144
+ user can `magicpixel whoami` to validate a key before bothering with init.
145
+ - **`magicpixel start` couldn’t recover from a broken `magicpixel.json`.** It
146
+ used to skip init on `existsSync` and then crash deep inside `syncCommand`.
147
+ It now tries `loadConfig` first and prints the friendly error with a
148
+ `magicpixel init --force` hint when the file is malformed.
149
+ - **`sync --watch` idle backoff was measured in *ticks*, not seconds.** At
150
+ `--watch 10`, the first slowdown waited 15 minutes (90 ticks) and the
151
+ second 50 minutes (300 ticks) instead of the documented ~3 / ~15 min.
152
+ Thresholds are now elapsed-seconds and behave the same at any interval.
153
+ - **`sync --watch` graceful stop overwrote `process.exitCode`.** A failed
154
+ download earlier in the loop set `exitCode = 1`; the post-loop
155
+ `process.exit(0)` then zeroed it out. The watcher now preserves any prior
156
+ exit code on graceful stop and on SIGINT.
157
+ - **304 responses now credit `bytesSaved`.** A steady-state sync (all assets
158
+ unchanged via ETag) used to report `~0 B saved`; it now reports the
159
+ manifest’s reported byte total.
160
+ - **`magicpixel.json` shape validation** at load time. Hand-edited configs
161
+ with wrong types (e.g. `include: "**/*"` instead of `["**/*"]`, or
162
+ `emitIndex: "true"`) surface friendly multi-line errors pointing at the
163
+ offending field instead of cryptic TypeErrors deep in glob matching.
164
+ - **`assertSafeOutDir` rejects absolute paths and `..` segments** at both
165
+ `init` prompt-time and `loadConfig` file-time — single helper, single
166
+ policy. Previously an absolute `outDir` could escape the cwd-relative
167
+ containment check.
168
+ - **`fetchAllManifest` / `fetchAssetBytes` shape-validate the manifest JSON.**
169
+ A malformed edge response (`{items: null}`, non-string `nextCursor`, etc.)
170
+ used to crash with `TypeError: null is not iterable` mid-pagination; now
171
+ surfaces a friendly `ApiError(502, …)` carrying the request id.
172
+ - **`fetchAllManifest` detects stuck cursors in O(1) round-trips.** A buggy
173
+ server that returns the same `nextCursor` twice now aborts immediately
174
+ instead of burning the 200-page budget.
175
+ - **`assertKeyValid` + `whoami` retry transient failures.** A single 503 or
176
+ network blip during onboarding used to kick the user back to “paste your
177
+ key again”, and `whoami` would falsely report “rejected”. Both now retry
178
+ 5xx/429 with `Retry-After` honoring; hard 4xx still bubble immediately.
179
+ - **`magicpixel repair` final line** no longer claims success when the
180
+ underlying `sync` set a non-zero exit code without throwing.
181
+ - **`init` validates outDir at input time** with a re-prompt loop, instead of
182
+ letting `loadConfig` reject it on the next `sync` / `status`.
183
+ - **`maxIsoTimestamp`** requires strict ISO-8601 (matches the edge function’s
184
+ regex), so loose `Date.parse`-able inputs can’t silently rewind `lastSync`.
185
+ - **`sync --watch` default documented correctly.** Help text and README said
186
+ 10s but the actual default is 2s (intentional — perceived-instant UX).
187
+ Docs now reflect 2s + adaptive idle backoff.
188
+ - **`sync -w <bad>` no longer silently coerces to 2s.** Commander now
189
+ validates the watch interval (`InvalidArgumentError` for non-integers and
190
+ out-of-range values, 1–3600s). The bare boolean form (`-w` with no arg)
191
+ still defaults to 2s.
192
+ - **Watcher network-error detection** now also matches `ENETUNREACH`,
193
+ `EHOSTUNREACH`, and `502/504` upstream-gateway text so corporate-proxy
194
+ blips back off correctly instead of being treated as auth failures.
195
+ - **`magicpixel list` size column** now formats bytes as `KB` / `MB` instead
196
+ of dumping raw byte counts (`1234567B`). Shares the same `formatBytes`
197
+ helper as `sync`.
198
+ - **`magicpixel whoami` shape-guards the manifest response** the same way
199
+ `fetchAllManifest` does. A malformed `{ items: null }` body now reports
200
+ "0 assets" with the request id rather than throwing `TypeError`.
201
+ - **`findKeyInDotenv` regex anchored on the exact key name.** Sibling vars
202
+ like `MAGICPIXEL_API_KEY_OLD` or `MAGICPIXEL_API_KEY_BACKUP` no longer
203
+ match and return the wrong value. Also tolerates `export ` prefix and
204
+ inline `# comment` trailing the value.
205
+ - **`magicpixel repair --dry-run`** now predicts the step-2 skip a real
206
+ run would hit on non-TTY without `--yes`, prints a yellow `note`, and
207
+ closes with "would skip the state reset" instead of an unconditional
208
+ green check.
209
+ - **`magicpixel start` no longer instructs users to run an npm script
210
+ they don't have.** When `init` couldn't patch `package.json` the final
211
+ hint falls back to `npx magicpixel sync --watch`. The concurrently tip
212
+ follows the same branch.
213
+ - **`retryTransient` wraps network failures with `{ cause }`** so the
214
+ underlying transport error keeps its stack for debugging.
215
+ - **`validateEndpointUrl` uses an explicit scheme allowlist** (`https:` /
216
+ `http:` for localhost). `file:`, `data:`, `javascript:`, etc. now get a
217
+ clear "scheme not allowed" message instead of the generic HTTPS error.
218
+
219
+ ### Changed
220
+ - **`magicpixel status` Diff vs remote** now parallelizes the per-asset SHA
221
+ check through the shared concurrency pool (matches the `sync` diff loop).
222
+ - **`sync.runOnce` parallel disk-SHA pre-check** using the same
223
+ `createLimit(concurrency)` pool as downloads (default 6 in flight). On a
224
+ 1k-asset all-unchanged sync this is the dominant wall-clock cost.
225
+ - **`sync.runOnce` caches `assetDiskPath` and per-entry SHA** in maps reused
226
+ across the diff / orphan / download loops. Saves three resolves + two
227
+ security asserts per asset and, on the download path, avoids re-hashing
228
+ the same file moments after the pre-check.
229
+ - **`sync` orphan-prune message no longer reads inverted** when `--no-prune`
230
+ is passed.
231
+
232
+ ### Internal
233
+ - New `util/framework.isStaticOutDir` — single predicate for "is outDir
234
+ served as a static asset?", used by `init` (skip typed index) and
235
+ `emitIndex` (emit absolute-URL AGENTS.md snippet). Replaces two copies of
236
+ the same regex.
237
+ - New `nextBackoffForIdle` exported from `sync.ts` — pure helper used by
238
+ the watch loop's idle backoff math; testable in isolation.
239
+ - New `util/authHeaders.ts` — one source of truth for the Authorization +
240
+ User-Agent + X-Request-Id triple (4 call sites collapsed).
241
+ - New `util/iso.ts` — `STRICT_ISO_8601_RE`, `isStrictIso8601`,
242
+ `maxIsoTimestamp` extracted from `sync.ts`.
243
+ - New `util/security.assertSafeOutDir` — single trim+validate helper used by
244
+ both `init` and `loadConfig`.
245
+ - New `util/paths.walkOutDirPngs` — single canonical disk walker for PNGs
246
+ under outDir, replacing the two near-identical implementations that used
247
+ to live in `sync.ts` and `emitIndex.ts`.
248
+ - New `util/paths.listEmptyDirs` — DFS post-order walker shared by
249
+ `pruneEmptyDirs` and `repair --dry-run`. Deletion order is now provably
250
+ safe.
251
+ - `fetchAssetBytes` refactored onto the shared `retryTransient` helper; one
252
+ retry policy now governs both manifest pagination and asset downloads
253
+ (including `Retry-After` parsing).
254
+ - `MAX_GLOB_LEN` exported from `util/security.ts`, imported by `config.ts`.
255
+ - `emitTypedIndex` + `ensureAgentsDoc` use atomic writes (`.tmp` →
256
+ `rename`) so a crash mid-write can never leave a truncated `index.ts` /
257
+ `AGENTS.md` that fails bundler resolves.
258
+ - New `util/format.ts` — `formatBytes` shared by `sync` and `list`; was
259
+ previously a private helper inside `sync.ts`.
260
+ - `start.ts` no longer dynamically imports `node:fs/promises` mid-function.
261
+ - `loadState` corrupt-state quarantine — renames a malformed `state.json`
262
+ to `.corrupt-<ts>` and falls back to a full sync.
263
+ - `_shared/errorHandler.ts` (edge) mints a fallback `X-Request-Id` when the
264
+ caller didn’t pass one through.
265
+
266
+ ### Tests
267
+ - Test suite expanded from 6 to **78** passing. New coverage: manifest
268
+ shape guard, cursor-loop detection, retry semantics (5xx triple-retry,
269
+ 429 Retry-After, network-error wrap, non-retryable 4xx bubble),
270
+ `assertSafeOutDir` (relative / absolute / `..` / null byte),
271
+ `assertSafeAssetSegments`, `validateEndpointUrl` (HTTPS-only, insecure
272
+ escape hatch, no embedded credentials, redirect refusal),
273
+ `assertPathInsideRoot`, `safeFetch` cross-origin redirect,
274
+ `walkOutDirPngs` (dotfile / symlink / cross-root), `listEmptyDirs`,
275
+ `maxIsoTimestamp` strictness, `doctor --json` discriminated `network`
276
+ shape, watcher idle backoff math at multiple intervals (regression
277
+ guard for the ticks-vs-seconds fix), `friendlyFsError` rewrites for
278
+ `EACCES`/`EROFS`/`EBUSY`, corrupt-state quarantine.
279
+
280
+ ## [0.3.3] — 2026-06-01
281
+
282
+ ### Added
283
+ - **Honor `Retry-After` on 429.** When the server rate-limits a download, the
284
+ CLI now waits for the duration the server suggests (capped at 60s) before
285
+ retrying instead of using its own fixed 250ms exponential backoff.
286
+ - **Stale-version nudge.** The CLI reads `X-MagicPixel-Min-CLI-Version` from
287
+ manifest responses and prints a one-time hint when it's running an older
288
+ version, pointing at `npm i -D @magicpixelart/cli@latest`.
289
+
290
+ ## [0.3.2] — 2026-06-01
291
+
292
+ ### Changed
293
+ - **`sync --watch` now backs off when idle.** After ~3 min of nothing-to-do the
294
+ poll relaxes from 2s → 5s, and after ~15 min → 10s, so a dev who walked away
295
+ isn't hammering the manifest endpoint. Any change *or* error snaps it back to
296
+ the fast interval immediately, so the "edit → see it" promise is intact the
297
+ moment you come back. Configurable floor still set by `--watch <seconds>`.
298
+
299
+ ## [0.3.1] — 2026-06-01
300
+
301
+ ### Changed
302
+ - **`sync --watch` polls every 2s by default** (was 10s). Edits in MagicPixel
303
+ now appear in your project within ~2s without you running `magicpixel sync`.
304
+ Incremental polls send `?since=<lastSync>` so empty ticks are a cheap
305
+ no-op manifest round-trip. Pass `--watch 5` (or any value ≥ 2) to slow it
306
+ down if needed.
307
+
308
+
309
+ ## [0.3.0] — 2026-06-01
310
+
311
+ This release rolls in the 0.2.0 polish (see below) and adds a first-run onboarding flow built for people who have never used a CLI.
312
+
313
+ ### Added
314
+ - **`magicpixel start`** — one-command bootstrap. Detects your framework, runs `init` with smart defaults, prompts for an API key, runs a first `sync --full`, then tells you how to start the watcher. The only command we now tell new users to run.
315
+ - **`magicpixel login` / `magicpixel logout`** — store / remove the API key in `.magicpixel/credentials` (mode `0600`). `login` validates the key against the server before saving and re-prompts on rejection instead of crashing.
316
+ - **`magicpixel doctor`** — single-page diagnostic (CLI/Node version, framework, `outDir`, key source, watch-script status, last sync time, last error) you can paste to an AI agent when something breaks.
317
+ - **`.magicpixel/credentials`** — gitignored, mode `0600` API-key store. `MAGICPIXEL_API_KEY` env var still takes precedence (CI keeps working unchanged); `init`/`start` offer to migrate any `MAGICPIXEL_API_KEY` they find in `.env` / `.env.local` so it stops shipping in your bundler.
318
+ - **`AGENTS.md`** — written to the repo root on first successful sync (idempotent — appends a single fenced `## MagicPixel sprites` section if a file already exists). Gives Lovable / Cursor / Claude Code three concrete `import` examples.
319
+ - **AI-agent header in `index.ts`** — generated barrel now opens with a comment block telling agents how to import by key vs by stable id.
320
+
321
+ ### Changed
322
+ - **`sync --watch`** is now self-explanatory and resilient:
323
+ - Header: `👀 MagicPixel watching for changes…` with the edit URL and stop-key.
324
+ - Each non-empty tick is prefixed `[HH:MM:SS] Pulled N changes from MagicPixel:` followed by the per-file list.
325
+ - 401 / 403 stops crashing the loop — prints `Run: magicpixel login` and retries every 30s.
326
+ - Network failures back off (2s → 60s cap) instead of aborting; "MagicPixel is offline or your internet is. Sprites you already have still work."
327
+ - **`getApiKey()`** now reads `MAGICPIXEL_API_KEY` env var first, then `.magicpixel/credentials`. Error message points at `magicpixel login` instead of an `export` snippet.
328
+ - **`init`** uses the shared `util/framework.ts` detector (Vite / Next / Remix / TanStack Start / Astro / Nuxt / SvelteKit / CRA) and falls back to `src/assets/magicpixel` when `src/` exists, otherwise `assets/magicpixel`.
329
+
330
+ ### 0.2.0 polish included in this release
331
+ - `sync` prunes by default; `--no-prune` opts out. The old `--prune` flag is gone.
332
+ - Per-file change summary at the end of every sync and on each non-empty watch tick (`+` added, `~` modified, `↪` renamed, `-` pruned), capped at 50 lines.
333
+ - Rename-resilient imports via `MagicPixelAssetsById` + auto-detected renames printed with old → new key hints and stale-PNG cleanup (even in incremental mode).
334
+ - `index.ts` emitted from the local filesystem (not the manifest) so the barrel can never disagree with what bundlers see on disk.
335
+ - `init` offers to add a `magicpixel:watch` npm script so users discover `sync --watch` without reading the docs.
336
+
337
+
338
+ ### Changed (breaking)
339
+ - **`sync` prunes by default.** Local PNGs no longer present in the manifest are deleted unless you pass the new `--no-prune` flag. The old `--prune` flag is gone; CI scripts that passed it will see an unknown-option error — drop the flag (behavior is now the default). Pruning still only runs on full syncs (no `lastSync`, or `--full`).
340
+ - **Manifest keys now use `<document>/<artboard>`** for every asset, including documents with only one artboard. A document **Items** containing artboards `tree`, `cards`, `rock` now syncs as three separate files (`items/tree.png`, `items/cards.png`, `items/rock.png`) instead of a single `items.png` of the active artboard. Old key shapes (`<library-folder>/<doc-slug>`) become orphan files on the next sync — and are now auto-pruned (see above).
341
+
342
+ ### Added
343
+ - **`init` offers a `magicpixel:watch` npm script** so users discover `sync --watch` (the documented happy path) without reading the docs. Existing scripts of the same name are never overwritten.
344
+ - **Rename-resilient imports via `MagicPixelAssetsById`.** Every emitted `index.ts` now also exposes an id-keyed map:
345
+ ```ts
346
+ import { MagicPixelAssetsById } from '@/assets/magicpixel';
347
+ <img src={MagicPixelAssetsById['abc123…']} /> // survives folder/slug renames
348
+ ```
349
+ - **Rename detection.** When an asset's path changes server-side, `sync` prints the old → new key (and the corresponding stable-id import hint), and auto-prunes the stale PNG even in incremental mode. Detection uses a small `id → key` snapshot persisted alongside `lastSync` in `.magicpixel/state.json`.
350
+ - **Per-file change summary** at the end of every sync (and on each watch tick that produced changes): `+` added, `~` modified, `↪` renamed, `-` pruned. Capped at 50 lines with a `…and N more` tail to keep CI logs readable; suppressed under `--quiet`.
351
+
352
+ ### Changed
353
+ - **`index.ts` is now emitted from the local filesystem**, not from the manifest. Previously a transient server error during the post-sync barrel refetch left `index.ts` silently stale while the PNGs on disk were fresh — a confusing split-brain state. The barrel can no longer disagree with disk.
354
+ - `sync` summary line now breaks `downloaded` into `added` + `modified` so you can tell new assets from updates at a glance.
355
+
356
+
357
+ ## [0.1.2] — 2026-06-01
358
+
359
+ ### Security
360
+ - Reject manifest `folder` / `slug` values that could escape `outDir` (`..`, `\`, null bytes); verify every write and prune stays inside `outDir`.
361
+ - Custom `endpoint` must be HTTPS (local `http://localhost` only with `MAGICPIXEL_ALLOW_INSECURE_ENDPOINT=1`); credentials in the URL are rejected.
362
+ - HTTP fetches follow redirects only within the same origin so an API key cannot be sent to a third party.
363
+ - Cap per-asset download size at 64 MiB; validate `Content-Length` before buffering.
364
+ - Sanitize `If-None-Match` to hex sha256 only; stricter `MAGICPIXEL_API_KEY` format check and whitespace trim.
365
+ - Skip symlinks when scanning local PNGs for orphan detection; random-suffix temp files for atomic writes.
366
+ - `.magicpixel/state.json` written with mode `0600`.
367
+
368
+ ### Changed
369
+ - `whoami` now requests a full manifest page (server-capped at 1000) and reports an honest asset count instead of always showing "1+".
370
+ - `sync --watch` waits for the in-flight sync to drain on Ctrl+C before exiting, so you never leave half-written `*.tmp` files or an unflushed `lastSync`. A second Ctrl+C still hard-quits (exit 130).
371
+ - `emitTypedIndex` is now idempotent — when the generated `index.ts` matches what's already on disk, the file is left untouched. Stops Vite/webpack HMR from reloading on every `--watch` poll.
372
+ - `whoami` now sends the standard `User-Agent` header (was the only command missing it).
373
+ - README documents that both `mp_live_` and `mp_test_` key prefixes are accepted.
374
+ - `magicpixel.json` validates `include` / `exclude` / `outDir` / `endpoint` on load; `add` / `remove` validate glob patterns.
375
+
376
+ ### Fixed
377
+ - `fetchAssetBytes` drains 304 / error response bodies so undici can return sockets to the keep-alive pool (fewer socket warnings on flaky networks).
378
+
379
+
380
+ ## [0.1.1] — 2026-05-30
381
+
382
+ ### Added
383
+ - `sync --watch` now handles `SIGINT` cleanly (clears the in-flight status line and prints `[watch] stopped`).
384
+ - Manifest pagination cycle guard (caps at 200 pages / ~100k assets) so a buggy server cursor can't hang the CLI.
385
+ - Smoke workflow exercises `sync --dry-run` to catch broken release builds before publish.
386
+ - `sync` warns when `magicpixel.json` has a custom `endpoint` configured (catches stray test overrides before they ship to CI).
387
+
388
+ ### Changed
389
+ - Version is now read from `package.json` at runtime (single source of truth for `--version` and `User-Agent`).
390
+ - `--concurrency` rejects non-integer / out-of-range values with a clear error instead of silently falling back to the default.
391
+ - `init` `.gitignore` insertion now treats `.magicpixel`, `.magicpixel/`, `/.magicpixel`, and `/.magicpixel/` as equivalent, so it doesn't append a duplicate marker.
392
+ - README clarifies that `MAGICPIXEL_API_KEY` is env-only by design.
393
+
394
+ ### Fixed
395
+ - `sync` verifies the downloaded payload's sha256 against the manifest before atomic rename — corrupt bodies now retry on next run instead of writing to disk.
396
+ - `emitTypedIndex` uses positional identifiers (`asset_0`, …) so keys differing only by non-alphanumeric chars no longer collide.
397
+ - `emitTypedIndex` dedupes duplicate manifest keys (warns) instead of emitting an invalid TS object literal.
398
+
399
+ ## [0.1.0] — 2026-05-29
400
+
401
+ Initial release.
402
+
403
+ ### Added
404
+ - `init` — interactive setup with framework detection (Vite/Next/Remix/Astro/Nuxt/SvelteKit/CRA), suggests `outDir`, offers typed-index emission and `.gitignore` updates. `-y` for CI.
405
+ - `sync` — incremental manifest fetch (`since=lastSync`), sha256 diff against disk, parallel atomic downloads (`*.tmp` → rename), `If-None-Match` 304 short-circuit, retry on 429/5xx.
406
+ - `--watch [seconds]` — poll and apply changes live (default 10s).
407
+ - `--full`, `--prune`, `--dry-run`, `-c/--concurrency`, `-q/--quiet`.
408
+ - Progress bar + bytes-saved summary.
409
+ - `add` / `remove` — manage include globs.
410
+ - `list` — print matching manifest as a table.
411
+ - `status` — config, lastSync, diff vs remote.
412
+ - `whoami` — verify API key, report visible assets.
413
+ - Typed asset index — when `emitIndex: true`, writes `<outDir>/index.ts` with `MagicPixelAssets` map + `MagicPixelAssetKey` type.
414
+ - Friendly errors with copy-pasteable `Fix:` blocks for missing key, malformed key, missing config, broken JSON, network failures, 401/403/404/429/5xx.
415
+ - `User-Agent: @magicpixelart/cli/0.1.0 (node X)` on all requests for server-side observability.
package/README.md CHANGED
@@ -52,7 +52,22 @@ npx magicpixel sync --watch # keeps assets fresh while you work
52
52
  npx magicpixel doctor
53
53
  ```
54
54
 
55
- Prints a one-screen diagnostic (CLI version, framework, outDir, key source, last sync, last error). Paste it to your AI agent — it's designed to be the only context they need.
55
+ Prints a one-screen diagnostic (CLI version, framework, outDir, key source, last sync, last error, live manifest probe). Paste it to your AI agent — it's designed to be the only context they need.
56
+
57
+ Behind a strict corporate proxy and the live probe times out? Add `--offline` to skip it. Need a machine-readable report? `magicpixel doctor --json | jq` — stable schema, no ANSI.
58
+
59
+ When something's actually broken, `magicpixel repair` runs the full "turn it off and on again" recovery: validates your key, quarantines `state.json`, prunes empty subdirs, and triggers a clean `sync --full`. `--dry-run` previews the exact paths it would touch.
60
+
61
+ ## Production checklist
62
+
63
+ Before shipping or onboarding a teammate, run through:
64
+
65
+ 1. **`magicpixel doctor`** — green network probe + populated key source.
66
+ 2. **`magicpixel repair --dry-run`** — review what a recovery would touch; nothing destructive should be flagged.
67
+ 3. **`magicpixel sync --full --dry-run`** — confirms the plan matches expectations (no surprise orphans/renames).
68
+ 4. **Watcher exit codes** — `sync --watch` exits `2` after 5 consecutive auth failures so your process supervisor (Vite plugin, systemd, pm2) can detect a revoked key. Make sure your supervisor surfaces non-zero exits.
69
+ 5. **`X-Request-Id` correlation** — every API response carries a request id. Friendly errors append `(request id: …)`; paste it in support threads and we can grep the edge logs in one shot.
70
+ 6. **Telemetry opt-out** — unexpected CLI failures (5xx + uncaught exceptions) are reported to `/admin/errors` so we can spot mass breakage without a support ping. Set `MAGICPIXEL_TELEMETRY=0` to disable; the full contract (what's filtered, what's sent, where) is in the [Telemetry](#telemetry) section below.
56
71
 
57
72
 
58
73
  ## Typed asset index (default on)
@@ -82,6 +97,7 @@ No bundler config, no runtime, no extra package.
82
97
  | `login [--key <key>]` | Save your API key to `.magicpixel/credentials` (mode `0600`). Validates against the server first. |
83
98
  | `logout` | Remove the stored API key. |
84
99
  | `doctor` | Print a one-screen diagnostic — paste it to your AI agent when something breaks. |
100
+ | `repair [--dry-run] [-y]` | Self-heal a broken sync: validate key → quarantine `state.json` → prune empty dirs → full re-sync. |
85
101
  | `sync [...flags]` | Fetch manifest, diff against disk, download changed assets, prune orphans. |
86
102
  | `add <glob>` / `remove <glob>` | Manage `include` patterns. |
87
103
  | `list` | Print matching manifest as a table. |
@@ -92,7 +108,7 @@ No bundler config, no runtime, no extra package.
92
108
 
93
109
  | Flag | Meaning |
94
110
  | --- | --- |
95
- | `-w, --watch [seconds]` | Poll for changes (default 10s). Ideal during development. |
111
+ | `-w, --watch [seconds]` | Poll for changes (default **2s**; auto-slows to 5s after ~3min idle, 10s after ~15min). Ideal during development. |
96
112
  | `--no-prune` | Keep local files not in the manifest (default: prune them on full syncs). |
97
113
  | `--dry-run` | Print plan, write nothing. |
98
114
  | `--full` | Ignore `lastSync`; re-fetch the full manifest. |
@@ -170,6 +186,19 @@ Every error message includes a `Fix:` block. Common ones:
170
186
  | `index.ts` doesn't update | Run `sync --full` once; renames may take a full pass to propagate. |
171
187
  | Files keep re-downloading | Your build system is rewriting PNGs. Sync into a dir your bundler reads but doesn't mutate. |
172
188
 
189
+ ## Telemetry
190
+
191
+ Unexpected CLI failures (5xx server errors, uncaught exceptions) are
192
+ reported fire-and-forget to MagicPixel so we can fix issues before users
193
+ have to file them. We send: the error message + stack, command name, CLI
194
+ version, Node version, OS platform, and the request id from the failed
195
+ call. We never send file paths, asset names, configuration, environment
196
+ variables, or your API key.
197
+
198
+ Opt out: `MAGICPIXEL_TELEMETRY=0`. Reporting is also automatically skipped
199
+ when no API key is configured or when `endpoint` in `magicpixel.json` points
200
+ at a non-canonical host.
201
+
173
202
  ## What v1 does NOT do
174
203
 
175
204
  - Layer JSON sync (PNG only)