@magicpixelart/cli 0.5.9 → 0.5.10

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 CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.10] — 2026-08-26
11
+
12
+ ### Changed
13
+
14
+ - **Watch header shows the working-set size**, not only the last successful
15
+ pull. Connecting 2,000+ game sprites no longer prints `Sprites: 469` when
16
+ `state.json` hasn't caught up yet (`2,147 in your game · 469 last pulled
17
+ from MagicPixel`).
18
+ - **Watch prints live status** while a tick is in flight (`Checking
19
+ MagicPixel…`, `Comparing N sprites…`, download progress, `Checking your
20
+ game files…`) instead of sitting silent on the header until the tick
21
+ finishes. Idle ticks say `Waiting for edits…`.
22
+
10
23
  ## [0.5.9] — 2026-08-26
11
24
 
12
25
  ### Security
@@ -23,6 +23,7 @@ import { maxIsoTimestamp } from '../util/iso.js';
23
23
  import { formatBytes } from '../util/format.js';
24
24
  import { computePreviousKeyOrphans } from '../util/previousKeyOrphans.js';
25
25
  import { cmd } from '../util/invoke.js';
26
+ import { formatWatchSpriteLine } from '../util/watchCopy.js';
26
27
  import { runPush } from './push.js';
27
28
  export async function syncCommand(opts) {
28
29
  // Sweep any leaked `<file>.<pid>.<hex>.tmp` files left behind by a prior
@@ -50,20 +51,33 @@ async function watchLoop(opts) {
50
51
  // Commander already validated the numeric value (2–3600); the `?? 2`
51
52
  // covers the bare boolean `-w` form.
52
53
  const intervalSec = typeof opts.watch === 'string' ? parseInt(opts.watch, 10) : 2;
53
- // Header — print once on start. Project/asset count is best-effort; if the
54
- // first manifest fetch fails we still want the loop to come up and retry.
55
- let headerCount = null;
56
- try {
57
- const state = await loadState();
58
- headerCount = state.assets ? Object.keys(state.assets).length : null;
59
- }
60
- catch {
61
- // ignore — header is cosmetic
62
- }
54
+ // Header — print once on start. Counts are best-effort; if the first
55
+ // manifest fetch fails we still want the loop to come up and retry.
63
56
  console.log(kleur.bold('👀 MagicPixel watching for changes…'));
64
57
  console.log(` Edit at: ${kleur.cyan('https://magicpixel.art')}`);
65
- if (headerCount !== null)
66
- console.log(kleur.dim(` Sprites: ${headerCount}`));
58
+ let countSpinner = null;
59
+ if (!opts.quiet) {
60
+ try {
61
+ const config = await loadConfig();
62
+ const kind = await detectProjectKind();
63
+ if (isEngineKind(kind) && (config.connect?.length ?? 0) > 0) {
64
+ countSpinner = ora({ text: 'Counting sprites in your game…', spinner: 'dots' }).start();
65
+ }
66
+ }
67
+ catch {
68
+ /* header is cosmetic */
69
+ }
70
+ }
71
+ const spriteLine = await loadWatchSpriteLine();
72
+ if (countSpinner) {
73
+ if (spriteLine)
74
+ countSpinner.succeed(spriteLine.trim());
75
+ else
76
+ countSpinner.stop();
77
+ }
78
+ else if (spriteLine && !opts.quiet) {
79
+ console.log(kleur.dim(spriteLine));
80
+ }
67
81
  console.log(kleur.dim(` Polling: every ${intervalSec}s (slows when idle) · Stop: Ctrl+C`));
68
82
  console.log();
69
83
  let stopping = false;
@@ -106,12 +120,12 @@ async function watchLoop(opts) {
106
120
  wakeStop();
107
121
  process.stdout.write('\x1b[2K\r');
108
122
  if (inFlight) {
109
- console.log(kleur.dim(`[watch] finishing current sync… (${signal} again to force quit)`));
123
+ console.log(kleur.dim(`Still finishing this check… (${signal} again to stop now)`));
110
124
  process.once(signal, () => process.exit(signal === 'SIGINT' ? 130 : 143));
111
125
  }
112
126
  else {
113
127
  if (!opts.quiet)
114
- console.log(kleur.dim('[watch] stopped.'));
128
+ console.log(kleur.dim('Stopped watching.'));
115
129
  // Preserve any exit code already set by a prior failed tick.
116
130
  process.exit(process.exitCode ?? 0);
117
131
  }
@@ -122,8 +136,14 @@ async function watchLoop(opts) {
122
136
  if (inFlight || stopping)
123
137
  return;
124
138
  inFlight = true;
139
+ const onStatus = (msg) => {
140
+ if (opts.quiet)
141
+ return;
142
+ process.stdout.write(`\r\x1b[2K${kleur.dim(`${timestamp()} ${msg}`)}`);
143
+ };
125
144
  try {
126
- const r = await runOnce({ ...opts, watch: false }, { watchMode: true });
145
+ onStatus('Checking MagicPixel…');
146
+ const r = await runOnce({ ...opts, watch: false }, { watchMode: true, onStatus });
127
147
  // Reset backoff on any successful tick. Resume message fires once when
128
148
  // we recover from an auth pause — `getApiKey()` re-reads
129
149
  // .magicpixel/credentials on every call, so a `magicpixel login` in
@@ -165,7 +185,7 @@ async function watchLoop(opts) {
165
185
  printChanges(r, /* indent */ ' ');
166
186
  }
167
187
  else {
168
- process.stdout.write(`\r\x1b[2K${kleur.dim(`${timestamp()} no changes (${r.unchanged} unchanged)`)}`);
188
+ process.stdout.write(`\r\x1b[2K${kleur.dim(`${timestamp()} Waiting for edits… (${r.unchanged} up to date)`)}`);
169
189
  }
170
190
  }
171
191
  catch (e) {
@@ -248,7 +268,7 @@ async function watchLoop(opts) {
248
268
  await tick();
249
269
  }
250
270
  if (!opts.quiet)
251
- console.log(kleur.dim('[watch] stopped.'));
271
+ console.log(kleur.dim('Stopped watching.'));
252
272
  // Preserve any non-zero exit code set by a failed tick — don't mask a
253
273
  // download failure with a clean exit just because the user pressed Ctrl+C.
254
274
  process.exit(process.exitCode ?? 0);
@@ -309,12 +329,35 @@ export function classifyTickError(err, state) {
309
329
  }
310
330
  return { kind: 'other', nextBackoffSec: Math.min(state.backoffSec * 2, 60) };
311
331
  }
332
+ async function loadWatchSpriteLine() {
333
+ try {
334
+ const config = await loadConfig();
335
+ const state = await loadState();
336
+ const lastPulled = state.assets ? Object.keys(state.assets).length : 0;
337
+ let workingSet = 0;
338
+ const kind = await detectProjectKind();
339
+ if (isEngineKind(kind) && (config.connect?.length ?? 0) > 0) {
340
+ const index = await indexGamePngs(kind, process.cwd(), config.outDir);
341
+ const matched = matchConnectGlobs(index, config.connect);
342
+ workingSet = matched.total;
343
+ }
344
+ else if (state.synced) {
345
+ workingSet = Object.keys(state.synced).length;
346
+ }
347
+ return formatWatchSpriteLine({ workingSet, lastPulled });
348
+ }
349
+ catch {
350
+ return null;
351
+ }
352
+ }
312
353
  async function runOnce(opts, runOpts = {}) {
313
354
  const config = await loadConfig();
314
355
  const state = await loadState();
315
356
  const startedAt = new Date().toISOString();
316
357
  const concurrency = Math.max(1, Math.min(opts.concurrency ?? 6, 16));
317
358
  const verbose = !opts.quiet && !runOpts.watchMode;
359
+ const live = !opts.quiet;
360
+ const onStatus = runOpts.onStatus;
318
361
  const shouldPrune = opts.prune !== false; // commander: --no-prune sets false
319
362
  if (verbose && config.endpoint) {
320
363
  console.log(kleur.yellow(`! using custom endpoint: ${config.endpoint}`));
@@ -324,6 +367,8 @@ async function runOnce(opts, runOpts = {}) {
324
367
  const spinner = verbose
325
368
  ? ora(since ? `Fetching manifest since ${humanTime(since)}…` : 'Fetching manifest…').start()
326
369
  : null;
370
+ if (!spinner)
371
+ onStatus?.(since ? 'Checking MagicPixel for edits…' : 'Fetching your sprites from MagicPixel…');
327
372
  let manifest;
328
373
  try {
329
374
  manifest = await fetchAllManifest(config, since);
@@ -359,6 +404,7 @@ async function runOnce(opts, runOpts = {}) {
359
404
  // Unity projects sync only the artboards flagged "Sync to Unity" in the
360
405
  // editor (parity with the in-app sync button). `unitySyncAll: true` in
361
406
  // magicpixel.json opts back into everything.
407
+ onStatus?.('Looking through your game sprites…');
362
408
  const projectKind = await detectProjectKind();
363
409
  const gameIndex = isEngineKind(projectKind)
364
410
  ? await indexGamePngs(projectKind, process.cwd(), config.outDir)
@@ -424,7 +470,7 @@ async function runOnce(opts, runOpts = {}) {
424
470
  bytesSaved: 0,
425
471
  renamed: [],
426
472
  };
427
- await maybePushLocalSprites(config.push, verbose, gameIndex);
473
+ await maybePushLocalSprites(config.push, verbose, gameIndex, live && !!runOpts.watchMode);
428
474
  return empty;
429
475
  }
430
476
  const skipped = manifest.length - filtered.entries.length;
@@ -460,6 +506,9 @@ async function runOnce(opts, runOpts = {}) {
460
506
  // the dominant wall-clock cost of an incremental sync. We cache each result
461
507
  // so the download loop can reuse it for `If-None-Match` ETags instead of
462
508
  // re-hashing the same file moments later.
509
+ onStatus?.(manifest.length > 0
510
+ ? `Comparing ${manifest.length.toLocaleString('en-US')} sprite${manifest.length === 1 ? '' : 's'} with files on disk…`
511
+ : 'Checking MagicPixel…');
463
512
  const diffLimit = createLimit(concurrency);
464
513
  const shaByEntryId = new Map();
465
514
  const toDownload = [];
@@ -679,7 +728,8 @@ async function runOnce(opts, runOpts = {}) {
679
728
  bytesSaved,
680
729
  };
681
730
  let progress = null;
682
- if (verbose && toDownload.length > 0) {
731
+ if (live && toDownload.length > 0) {
732
+ onStatus?.(`Pulling ${toDownload.length.toLocaleString('en-US')} updated sprite${toDownload.length === 1 ? '' : 's'}…`);
683
733
  progress = ora({ text: progressText(0, toDownload.length, 0), spinner: 'dots' }).start();
684
734
  }
685
735
  const run = createLimit(concurrency);
@@ -771,10 +821,15 @@ async function runOnce(opts, runOpts = {}) {
771
821
  })));
772
822
  const downloaded = result.added.length + result.modified.length;
773
823
  if (progress) {
774
- if (result.failed === 0)
824
+ if (runOpts.watchMode) {
825
+ progress.stop();
826
+ }
827
+ else if (result.failed === 0) {
775
828
  progress.succeed(`Downloaded ${downloaded} (${formatBytes(result.bytesIn)})`);
776
- else
829
+ }
830
+ else {
777
831
  progress.warn(`Downloaded ${downloaded}, failed ${result.failed} (${formatBytes(result.bytesIn)})`);
832
+ }
778
833
  }
779
834
  // Unity: write pixel-art-correct `.meta` sidecars so sprites import with
780
835
  // point filtering / no compression instead of Unity's blurry defaults —
@@ -982,21 +1037,22 @@ async function runOnce(opts, runOpts = {}) {
982
1037
  if (renamed.length > 0)
983
1038
  printRenames(renamed, { withHints: true });
984
1039
  }
985
- await maybePushLocalSprites(config.push, verbose, gameIndex);
1040
+ onStatus?.('Checking your game files…');
1041
+ await maybePushLocalSprites(config.push, verbose, gameIndex, live && !!runOpts.watchMode);
986
1042
  if (result.failed)
987
1043
  process.exitCode = 1;
988
1044
  return result;
989
1045
  }
990
- async function maybePushLocalSprites(pushEnabled, verbose, gameIndex) {
1046
+ async function maybePushLocalSprites(pushEnabled, verbose, gameIndex, announceErrors = verbose) {
991
1047
  if (pushEnabled === false)
992
1048
  return null;
993
1049
  try {
994
1050
  return await runPush({ quiet: !verbose, gameIndex });
995
1051
  }
996
1052
  catch (e) {
997
- if (verbose) {
1053
+ if (announceErrors) {
998
1054
  console.log();
999
- console.log(kleur.yellow(`! could not push working-set sprites into MagicPixel: ${e.message}`));
1055
+ console.log(kleur.yellow(`! could not update MagicPixel from your game files: ${e.message}`));
1000
1056
  console.log(kleur.dim(` Fix: run \`${cmd('push')}\` once the connection is healthy.`));
1001
1057
  }
1002
1058
  return null;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Watch-mode header copy. `state.assets` is only the last *successful* pull,
3
+ * so a connected working set of 2k sprites used to show as "Sprites: 469"
4
+ * while catch-up was still pending. Show both numbers when they differ.
5
+ */
6
+ function fmt(n) {
7
+ return n.toLocaleString('en-US');
8
+ }
9
+ /** One header line, or null when we have nothing useful to print. */
10
+ export function formatWatchSpriteLine(c) {
11
+ const set = c.workingSet > 0 ? c.workingSet : 0;
12
+ const pulled = c.lastPulled > 0 ? c.lastPulled : 0;
13
+ if (set === 0 && pulled === 0)
14
+ return null;
15
+ if (set > 0 && pulled > 0 && set !== pulled) {
16
+ return ` Sprites: ${fmt(set)} in your game · ${fmt(pulled)} last pulled from MagicPixel`;
17
+ }
18
+ if (set > 0)
19
+ return ` Sprites: ${fmt(set)} in your working set`;
20
+ return ` Sprites: ${fmt(pulled)}`;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magicpixelart/cli",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
4
4
  "description": "Sync MagicPixel pixel-art assets to your local project.",
5
5
  "type": "module",
6
6
  "bin": {