@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 +13 -0
- package/dist/commands/sync.js +81 -25
- package/dist/util/watchCopy.js +21 -0
- package/package.json +1 -1
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
|
package/dist/commands/sync.js
CHANGED
|
@@ -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.
|
|
54
|
-
//
|
|
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
|
-
|
|
66
|
-
|
|
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(`
|
|
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('
|
|
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
|
-
|
|
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()}
|
|
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('
|
|
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 (
|
|
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 (
|
|
824
|
+
if (runOpts.watchMode) {
|
|
825
|
+
progress.stop();
|
|
826
|
+
}
|
|
827
|
+
else if (result.failed === 0) {
|
|
775
828
|
progress.succeed(`Downloaded ${downloaded} (${formatBytes(result.bytesIn)})`);
|
|
776
|
-
|
|
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
|
-
|
|
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 (
|
|
1053
|
+
if (announceErrors) {
|
|
998
1054
|
console.log();
|
|
999
|
-
console.log(kleur.yellow(`! could not
|
|
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
|
+
}
|