@magicpixelart/cli 0.5.12 → 0.5.13
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/connect.js +9 -4
- package/dist/commands/sync.js +5 -3
- package/dist/util/gameScan.js +106 -56
- 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.13] — 2026-08-26
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Connect no longer hangs at `0 sprites · 1 folder`.** Full Unity games
|
|
15
|
+
(`ProjectSettings` present) scan `Assets/` only — listing Library and the
|
|
16
|
+
rest of the project root blocked the spinner before any sprite was found.
|
|
17
|
+
Directory reads stream so the path keeps updating (`Assets/Sprites`), and
|
|
18
|
+
nested walks no longer deadlock on the folder limiter.
|
|
19
|
+
- **`connect` copy.** Re-running `connect '**'` no longer prints
|
|
20
|
+
`connect pattern already present: **`. First-time connect says
|
|
21
|
+
`now syncing all sprites in your game`.
|
|
22
|
+
|
|
10
23
|
## [0.5.12] — 2026-08-26
|
|
11
24
|
|
|
12
25
|
### Fixed
|
package/dist/commands/connect.js
CHANGED
|
@@ -6,6 +6,14 @@ import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
|
6
6
|
import { indexGamePngs, matchConnectGlobs, GAME_INDEX_CAP_HINT, connectCapMessage, countingSpritesText } from '../util/gameScan.js';
|
|
7
7
|
import { cmd } from '../util/invoke.js';
|
|
8
8
|
import { runPush } from './push.js';
|
|
9
|
+
/** Plain-language label for a connect glob (`**` is not meaningful to players). */
|
|
10
|
+
export function describeWorkingSet(glob) {
|
|
11
|
+
const g = glob.trim();
|
|
12
|
+
if (g === '**' || g === '**/*' || g === '**/**' || g === '**/*.png' || g === '**/**/*.png') {
|
|
13
|
+
return 'all sprites in your game';
|
|
14
|
+
}
|
|
15
|
+
return g;
|
|
16
|
+
}
|
|
9
17
|
/**
|
|
10
18
|
* Add a working-set glob and ingest matching game PNGs into Connected.
|
|
11
19
|
* Sync writes edits back to the original files — not a copy under outDir.
|
|
@@ -17,10 +25,7 @@ export async function connectCommand(glob) {
|
|
|
17
25
|
if (!already) {
|
|
18
26
|
config.connect.push(pattern);
|
|
19
27
|
await saveConfig(config);
|
|
20
|
-
console.log(kleur.green(`✓
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
console.log(kleur.dim(` connect pattern already present: ${pattern}`));
|
|
28
|
+
console.log(kleur.green(`✓ now syncing ${describeWorkingSet(pattern)}`));
|
|
24
29
|
}
|
|
25
30
|
const kind = await detectProjectKind();
|
|
26
31
|
if (!isEngineKind(kind)) {
|
package/dist/commands/sync.js
CHANGED
|
@@ -413,9 +413,11 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
413
413
|
onProgress: (p) => {
|
|
414
414
|
const n = p.pngs.toLocaleString('en-US');
|
|
415
415
|
const f = p.folders.toLocaleString('en-US');
|
|
416
|
-
onStatus?.(p.
|
|
417
|
-
? `Looking through your game sprites… ${n} sprites · ${f} folders`
|
|
418
|
-
:
|
|
416
|
+
onStatus?.(p.current
|
|
417
|
+
? `Looking through your game sprites… ${n} sprites · ${f} folders · ${p.current}`
|
|
418
|
+
: p.folders > 0
|
|
419
|
+
? `Looking through your game sprites… ${n} sprites · ${f} folders`
|
|
420
|
+
: `Looking through your game sprites… ${n}`);
|
|
419
421
|
},
|
|
420
422
|
})
|
|
421
423
|
: { files: [], capped: false });
|
package/dist/util/gameScan.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* globs select a working set; sync writes back to those original paths.
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
|
-
import {
|
|
7
|
+
import { opendir } from 'node:fs/promises';
|
|
8
8
|
import { relative, resolve } from 'node:path';
|
|
9
9
|
import { isEngineKind, resolveChildDir } from './framework.js';
|
|
10
10
|
import { assertPathInsideRoot, sanitizeSourceRel } from './security.js';
|
|
@@ -52,8 +52,12 @@ export const MAX_GAME_INDEX = 10_000;
|
|
|
52
52
|
export const MAX_GAME_CONNECT = MAX_GAME_INDEX;
|
|
53
53
|
export const GAME_INDEX_CAP_HINT = `Game index hit ${MAX_GAME_INDEX} PNGs — later files aren't searchable or connectable.`;
|
|
54
54
|
export function connectCapMessage(total, ingested) {
|
|
55
|
-
return (`${total}
|
|
56
|
-
`
|
|
55
|
+
return (`${total} sprites match — syncing the first ${ingested} (cap ${MAX_GAME_CONNECT}). ` +
|
|
56
|
+
`Connect a smaller folder if you don't need all of them.`);
|
|
57
|
+
}
|
|
58
|
+
/** True for a full Unity game project (not an embedded UPM package). */
|
|
59
|
+
function isUnityGameProject(cwd, assetRoot) {
|
|
60
|
+
return Boolean(assetRoot && existsSync(resolve(cwd, 'ProjectSettings')));
|
|
57
61
|
}
|
|
58
62
|
export function gameScanRoot(kind) {
|
|
59
63
|
switch (kind) {
|
|
@@ -101,32 +105,58 @@ function toEntry(abs, cwdAbs, assetRoot) {
|
|
|
101
105
|
function fmtCount(n, noun) {
|
|
102
106
|
return `${n.toLocaleString('en-US')} ${noun}${n === 1 ? '' : 's'}`;
|
|
103
107
|
}
|
|
108
|
+
function shortPath(rel) {
|
|
109
|
+
const n = rel.replace(/\\/g, '/');
|
|
110
|
+
if (!n || n === '.')
|
|
111
|
+
return '';
|
|
112
|
+
return n.length <= 48 ? n : `…${n.slice(-47)}`;
|
|
113
|
+
}
|
|
104
114
|
/** Spinner / status copy while walking the game tree. */
|
|
105
115
|
export function countingSpritesText(progress = 0) {
|
|
106
116
|
const pngs = typeof progress === 'number' ? progress : progress.pngs;
|
|
107
117
|
const folders = typeof progress === 'number' ? 0 : progress.folders;
|
|
108
|
-
|
|
118
|
+
const current = typeof progress === 'number' ? undefined : progress.current;
|
|
119
|
+
if (pngs === 0 && folders === 0 && !current)
|
|
109
120
|
return 'Counting sprites in your game…';
|
|
110
121
|
const spriteBit = fmtCount(pngs, 'sprite');
|
|
122
|
+
const parts = [spriteBit];
|
|
111
123
|
if (folders > 0)
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
parts.push(fmtCount(folders, 'folder'));
|
|
125
|
+
const where = current ? shortPath(current) : '';
|
|
126
|
+
if (where)
|
|
127
|
+
parts.push(where);
|
|
128
|
+
return `Counting sprites in your game… ${parts.join(' · ')}`;
|
|
114
129
|
}
|
|
115
130
|
function bindScanProgress(onProgress) {
|
|
116
131
|
if (!onProgress)
|
|
117
132
|
return () => { };
|
|
118
133
|
let lastAt = 0;
|
|
119
|
-
let
|
|
120
|
-
let
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
let pending = null;
|
|
135
|
+
let timer = null;
|
|
136
|
+
const flush = (p) => {
|
|
137
|
+
pending = null;
|
|
138
|
+
lastAt = Date.now();
|
|
139
|
+
onProgress(p);
|
|
140
|
+
};
|
|
141
|
+
return (pngs, folders, current) => {
|
|
142
|
+
const next = { pngs, folders, current };
|
|
124
143
|
const now = Date.now();
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
144
|
+
if (lastAt === 0 || now - lastAt >= 80) {
|
|
145
|
+
if (timer) {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
timer = null;
|
|
148
|
+
}
|
|
149
|
+
flush(next);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
pending = next;
|
|
153
|
+
if (!timer) {
|
|
154
|
+
timer = setTimeout(() => {
|
|
155
|
+
timer = null;
|
|
156
|
+
if (pending)
|
|
157
|
+
flush(pending);
|
|
158
|
+
}, 80 - (now - lastAt));
|
|
159
|
+
timer.unref();
|
|
130
160
|
}
|
|
131
161
|
};
|
|
132
162
|
}
|
|
@@ -148,7 +178,11 @@ export async function indexGamePngs(kind, cwd = process.cwd(), outDir = '', opts
|
|
|
148
178
|
const stats = { folders: 0 };
|
|
149
179
|
const io = createLimit(SCAN_WALK_CONCURRENCY);
|
|
150
180
|
const report = bindScanProgress(opts.onProgress);
|
|
151
|
-
|
|
181
|
+
// Full Unity games: walk Assets/ only. Listing the project root waits on
|
|
182
|
+
// Library/Packages/cloud placeholders and looks hung at "1 folder".
|
|
183
|
+
const startAt = isUnityGameProject(cwd, assetRoot) ? assetRoot : scanRoot;
|
|
184
|
+
const preferFirst = startAt === scanRoot ? assetRoot : null;
|
|
185
|
+
await walkPngs(startAt, scanRoot, destNorm, found, MAX_GAME_INDEX + 1, io, report, stats, preferFirst);
|
|
152
186
|
opts.onProgress?.({ pngs: found.length, folders: stats.folders });
|
|
153
187
|
found.sort((a, b) => a.localeCompare(b));
|
|
154
188
|
const capped = found.length > MAX_GAME_INDEX;
|
|
@@ -178,58 +212,74 @@ export function searchGameIndex(index, query) {
|
|
|
178
212
|
return [];
|
|
179
213
|
return index.files.filter((e) => e.sourceRel.toLowerCase().includes(q) || e.key.includes(q) || e.adoptRel.toLowerCase().includes(q));
|
|
180
214
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
215
|
+
function folderLabel(dir, root) {
|
|
216
|
+
return relative(root, dir).replace(/\\/g, '/') || '.';
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* List one directory. The limiter wraps *only* this listing so a parent does
|
|
220
|
+
* not hold a slot while waiting on children (that deadlocks wide Unity trees).
|
|
221
|
+
*/
|
|
222
|
+
async function listDir(dir, root, destNorm, out, limit, onVisit, stats, preferFirst, current) {
|
|
223
|
+
const preferred = [];
|
|
224
|
+
const other = [];
|
|
225
|
+
let dh;
|
|
188
226
|
try {
|
|
189
|
-
|
|
227
|
+
dh = await opendir(dir);
|
|
190
228
|
}
|
|
191
229
|
catch {
|
|
192
|
-
return;
|
|
230
|
+
return { preferred, other };
|
|
193
231
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (ent.isDirectory()) {
|
|
210
|
-
if (shouldSkipDir(ent.name))
|
|
232
|
+
let lastBeat = Date.now();
|
|
233
|
+
try {
|
|
234
|
+
for await (const ent of dh) {
|
|
235
|
+
if (out.length >= limit)
|
|
236
|
+
break;
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
if (now - lastBeat >= 80) {
|
|
239
|
+
lastBeat = now;
|
|
240
|
+
onVisit(out.length, stats.folders, current);
|
|
241
|
+
}
|
|
242
|
+
if (shouldSkipDir(ent.name) || ent.isSymbolicLink())
|
|
211
243
|
continue;
|
|
212
|
-
|
|
244
|
+
const full = resolve(dir, ent.name);
|
|
245
|
+
try {
|
|
246
|
+
assertPathInsideRoot(full, root, 'scan');
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
213
249
|
continue;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
250
|
+
}
|
|
251
|
+
if (ent.isDirectory()) {
|
|
252
|
+
if (destNorm && full.replace(/\\/g, '/').toLowerCase() === destNorm)
|
|
253
|
+
continue;
|
|
254
|
+
if (preferFirst && samePath(full, preferFirst))
|
|
255
|
+
preferred.push(full);
|
|
256
|
+
else
|
|
257
|
+
other.push(full);
|
|
258
|
+
}
|
|
259
|
+
else if (ent.isFile() && ent.name.toLowerCase().endsWith('.png')) {
|
|
260
|
+
out.push(full);
|
|
261
|
+
onVisit(out.length, stats.folders, current);
|
|
262
|
+
}
|
|
222
263
|
}
|
|
223
264
|
}
|
|
265
|
+
catch {
|
|
266
|
+
/* unreadable mid-listing */
|
|
267
|
+
}
|
|
268
|
+
return { preferred, other };
|
|
269
|
+
}
|
|
270
|
+
async function walkPngs(dir, root, destNorm, out, limit, io, onVisit, stats, preferFirst) {
|
|
271
|
+
if (out.length >= limit)
|
|
272
|
+
return;
|
|
273
|
+
stats.folders++;
|
|
274
|
+
const current = folderLabel(dir, root);
|
|
275
|
+
onVisit(out.length, stats.folders, current);
|
|
276
|
+
const { preferred, other } = await io(() => listDir(dir, root, destNorm, out, limit, onVisit, stats, preferFirst, current));
|
|
224
277
|
if (out.length >= limit)
|
|
225
278
|
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
279
|
for (const d of preferred) {
|
|
230
280
|
await walkPngs(d, root, destNorm, out, limit, io, onVisit, stats, null);
|
|
231
281
|
}
|
|
232
282
|
if (other.length === 0 || out.length >= limit)
|
|
233
283
|
return;
|
|
234
|
-
await Promise.all(other.map((d) =>
|
|
284
|
+
await Promise.all(other.map((d) => walkPngs(d, root, destNorm, out, limit, io, onVisit, stats, null)));
|
|
235
285
|
}
|