@magicpixelart/cli 0.5.11 → 0.5.12

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,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.12] — 2026-08-26
11
+
12
+ ### Fixed
13
+
14
+ - **Watch counting no longer looks hung.** Sprite counting used to stay on
15
+ `Counting sprites in your game…` with no numbers until the first PNG, which
16
+ on a large Unity tree can take a long time. The spinner now updates as
17
+ folders are visited (`0 sprites · 128 folders`) and walks `Assets/` before
18
+ sibling project folders so the sprite count appears sooner.
19
+
10
20
  ## [0.5.11] — 2026-08-26
11
21
 
12
22
  ### Changed
@@ -29,7 +29,7 @@ export async function connectCommand(glob) {
29
29
  }
30
30
  const spinner = ora({ text: countingSpritesText(0), spinner: 'dots' }).start();
31
31
  const index = await indexGamePngs(kind, process.cwd(), config.outDir, {
32
- onProgress: (n) => { spinner.text = countingSpritesText(n); },
32
+ onProgress: (p) => { spinner.text = countingSpritesText(p); },
33
33
  });
34
34
  spinner.stop();
35
35
  if (index.capped) {
@@ -47,7 +47,7 @@ export async function runPushWith(config, state, opts = {}) {
47
47
  const index = opts.gameIndex
48
48
  ?? (isEngineKind(kind)
49
49
  ? await indexGamePngs(kind, process.cwd(), config.outDir, {
50
- onProgress: spinner ? (n) => { spinner.text = countingSpritesText(n); } : undefined,
50
+ onProgress: spinner ? (p) => { spinner.text = countingSpritesText(p); } : undefined,
51
51
  })
52
52
  : { files: [], capped: false });
53
53
  const matched = matchConnectGlobs(index, config.connect ?? []);
@@ -28,7 +28,7 @@ export async function searchCommand(query) {
28
28
  }
29
29
  const spinner = ora({ text: countingSpritesText(0), spinner: 'dots' }).start();
30
30
  const index = await indexGamePngs(kind, process.cwd(), outDir, {
31
- onProgress: (n) => { spinner.text = countingSpritesText(n); },
31
+ onProgress: (p) => { spinner.text = countingSpritesText(p); },
32
32
  });
33
33
  spinner.stop();
34
34
  if (index.capped) {
@@ -68,7 +68,7 @@ async function watchLoop(opts) {
68
68
  /* header is cosmetic */
69
69
  }
70
70
  }
71
- const header = await loadWatchHeader(countSpinner ? (n) => { countSpinner.text = countingSpritesText(n); } : undefined);
71
+ const header = await loadWatchHeader(countSpinner ? (p) => { countSpinner.text = countingSpritesText(p); } : undefined);
72
72
  if (countSpinner) {
73
73
  if (header.line)
74
74
  countSpinner.succeed(header.line.trim());
@@ -410,7 +410,13 @@ async function runOnce(opts, runOpts = {}) {
410
410
  const gameIndex = runOpts.gameIndex
411
411
  ?? (isEngineKind(projectKind)
412
412
  ? await indexGamePngs(projectKind, process.cwd(), config.outDir, {
413
- onProgress: (n) => onStatus?.(`Looking through your game sprites… ${n.toLocaleString('en-US')}`),
413
+ onProgress: (p) => {
414
+ const n = p.pngs.toLocaleString('en-US');
415
+ const f = p.folders.toLocaleString('en-US');
416
+ onStatus?.(p.folders > 0
417
+ ? `Looking through your game sprites… ${n} sprites · ${f} folders`
418
+ : `Looking through your game sprites… ${n}`);
419
+ },
414
420
  })
415
421
  : { files: [], capped: false });
416
422
  const connected = matchConnectGlobs(gameIndex, config.connect ?? []);
@@ -23,6 +23,10 @@ export const GAME_SCAN_SKIP_DIRS = new Set([
23
23
  'streamingassets',
24
24
  'node_modules',
25
25
  'magicpixel',
26
+ 'build',
27
+ 'builds',
28
+ 'recordings',
29
+ 'memorycaptures',
26
30
  ]);
27
31
  /**
28
32
  * Dot-directories we never walk. Other hidden folders (e.g. `.SpineRaw_*`)
@@ -94,28 +98,41 @@ function toEntry(abs, cwdAbs, assetRoot) {
94
98
  return null;
95
99
  return { abs, sourceRel, adoptRel, key: adopted.path.join('/') };
96
100
  }
101
+ function fmtCount(n, noun) {
102
+ return `${n.toLocaleString('en-US')} ${noun}${n === 1 ? '' : 's'}`;
103
+ }
97
104
  /** 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…';
105
+ export function countingSpritesText(progress = 0) {
106
+ const pngs = typeof progress === 'number' ? progress : progress.pngs;
107
+ const folders = typeof progress === 'number' ? 0 : progress.folders;
108
+ if (pngs === 0 && folders === 0)
109
+ return 'Counting sprites in your game…';
110
+ const spriteBit = fmtCount(pngs, 'sprite');
111
+ if (folders > 0)
112
+ return `Counting sprites in your game… ${spriteBit} · ${fmtCount(folders, 'folder')}`;
113
+ return `Counting sprites in your game… ${spriteBit}`;
102
114
  }
103
115
  function bindScanProgress(onProgress) {
104
116
  if (!onProgress)
105
117
  return () => { };
106
118
  let lastAt = 0;
107
- let lastN = 0;
108
- return (n) => {
109
- if (n === lastN)
119
+ let lastPngs = -1;
120
+ let lastFolders = -1;
121
+ return (pngs, folders) => {
122
+ if (pngs === lastPngs && folders === lastFolders)
110
123
  return;
111
124
  const now = Date.now();
112
- if (lastN === 0 || now - lastAt >= 80) {
125
+ if (lastPngs < 0 || now - lastAt >= 80) {
113
126
  lastAt = now;
114
- lastN = n;
115
- onProgress(n);
127
+ lastPngs = pngs;
128
+ lastFolders = folders;
129
+ onProgress({ pngs, folders });
116
130
  }
117
131
  };
118
132
  }
133
+ function samePath(a, b) {
134
+ return a.replace(/\\/g, '/').toLowerCase() === b.replace(/\\/g, '/').toLowerCase();
135
+ }
119
136
  /** Walk the engine tree and return index entries. Does not copy files. */
120
137
  export async function indexGamePngs(kind, cwd = process.cwd(), outDir = '', opts = {}) {
121
138
  const empty = { files: [], capped: false };
@@ -128,11 +145,11 @@ export async function indexGamePngs(kind, cwd = process.cwd(), outDir = '', opts
128
145
  const namedRoot = gameScanRoot(kind);
129
146
  const assetRoot = namedRoot ? resolveChildDir(cwd, namedRoot) : null;
130
147
  const found = [];
148
+ const stats = { folders: 0 };
131
149
  const io = createLimit(SCAN_WALK_CONCURRENCY);
132
150
  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);
151
+ await walkPngs(scanRoot, scanRoot, destNorm, found, MAX_GAME_INDEX + 1, io, report, stats, assetRoot);
152
+ opts.onProgress?.({ pngs: found.length, folders: stats.folders });
136
153
  found.sort((a, b) => a.localeCompare(b));
137
154
  const capped = found.length > MAX_GAME_INDEX;
138
155
  if (capped)
@@ -161,9 +178,12 @@ export function searchGameIndex(index, query) {
161
178
  return [];
162
179
  return index.files.filter((e) => e.sourceRel.toLowerCase().includes(q) || e.key.includes(q) || e.adoptRel.toLowerCase().includes(q));
163
180
  }
164
- async function walkPngs(dir, root, destNorm, out, limit, io, onFound) {
181
+ async function walkPngs(dir, root, destNorm, out, limit, io, onVisit, stats, preferFirst) {
165
182
  if (out.length >= limit)
166
183
  return;
184
+ // Count the folder *before* readdir so a huge listing still shows movement.
185
+ stats.folders++;
186
+ onVisit(out.length, stats.folders);
167
187
  let entries;
168
188
  try {
169
189
  entries = await readdir(dir, { withFileTypes: true });
@@ -172,7 +192,8 @@ async function walkPngs(dir, root, destNorm, out, limit, io, onFound) {
172
192
  return;
173
193
  }
174
194
  entries.sort((a, b) => a.name.localeCompare(b.name));
175
- const subdirs = [];
195
+ const preferred = [];
196
+ const other = [];
176
197
  for (const ent of entries) {
177
198
  if (out.length >= limit)
178
199
  return;
@@ -190,14 +211,25 @@ async function walkPngs(dir, root, destNorm, out, limit, io, onFound) {
190
211
  continue;
191
212
  if (destNorm && full.replace(/\\/g, '/').toLowerCase() === destNorm)
192
213
  continue;
193
- subdirs.push(full);
214
+ if (preferFirst && samePath(full, preferFirst))
215
+ preferred.push(full);
216
+ else
217
+ other.push(full);
194
218
  }
195
219
  else if (ent.isFile() && ent.name.toLowerCase().endsWith('.png')) {
196
220
  out.push(full);
197
- onFound?.(out.length);
221
+ onVisit(out.length, stats.folders);
198
222
  }
199
223
  }
200
- if (subdirs.length === 0 || out.length >= limit)
224
+ if (out.length >= limit)
225
+ return;
226
+ // Engine asset tree first (Unity `Assets/`) so sprite counts appear before
227
+ // sibling folders like Builds/Editor. Nested walks keep preferFirst so a
228
+ // misplaced `Assets` dir is still prioritized if we ever see it.
229
+ for (const d of preferred) {
230
+ await walkPngs(d, root, destNorm, out, limit, io, onVisit, stats, null);
231
+ }
232
+ if (other.length === 0 || out.length >= limit)
201
233
  return;
202
- await Promise.all(subdirs.map((d) => io(() => walkPngs(d, root, destNorm, out, limit, io, onFound))));
234
+ await Promise.all(other.map((d) => io(() => walkPngs(d, root, destNorm, out, limit, io, onVisit, stats, null))));
203
235
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magicpixelart/cli",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "Sync MagicPixel pixel-art assets to your local project.",
5
5
  "type": "module",
6
6
  "bin": {