@magicpixelart/cli 0.5.10 → 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 CHANGED
@@ -7,6 +7,14 @@ 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
+
10
18
  ## [0.5.10] — 2026-08-26
11
19
 
12
20
  ### 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 index = await indexGamePngs(kind, process.cwd(), config.outDir);
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
  }
@@ -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('Scanning local sprites…').start();
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);
@@ -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 index = await indexGamePngs(kind, process.cwd(), outDir);
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
  }
@@ -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';
@@ -61,22 +61,22 @@ async function watchLoop(opts) {
61
61
  const config = await loadConfig();
62
62
  const kind = await detectProjectKind();
63
63
  if (isEngineKind(kind) && (config.connect?.length ?? 0) > 0) {
64
- countSpinner = ora({ text: 'Counting sprites in your game…', spinner: 'dots' }).start();
64
+ countSpinner = ora({ text: countingSpritesText(0), spinner: 'dots' }).start();
65
65
  }
66
66
  }
67
67
  catch {
68
68
  /* header is cosmetic */
69
69
  }
70
70
  }
71
- const spriteLine = await loadWatchSpriteLine();
71
+ const header = await loadWatchHeader(countSpinner ? (n) => { countSpinner.text = countingSpritesText(n); } : undefined);
72
72
  if (countSpinner) {
73
- if (spriteLine)
74
- countSpinner.succeed(spriteLine.trim());
73
+ if (header.line)
74
+ countSpinner.succeed(header.line.trim());
75
75
  else
76
76
  countSpinner.stop();
77
77
  }
78
- else if (spriteLine && !opts.quiet) {
79
- console.log(kleur.dim(spriteLine));
78
+ else if (header.line && !opts.quiet) {
79
+ console.log(kleur.dim(header.line));
80
80
  }
81
81
  console.log(kleur.dim(` Polling: every ${intervalSec}s (slows when idle) · Stop: Ctrl+C`));
82
82
  console.log();
@@ -143,7 +143,7 @@ async function watchLoop(opts) {
143
143
  };
144
144
  try {
145
145
  onStatus('Checking MagicPixel…');
146
- const r = await runOnce({ ...opts, watch: false }, { watchMode: true, onStatus });
146
+ const r = await runOnce({ ...opts, watch: false }, { watchMode: true, onStatus, gameIndex: header.gameIndex });
147
147
  // Reset backoff on any successful tick. Resume message fires once when
148
148
  // we recover from an auth pause — `getApiKey()` re-reads
149
149
  // .magicpixel/credentials on every call, so a `magicpixel login` in
@@ -242,6 +242,7 @@ async function watchLoop(opts) {
242
242
  }
243
243
  }
244
244
  finally {
245
+ header.gameIndex = undefined;
245
246
  inFlight = false;
246
247
  }
247
248
  };
@@ -329,25 +330,25 @@ export function classifyTickError(err, state) {
329
330
  }
330
331
  return { kind: 'other', nextBackoffSec: Math.min(state.backoffSec * 2, 60) };
331
332
  }
332
- async function loadWatchSpriteLine() {
333
+ async function loadWatchHeader(onProgress) {
333
334
  try {
334
335
  const config = await loadConfig();
335
336
  const state = await loadState();
336
337
  const lastPulled = state.assets ? Object.keys(state.assets).length : 0;
337
338
  let workingSet = 0;
339
+ let gameIndex;
338
340
  const kind = await detectProjectKind();
339
341
  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;
342
+ gameIndex = await indexGamePngs(kind, process.cwd(), config.outDir, { onProgress });
343
+ workingSet = matchConnectGlobs(gameIndex, config.connect).total;
343
344
  }
344
345
  else if (state.synced) {
345
346
  workingSet = Object.keys(state.synced).length;
346
347
  }
347
- return formatWatchSpriteLine({ workingSet, lastPulled });
348
+ return { line: formatWatchSpriteLine({ workingSet, lastPulled }), gameIndex };
348
349
  }
349
350
  catch {
350
- return null;
351
+ return { line: null };
351
352
  }
352
353
  }
353
354
  async function runOnce(opts, runOpts = {}) {
@@ -406,9 +407,12 @@ async function runOnce(opts, runOpts = {}) {
406
407
  // magicpixel.json opts back into everything.
407
408
  onStatus?.('Looking through your game sprites…');
408
409
  const projectKind = await detectProjectKind();
409
- const gameIndex = isEngineKind(projectKind)
410
- ? await indexGamePngs(projectKind, process.cwd(), config.outDir)
411
- : { files: [], capped: false };
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 });
412
416
  const connected = matchConnectGlobs(gameIndex, config.connect ?? []);
413
417
  const sourceByKey = collectSourceRelMap(connected.entries, state.synced);
414
418
  if (verbose && gameIndex.capped) {
@@ -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
- await walkPngs(scanRoot, scanRoot, destNorm, found, MAX_GAME_INDEX + 1, io);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magicpixelart/cli",
3
- "version": "0.5.10",
3
+ "version": "0.5.11",
4
4
  "description": "Sync MagicPixel pixel-art assets to your local project.",
5
5
  "type": "module",
6
6
  "bin": {