@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 +8 -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 +21 -17
- package/dist/util/gameScan.js +30 -4
- package/package.json +1 -1
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
|
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';
|
|
@@ -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:
|
|
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
|
|
71
|
+
const header = await loadWatchHeader(countSpinner ? (n) => { countSpinner.text = countingSpritesText(n); } : undefined);
|
|
72
72
|
if (countSpinner) {
|
|
73
|
-
if (
|
|
74
|
-
countSpinner.succeed(
|
|
73
|
+
if (header.line)
|
|
74
|
+
countSpinner.succeed(header.line.trim());
|
|
75
75
|
else
|
|
76
76
|
countSpinner.stop();
|
|
77
77
|
}
|
|
78
|
-
else if (
|
|
79
|
-
console.log(kleur.dim(
|
|
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
|
|
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
|
-
|
|
341
|
-
|
|
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 =
|
|
410
|
-
|
|
411
|
-
|
|
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) {
|
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
|
}
|