@magicpixelart/cli 0.5.9 → 0.5.11
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 +21 -0
- package/dist/commands/connect.js +7 -2
- package/dist/commands/push.js +5 -3
- package/dist/commands/search.js +7 -2
- package/dist/commands/sync.js +89 -29
- package/dist/util/gameScan.js +30 -4
- package/dist/util/watchCopy.js +21 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.11] — 2026-08-26
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Sprite counting shows a running total** (`Counting sprites in your
|
|
15
|
+
game… 1,247`) while walking the project, and watch reuses that first
|
|
16
|
+
scan so startup does not walk the tree twice.
|
|
17
|
+
|
|
18
|
+
## [0.5.10] — 2026-08-26
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Watch header shows the working-set size**, not only the last successful
|
|
23
|
+
pull. Connecting 2,000+ game sprites no longer prints `Sprites: 469` when
|
|
24
|
+
`state.json` hasn't caught up yet (`2,147 in your game · 469 last pulled
|
|
25
|
+
from MagicPixel`).
|
|
26
|
+
- **Watch prints live status** while a tick is in flight (`Checking
|
|
27
|
+
MagicPixel…`, `Comparing N sprites…`, download progress, `Checking your
|
|
28
|
+
game files…`) instead of sitting silent on the header until the tick
|
|
29
|
+
finishes. Idle ticks say `Waiting for edits…`.
|
|
30
|
+
|
|
10
31
|
## [0.5.9] — 2026-08-26
|
|
11
32
|
|
|
12
33
|
### Security
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
|
+
import ora from 'ora';
|
|
2
3
|
import { loadConfig, saveConfig } from '../config.js';
|
|
3
4
|
import { assertSafeGlob } from '../util/security.js';
|
|
4
5
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
5
|
-
import { indexGamePngs, matchConnectGlobs, GAME_INDEX_CAP_HINT, connectCapMessage } from '../util/gameScan.js';
|
|
6
|
+
import { indexGamePngs, matchConnectGlobs, GAME_INDEX_CAP_HINT, connectCapMessage, countingSpritesText } from '../util/gameScan.js';
|
|
6
7
|
import { cmd } from '../util/invoke.js';
|
|
7
8
|
import { runPush } from './push.js';
|
|
8
9
|
/**
|
|
@@ -26,7 +27,11 @@ export async function connectCommand(glob) {
|
|
|
26
27
|
console.log(kleur.yellow(' This folder is not a Unity/Godot/GameMaker project — nothing to ingest from disk.'));
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
|
-
const
|
|
30
|
+
const spinner = ora({ text: countingSpritesText(0), spinner: 'dots' }).start();
|
|
31
|
+
const index = await indexGamePngs(kind, process.cwd(), config.outDir, {
|
|
32
|
+
onProgress: (n) => { spinner.text = countingSpritesText(n); },
|
|
33
|
+
});
|
|
34
|
+
spinner.stop();
|
|
30
35
|
if (index.capped) {
|
|
31
36
|
console.log(kleur.yellow(`! ${GAME_INDEX_CAP_HINT}`));
|
|
32
37
|
}
|
package/dist/commands/push.js
CHANGED
|
@@ -19,7 +19,7 @@ import { assetDiskPathFromKey, walkOutDirPngs } from '../util/paths.js';
|
|
|
19
19
|
import { applyDiskFingerprints, indexSyncedBySourceRel, planPush, resolveSyncedKey } from '../util/pushPlan.js';
|
|
20
20
|
import { cmd } from '../util/invoke.js';
|
|
21
21
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
22
|
-
import { indexGamePngs, matchConnectGlobs, GAME_INDEX_CAP_HINT, connectCapMessage } from '../util/gameScan.js';
|
|
22
|
+
import { indexGamePngs, matchConnectGlobs, GAME_INDEX_CAP_HINT, connectCapMessage, countingSpritesText } from '../util/gameScan.js';
|
|
23
23
|
import { collectSourceRelMap, remapAbsToCloudKeys } from '../util/syncPath.js';
|
|
24
24
|
import { assertSafeIoPath } from '../util/security.js';
|
|
25
25
|
/** Parallel hashing / path checks — enough to saturate disk, low enough to stay polite. */
|
|
@@ -42,11 +42,13 @@ export async function runPush(opts = {}) {
|
|
|
42
42
|
export async function runPushWith(config, state, opts = {}) {
|
|
43
43
|
const quiet = opts.quiet === true;
|
|
44
44
|
const empty = { created: 0, updated: 0, unchanged: 0, conflict: 0, error: 0, imported: 0 };
|
|
45
|
-
const spinner = quiet ? null : ora(
|
|
45
|
+
const spinner = quiet ? null : ora(countingSpritesText(0)).start();
|
|
46
46
|
const kind = await detectProjectKind();
|
|
47
47
|
const index = opts.gameIndex
|
|
48
48
|
?? (isEngineKind(kind)
|
|
49
|
-
? await indexGamePngs(kind, process.cwd(), config.outDir
|
|
49
|
+
? await indexGamePngs(kind, process.cwd(), config.outDir, {
|
|
50
|
+
onProgress: spinner ? (n) => { spinner.text = countingSpritesText(n); } : undefined,
|
|
51
|
+
})
|
|
50
52
|
: { files: [], capped: false });
|
|
51
53
|
const matched = matchConnectGlobs(index, config.connect ?? []);
|
|
52
54
|
const sourceByKey = collectSourceRelMap(matched.entries, state.synced);
|
package/dist/commands/search.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
|
+
import ora from 'ora';
|
|
2
3
|
import { loadConfig } from '../config.js';
|
|
3
4
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
4
|
-
import { indexGamePngs, searchGameIndex, GAME_INDEX_CAP_HINT } from '../util/gameScan.js';
|
|
5
|
+
import { indexGamePngs, searchGameIndex, GAME_INDEX_CAP_HINT, countingSpritesText } from '../util/gameScan.js';
|
|
5
6
|
import { cmd } from '../util/invoke.js';
|
|
6
7
|
const PRINT_CAP = 50;
|
|
7
8
|
/**
|
|
@@ -25,7 +26,11 @@ export async function searchCommand(query) {
|
|
|
25
26
|
catch {
|
|
26
27
|
// Config is optional for search — skip MagicPixel outDir when missing.
|
|
27
28
|
}
|
|
28
|
-
const
|
|
29
|
+
const spinner = ora({ text: countingSpritesText(0), spinner: 'dots' }).start();
|
|
30
|
+
const index = await indexGamePngs(kind, process.cwd(), outDir, {
|
|
31
|
+
onProgress: (n) => { spinner.text = countingSpritesText(n); },
|
|
32
|
+
});
|
|
33
|
+
spinner.stop();
|
|
29
34
|
if (index.capped) {
|
|
30
35
|
console.log(kleur.yellow(`! ${GAME_INDEX_CAP_HINT}`));
|
|
31
36
|
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -12,7 +12,7 @@ import { createLimit } from '../util/limit.js';
|
|
|
12
12
|
import { emitTypedIndex, ensureAgentsDoc, scanDiskAssets } from '../util/emitIndex.js';
|
|
13
13
|
import { assertPathInsideRoot, assertSafeIoPath } from '../util/security.js';
|
|
14
14
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
15
|
-
import { indexGamePngs, matchConnectGlobs, GAME_INDEX_CAP_HINT, connectCapMessage } from '../util/gameScan.js';
|
|
15
|
+
import { indexGamePngs, matchConnectGlobs, GAME_INDEX_CAP_HINT, connectCapMessage, countingSpritesText } from '../util/gameScan.js';
|
|
16
16
|
import { collectSourceRelMap, isPathInside, syncDiskPathFromKey } from '../util/syncPath.js';
|
|
17
17
|
import { DEFAULT_UNITY_PPU, writeMissingUnityMetas } from '../util/unityMeta.js';
|
|
18
18
|
import { filterUnityManifest } from '../util/unityFilter.js';
|
|
@@ -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: countingSpritesText(0), spinner: 'dots' }).start();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* header is cosmetic */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const header = await loadWatchHeader(countSpinner ? (n) => { countSpinner.text = countingSpritesText(n); } : undefined);
|
|
72
|
+
if (countSpinner) {
|
|
73
|
+
if (header.line)
|
|
74
|
+
countSpinner.succeed(header.line.trim());
|
|
75
|
+
else
|
|
76
|
+
countSpinner.stop();
|
|
77
|
+
}
|
|
78
|
+
else if (header.line && !opts.quiet) {
|
|
79
|
+
console.log(kleur.dim(header.line));
|
|
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, gameIndex: header.gameIndex });
|
|
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) {
|
|
@@ -222,6 +242,7 @@ async function watchLoop(opts) {
|
|
|
222
242
|
}
|
|
223
243
|
}
|
|
224
244
|
finally {
|
|
245
|
+
header.gameIndex = undefined;
|
|
225
246
|
inFlight = false;
|
|
226
247
|
}
|
|
227
248
|
};
|
|
@@ -248,7 +269,7 @@ async function watchLoop(opts) {
|
|
|
248
269
|
await tick();
|
|
249
270
|
}
|
|
250
271
|
if (!opts.quiet)
|
|
251
|
-
console.log(kleur.dim('
|
|
272
|
+
console.log(kleur.dim('Stopped watching.'));
|
|
252
273
|
// Preserve any non-zero exit code set by a failed tick — don't mask a
|
|
253
274
|
// download failure with a clean exit just because the user pressed Ctrl+C.
|
|
254
275
|
process.exit(process.exitCode ?? 0);
|
|
@@ -309,12 +330,35 @@ export function classifyTickError(err, state) {
|
|
|
309
330
|
}
|
|
310
331
|
return { kind: 'other', nextBackoffSec: Math.min(state.backoffSec * 2, 60) };
|
|
311
332
|
}
|
|
333
|
+
async function loadWatchHeader(onProgress) {
|
|
334
|
+
try {
|
|
335
|
+
const config = await loadConfig();
|
|
336
|
+
const state = await loadState();
|
|
337
|
+
const lastPulled = state.assets ? Object.keys(state.assets).length : 0;
|
|
338
|
+
let workingSet = 0;
|
|
339
|
+
let gameIndex;
|
|
340
|
+
const kind = await detectProjectKind();
|
|
341
|
+
if (isEngineKind(kind) && (config.connect?.length ?? 0) > 0) {
|
|
342
|
+
gameIndex = await indexGamePngs(kind, process.cwd(), config.outDir, { onProgress });
|
|
343
|
+
workingSet = matchConnectGlobs(gameIndex, config.connect).total;
|
|
344
|
+
}
|
|
345
|
+
else if (state.synced) {
|
|
346
|
+
workingSet = Object.keys(state.synced).length;
|
|
347
|
+
}
|
|
348
|
+
return { line: formatWatchSpriteLine({ workingSet, lastPulled }), gameIndex };
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return { line: null };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
312
354
|
async function runOnce(opts, runOpts = {}) {
|
|
313
355
|
const config = await loadConfig();
|
|
314
356
|
const state = await loadState();
|
|
315
357
|
const startedAt = new Date().toISOString();
|
|
316
358
|
const concurrency = Math.max(1, Math.min(opts.concurrency ?? 6, 16));
|
|
317
359
|
const verbose = !opts.quiet && !runOpts.watchMode;
|
|
360
|
+
const live = !opts.quiet;
|
|
361
|
+
const onStatus = runOpts.onStatus;
|
|
318
362
|
const shouldPrune = opts.prune !== false; // commander: --no-prune sets false
|
|
319
363
|
if (verbose && config.endpoint) {
|
|
320
364
|
console.log(kleur.yellow(`! using custom endpoint: ${config.endpoint}`));
|
|
@@ -324,6 +368,8 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
324
368
|
const spinner = verbose
|
|
325
369
|
? ora(since ? `Fetching manifest since ${humanTime(since)}…` : 'Fetching manifest…').start()
|
|
326
370
|
: null;
|
|
371
|
+
if (!spinner)
|
|
372
|
+
onStatus?.(since ? 'Checking MagicPixel for edits…' : 'Fetching your sprites from MagicPixel…');
|
|
327
373
|
let manifest;
|
|
328
374
|
try {
|
|
329
375
|
manifest = await fetchAllManifest(config, since);
|
|
@@ -359,10 +405,14 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
359
405
|
// Unity projects sync only the artboards flagged "Sync to Unity" in the
|
|
360
406
|
// editor (parity with the in-app sync button). `unitySyncAll: true` in
|
|
361
407
|
// magicpixel.json opts back into everything.
|
|
408
|
+
onStatus?.('Looking through your game sprites…');
|
|
362
409
|
const projectKind = await detectProjectKind();
|
|
363
|
-
const gameIndex =
|
|
364
|
-
|
|
365
|
-
|
|
410
|
+
const gameIndex = runOpts.gameIndex
|
|
411
|
+
?? (isEngineKind(projectKind)
|
|
412
|
+
? await indexGamePngs(projectKind, process.cwd(), config.outDir, {
|
|
413
|
+
onProgress: (n) => onStatus?.(`Looking through your game sprites… ${n.toLocaleString('en-US')}`),
|
|
414
|
+
})
|
|
415
|
+
: { files: [], capped: false });
|
|
366
416
|
const connected = matchConnectGlobs(gameIndex, config.connect ?? []);
|
|
367
417
|
const sourceByKey = collectSourceRelMap(connected.entries, state.synced);
|
|
368
418
|
if (verbose && gameIndex.capped) {
|
|
@@ -424,7 +474,7 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
424
474
|
bytesSaved: 0,
|
|
425
475
|
renamed: [],
|
|
426
476
|
};
|
|
427
|
-
await maybePushLocalSprites(config.push, verbose, gameIndex);
|
|
477
|
+
await maybePushLocalSprites(config.push, verbose, gameIndex, live && !!runOpts.watchMode);
|
|
428
478
|
return empty;
|
|
429
479
|
}
|
|
430
480
|
const skipped = manifest.length - filtered.entries.length;
|
|
@@ -460,6 +510,9 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
460
510
|
// the dominant wall-clock cost of an incremental sync. We cache each result
|
|
461
511
|
// so the download loop can reuse it for `If-None-Match` ETags instead of
|
|
462
512
|
// re-hashing the same file moments later.
|
|
513
|
+
onStatus?.(manifest.length > 0
|
|
514
|
+
? `Comparing ${manifest.length.toLocaleString('en-US')} sprite${manifest.length === 1 ? '' : 's'} with files on disk…`
|
|
515
|
+
: 'Checking MagicPixel…');
|
|
463
516
|
const diffLimit = createLimit(concurrency);
|
|
464
517
|
const shaByEntryId = new Map();
|
|
465
518
|
const toDownload = [];
|
|
@@ -679,7 +732,8 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
679
732
|
bytesSaved,
|
|
680
733
|
};
|
|
681
734
|
let progress = null;
|
|
682
|
-
if (
|
|
735
|
+
if (live && toDownload.length > 0) {
|
|
736
|
+
onStatus?.(`Pulling ${toDownload.length.toLocaleString('en-US')} updated sprite${toDownload.length === 1 ? '' : 's'}…`);
|
|
683
737
|
progress = ora({ text: progressText(0, toDownload.length, 0), spinner: 'dots' }).start();
|
|
684
738
|
}
|
|
685
739
|
const run = createLimit(concurrency);
|
|
@@ -771,10 +825,15 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
771
825
|
})));
|
|
772
826
|
const downloaded = result.added.length + result.modified.length;
|
|
773
827
|
if (progress) {
|
|
774
|
-
if (
|
|
828
|
+
if (runOpts.watchMode) {
|
|
829
|
+
progress.stop();
|
|
830
|
+
}
|
|
831
|
+
else if (result.failed === 0) {
|
|
775
832
|
progress.succeed(`Downloaded ${downloaded} (${formatBytes(result.bytesIn)})`);
|
|
776
|
-
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
777
835
|
progress.warn(`Downloaded ${downloaded}, failed ${result.failed} (${formatBytes(result.bytesIn)})`);
|
|
836
|
+
}
|
|
778
837
|
}
|
|
779
838
|
// Unity: write pixel-art-correct `.meta` sidecars so sprites import with
|
|
780
839
|
// point filtering / no compression instead of Unity's blurry defaults —
|
|
@@ -982,21 +1041,22 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
982
1041
|
if (renamed.length > 0)
|
|
983
1042
|
printRenames(renamed, { withHints: true });
|
|
984
1043
|
}
|
|
985
|
-
|
|
1044
|
+
onStatus?.('Checking your game files…');
|
|
1045
|
+
await maybePushLocalSprites(config.push, verbose, gameIndex, live && !!runOpts.watchMode);
|
|
986
1046
|
if (result.failed)
|
|
987
1047
|
process.exitCode = 1;
|
|
988
1048
|
return result;
|
|
989
1049
|
}
|
|
990
|
-
async function maybePushLocalSprites(pushEnabled, verbose, gameIndex) {
|
|
1050
|
+
async function maybePushLocalSprites(pushEnabled, verbose, gameIndex, announceErrors = verbose) {
|
|
991
1051
|
if (pushEnabled === false)
|
|
992
1052
|
return null;
|
|
993
1053
|
try {
|
|
994
1054
|
return await runPush({ quiet: !verbose, gameIndex });
|
|
995
1055
|
}
|
|
996
1056
|
catch (e) {
|
|
997
|
-
if (
|
|
1057
|
+
if (announceErrors) {
|
|
998
1058
|
console.log();
|
|
999
|
-
console.log(kleur.yellow(`! could not
|
|
1059
|
+
console.log(kleur.yellow(`! could not update MagicPixel from your game files: ${e.message}`));
|
|
1000
1060
|
console.log(kleur.dim(` Fix: run \`${cmd('push')}\` once the connection is healthy.`));
|
|
1001
1061
|
}
|
|
1002
1062
|
return null;
|
package/dist/util/gameScan.js
CHANGED
|
@@ -94,8 +94,30 @@ function toEntry(abs, cwdAbs, assetRoot) {
|
|
|
94
94
|
return null;
|
|
95
95
|
return { abs, sourceRel, adoptRel, key: adopted.path.join('/') };
|
|
96
96
|
}
|
|
97
|
+
/** Spinner / status copy while walking the game tree. */
|
|
98
|
+
export function countingSpritesText(found) {
|
|
99
|
+
return found > 0
|
|
100
|
+
? `Counting sprites in your game… ${found.toLocaleString('en-US')}`
|
|
101
|
+
: 'Counting sprites in your game…';
|
|
102
|
+
}
|
|
103
|
+
function bindScanProgress(onProgress) {
|
|
104
|
+
if (!onProgress)
|
|
105
|
+
return () => { };
|
|
106
|
+
let lastAt = 0;
|
|
107
|
+
let lastN = 0;
|
|
108
|
+
return (n) => {
|
|
109
|
+
if (n === lastN)
|
|
110
|
+
return;
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
if (lastN === 0 || now - lastAt >= 80) {
|
|
113
|
+
lastAt = now;
|
|
114
|
+
lastN = n;
|
|
115
|
+
onProgress(n);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
97
119
|
/** Walk the engine tree and return index entries. Does not copy files. */
|
|
98
|
-
export async function indexGamePngs(kind, cwd = process.cwd(), outDir = '') {
|
|
120
|
+
export async function indexGamePngs(kind, cwd = process.cwd(), outDir = '', opts = {}) {
|
|
99
121
|
const empty = { files: [], capped: false };
|
|
100
122
|
if (!isEngineKind(kind))
|
|
101
123
|
return empty;
|
|
@@ -107,7 +129,10 @@ export async function indexGamePngs(kind, cwd = process.cwd(), outDir = '') {
|
|
|
107
129
|
const assetRoot = namedRoot ? resolveChildDir(cwd, namedRoot) : null;
|
|
108
130
|
const found = [];
|
|
109
131
|
const io = createLimit(SCAN_WALK_CONCURRENCY);
|
|
110
|
-
|
|
132
|
+
const report = bindScanProgress(opts.onProgress);
|
|
133
|
+
await walkPngs(scanRoot, scanRoot, destNorm, found, MAX_GAME_INDEX + 1, io, report);
|
|
134
|
+
if (found.length > 0)
|
|
135
|
+
opts.onProgress?.(found.length);
|
|
111
136
|
found.sort((a, b) => a.localeCompare(b));
|
|
112
137
|
const capped = found.length > MAX_GAME_INDEX;
|
|
113
138
|
if (capped)
|
|
@@ -136,7 +161,7 @@ export function searchGameIndex(index, query) {
|
|
|
136
161
|
return [];
|
|
137
162
|
return index.files.filter((e) => e.sourceRel.toLowerCase().includes(q) || e.key.includes(q) || e.adoptRel.toLowerCase().includes(q));
|
|
138
163
|
}
|
|
139
|
-
async function walkPngs(dir, root, destNorm, out, limit, io) {
|
|
164
|
+
async function walkPngs(dir, root, destNorm, out, limit, io, onFound) {
|
|
140
165
|
if (out.length >= limit)
|
|
141
166
|
return;
|
|
142
167
|
let entries;
|
|
@@ -169,9 +194,10 @@ async function walkPngs(dir, root, destNorm, out, limit, io) {
|
|
|
169
194
|
}
|
|
170
195
|
else if (ent.isFile() && ent.name.toLowerCase().endsWith('.png')) {
|
|
171
196
|
out.push(full);
|
|
197
|
+
onFound?.(out.length);
|
|
172
198
|
}
|
|
173
199
|
}
|
|
174
200
|
if (subdirs.length === 0 || out.length >= limit)
|
|
175
201
|
return;
|
|
176
|
-
await Promise.all(subdirs.map((d) => io(() => walkPngs(d, root, destNorm, out, limit, io))));
|
|
202
|
+
await Promise.all(subdirs.map((d) => io(() => walkPngs(d, root, destNorm, out, limit, io, onFound))));
|
|
177
203
|
}
|
|
@@ -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
|
+
}
|