@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 +415 -0
- package/README.md +31 -2
- package/dist/api.js +173 -54
- package/dist/commands/doctor.js +201 -56
- package/dist/commands/init.js +25 -15
- package/dist/commands/list.js +6 -5
- package/dist/commands/login.js +3 -34
- package/dist/commands/repair.js +157 -0
- package/dist/commands/start.js +56 -14
- package/dist/commands/status.js +41 -13
- package/dist/commands/sync.js +297 -135
- package/dist/commands/whoami.js +52 -12
- package/dist/config.js +88 -20
- package/dist/index.js +38 -23
- package/dist/util/atomicWrite.js +25 -0
- package/dist/util/auth.js +41 -0
- package/dist/util/authHeaders.js +24 -0
- package/dist/util/credentials.js +27 -10
- package/dist/util/emitIndex.js +78 -70
- package/dist/util/errors.js +49 -0
- package/dist/util/flagValidators.js +49 -0
- package/dist/util/format.js +14 -0
- package/dist/util/framework.js +10 -0
- package/dist/util/iso.js +28 -0
- package/dist/util/paths.js +99 -0
- package/dist/util/security.js +39 -2
- package/dist/util/telemetry.js +182 -0
- package/package.json +11 -4
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)
|