@magicpixelart/cli 0.5.22 → 0.5.24

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,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.24] — 2026-09-21
11
+
12
+ - `push --force` keeps your local copy when a sprite changed in both places. It
13
+ re-sends against the live cloud state rather than skipping the safety check,
14
+ so a save that lands in between is still reported instead of overwritten.
15
+ - A both-sides-changed refusal now carries the live cloud fingerprint, so a
16
+ follow-up `push` works even for large documents whose hash wasn't cached —
17
+ the last case where the sync/push advice could still dead-end.
18
+ - Conflict messages name both ways out: press Sync on the artboard, or
19
+ `push --force`.
20
+
21
+ ## [0.5.23] — 2026-09-21
22
+
23
+ - Pressing Sync on an artboard now explicitly chooses MagicPixel's copy when
24
+ that PNG was also edited locally, resolving the previous sync/push loop.
25
+ - Conflict lists are capped, nested push warnings no longer repeat during
26
+ sync, and completion summaries include locally unchanged sprites accurately.
27
+
10
28
  ## [0.5.22] — 2026-09-14
11
29
 
12
30
  - Raised the daily sprite download allowance from 1 GiB to 10 GiB for large
@@ -110,6 +110,10 @@ export async function runPushWith(config, state, opts = {}) {
110
110
  const segments = entry
111
111
  ? entry.adoptRel.replace(/\.png$/i, '').split('/')
112
112
  : key.split('/');
113
+ if (opts.excludeKeys?.has(key)) {
114
+ reportHash();
115
+ return;
116
+ }
113
117
  candidates.push({
114
118
  key,
115
119
  segments,
@@ -141,12 +145,12 @@ export async function runPushWith(config, state, opts = {}) {
141
145
  if (skippedUnsafe > 0 && !quiet) {
142
146
  console.log(kleur.yellow(`! skipped ${skippedUnsafe} path${skippedUnsafe === 1 ? '' : 's'} that were not regular files inside the project.`));
143
147
  }
144
- if (legacy.length > 0 && !quiet) {
148
+ if (legacy.length > 0 && !quiet && !opts.nestedSync) {
145
149
  console.log();
146
150
  console.log(kleur.yellow(`! ${legacy.length} sprite${legacy.length === 1 ? '' : 's'} came from a legacy single-image file — skipped.`));
147
151
  console.log(kleur.dim(` Fix: open the file in MagicPixel and save it once, then re-run \`${cmd('sync')}\`.`));
148
152
  }
149
- if (flattenBlocked.length > 0 && !quiet) {
153
+ if (flattenBlocked.length > 0 && !quiet && !opts.nestedSync) {
150
154
  console.log();
151
155
  console.log(kleur.yellow(`! ${flattenBlocked.length} sprite${flattenBlocked.length === 1 ? '' : 's'} would flatten a multi-layer artboard — skipped.`));
152
156
  for (const a of flattenBlocked.slice(0, 10)) {
@@ -225,7 +229,38 @@ export async function runPushWith(config, state, opts = {}) {
225
229
  await saveState({ ...state, synced });
226
230
  throw e;
227
231
  }
228
- const counts = { created: 0, updated: 0, unchanged: 0, conflict: 0, error: 0 };
232
+ const spriteByKey = new Map(sprites.map((s) => [s.key, s]));
233
+ if (opts.force) {
234
+ // Two rounds at most: an adopt-shaped or hash-less refusal comes back
235
+ // without a live sha, and the retry as an update is what makes the server
236
+ // composite the artboard and report one.
237
+ for (let round = 0; round < 2; round++) {
238
+ const retry = forcedRetrySprites(results, spriteByKey, opts.flatten === true);
239
+ if (retry.length === 0)
240
+ break;
241
+ const retryResults = await pushSpritesAdaptive(retry);
242
+ synced = await commitPushResults({
243
+ synced,
244
+ batchResults: retryResults,
245
+ candidates,
246
+ sourceByKey,
247
+ shaByKey,
248
+ fpByKey,
249
+ outDir: config.outDir,
250
+ });
251
+ await saveState({ ...state, synced });
252
+ replaceResultsByKey(results, retryResults);
253
+ if (retryResults.every((r) => r.status === 'conflict' && !r.sha256))
254
+ break;
255
+ }
256
+ }
257
+ // Remember the live cloud sha a refusal reported, so a later `push` (with or
258
+ // without --force) has a baseline the concurrency check accepts. The manifest
259
+ // hash can be absent for big documents, which used to dead-end this path.
260
+ if (rememberRefusedCloudShas(synced, results)) {
261
+ await saveState({ ...state, synced });
262
+ }
263
+ const counts = { created: 0, updated: 0, unchanged: skipped, conflict: 0, error: 0 };
229
264
  for (const r of results) {
230
265
  if (r.status in counts)
231
266
  counts[r.status]++;
@@ -241,14 +276,19 @@ export async function runPushWith(config, state, opts = {}) {
241
276
  progress.succeed(`Pushed. ${summary}`);
242
277
  }
243
278
  if (!quiet) {
279
+ let printedIssues = 0;
244
280
  for (const r of results) {
245
281
  if (r.status === 'conflict') {
246
- console.log(` ${kleur.yellow('!')} ${r.key}: ${conflictHint(r.reason)}`);
282
+ if (printedIssues++ < 10)
283
+ console.log(` ${kleur.yellow('!')} ${r.key}: ${conflictHint(r.reason)}`);
247
284
  }
248
285
  else if (r.status === 'error') {
249
- console.log(` ${kleur.red('!')} ${r.key}: ${r.message ?? 'push failed'}`);
286
+ if (printedIssues++ < 10)
287
+ console.log(` ${kleur.red('!')} ${r.key}: ${r.message ?? 'push failed'}`);
250
288
  }
251
289
  }
290
+ if (printedIssues > 10)
291
+ console.log(kleur.dim(` …and ${printedIssues - 10} more`));
252
292
  if (counts.created > 0) {
253
293
  console.log(kleur.dim(` ${counts.created} new sprite${counts.created === 1 ? '' : 's'} now live in your MagicPixel library ` +
254
294
  `(working set + ${relative(process.cwd(), outRoot)}). They appear in Connected as they import.`));
@@ -258,6 +298,62 @@ export async function runPushWith(config, state, opts = {}) {
258
298
  process.exitCode = 1;
259
299
  return { ...counts, imported: matched.entries.length };
260
300
  }
301
+ /**
302
+ * Sprites to re-send under `--force`: every sprite the cloud refused because it
303
+ * also changed there, re-addressed as an update against the sha the refusal
304
+ * reported. Not a bypass — the server still compares, so a save that lands in
305
+ * between refuses again.
306
+ */
307
+ export function forcedRetrySprites(results, spriteByKey, flatten) {
308
+ const out = [];
309
+ for (const r of results) {
310
+ if (r.status !== 'conflict' || r.reason !== 'cloud-changed' || !r.assetId)
311
+ continue;
312
+ const original = spriteByKey.get(r.key);
313
+ if (!original)
314
+ continue;
315
+ // Skip a round we already tried with this exact baseline.
316
+ if (r.sha256 && original.baseSha256 === r.sha256)
317
+ continue;
318
+ out.push({
319
+ key: original.key,
320
+ pngBase64: original.pngBase64,
321
+ diskSha256: original.diskSha256,
322
+ assetId: r.assetId,
323
+ layerIdx: typeof r.layerIdx === 'number' ? r.layerIdx : (original.layerIdx ?? 0),
324
+ baseSha256: r.sha256 ?? null,
325
+ flatten,
326
+ });
327
+ }
328
+ return out;
329
+ }
330
+ /** Overwrite the earlier outcome for every retried key, in place. */
331
+ export function replaceResultsByKey(results, updates) {
332
+ for (const u of updates) {
333
+ const i = results.findIndex((r) => r.key === u.key);
334
+ if (i >= 0)
335
+ results[i] = u;
336
+ else
337
+ results.push(u);
338
+ }
339
+ }
340
+ /**
341
+ * Store the live cloud sha a `cloud-changed` refusal reported as this sprite's
342
+ * pending baseline. Returns true when anything changed.
343
+ */
344
+ export function rememberRefusedCloudShas(synced, results) {
345
+ let changed = false;
346
+ for (const r of results) {
347
+ if (r.status !== 'conflict' || r.reason !== 'cloud-changed' || !r.sha256)
348
+ continue;
349
+ const prev = synced[r.key];
350
+ if (!prev || prev.pendingCloudSha256 === r.sha256)
351
+ continue;
352
+ synced[r.key] = { ...prev, pendingCloudSha256: r.sha256 };
353
+ changed = true;
354
+ }
355
+ return changed;
356
+ }
261
357
  async function commitPushResults(opts) {
262
358
  const { batchResults, candidates, sourceByKey, shaByKey, fpByKey, outDir } = opts;
263
359
  const failedKeys = new Set(batchResults.filter((r) => r.status === 'conflict' || r.status === 'error').map((r) => r.key));
@@ -276,6 +372,7 @@ async function commitPushResults(opts) {
276
372
  delete nextSynced[r.key];
277
373
  const diskSha256 = shaByKey.get(r.key) ?? shaByKey.get(stateKey) ?? nextSynced[stateKey]?.diskSha256;
278
374
  const fp = fpByKey.get(r.key) ?? fpByKey.get(stateKey);
375
+ const previous = nextSynced[stateKey];
279
376
  nextSynced[stateKey] = {
280
377
  assetId: r.assetId,
281
378
  layerIdx: typeof r.layerIdx === 'number' ? r.layerIdx : (nextSynced[stateKey]?.layerIdx ?? 0),
@@ -285,6 +382,8 @@ async function commitPushResults(opts) {
285
382
  ...(fp?.diskMtimeMs != null ? { diskMtimeMs: fp.diskMtimeMs } : {}),
286
383
  ...(fp?.diskSize != null ? { diskSize: fp.diskSize } : {}),
287
384
  ...(sourceRel ? { sourceRel } : {}),
385
+ ...(previous?.releasedAt ? { releasedAt: previous.releasedAt } : {}),
386
+ ...(previous?.releasedVersion != null ? { releasedVersion: previous.releasedVersion } : {}),
288
387
  };
289
388
  }
290
389
  }
@@ -300,7 +399,7 @@ async function persistDiskFingerprints(state, candidates) {
300
399
  function conflictHint(reason) {
301
400
  switch (reason) {
302
401
  case 'cloud-changed':
303
- return `changed in MagicPixel since the last sync — run \`${cmd('sync')}\` first, then re-push.`;
402
+ return `also changed in MagicPixel. Press Sync on that artboard to use MagicPixel's copy, or run \`${cmd('push --force')}\` to keep this local one.`;
304
403
  case 'would-flatten':
305
404
  return 'artboard has multiple layers or frames — re-run with --flatten to replace it.';
306
405
  case 'legacy-document':
@@ -308,7 +407,7 @@ function conflictHint(reason) {
308
407
  case 'not-found':
309
408
  return `artboard no longer exists in MagicPixel — run \`${cmd('sync')}\` to refresh.`;
310
409
  default:
311
- return `conflict — run \`${cmd('sync')}\` and try again.`;
410
+ return `conflict — review both copies, then press the artboard's Sync button to use MagicPixel's copy.`;
312
411
  }
313
412
  }
314
413
  /**
@@ -24,7 +24,7 @@ import { formatBytes } from '../util/format.js';
24
24
  import { computePreviousKeyOrphans } from '../util/previousKeyOrphans.js';
25
25
  import { cmd } from '../util/invoke.js';
26
26
  import { formatSlowTickLine, formatWatchSpriteLine, SLOW_TICK_HEARTBEAT_MS } from '../util/watchCopy.js';
27
- import { decidePull } from '../util/pullDecision.js';
27
+ import { decidePull, hasNewExplicitRelease } from '../util/pullDecision.js';
28
28
  import { hasUnpushedLocalEdit } from '../util/localEdit.js';
29
29
  import { shouldReconcile } from '../util/reconcile.js';
30
30
  import { runPush } from './push.js';
@@ -727,6 +727,7 @@ async function runOnce(opts, runOpts = {}) {
727
727
  const shaByEntryId = new Map();
728
728
  const toDownload = [];
729
729
  const conflicts = [];
730
+ const failedDownloadKeys = new Set();
730
731
  let bytesSaved = 0;
731
732
  let unchanged = 0;
732
733
  const synced = state.synced ?? {};
@@ -761,7 +762,15 @@ async function runOnce(opts, runOpts = {}) {
761
762
  const inWorkingSet = isWorkingSetEntry(entry, workingSet);
762
763
  // Unchanged cloud composite: skip hashing the original PNG (it almost
763
764
  // never matches) unless the file is gone and we need to restore it.
764
- if (entry.sha256 && previousCloudSha256 === entry.sha256 && existsSync(pathFor(entry))) {
765
+ const previousReleasedAt = synced[entry.key]?.releasedAt;
766
+ const freshRelease = hasNewExplicitRelease({
767
+ releasedAt: entry.released_at,
768
+ previousReleasedAt,
769
+ releasedVersion: entry.released_version,
770
+ previousReleasedVersion: synced[entry.key]?.releasedVersion,
771
+ lastSync: state.lastSync,
772
+ });
773
+ if (!freshRelease && entry.sha256 && previousCloudSha256 === entry.sha256 && existsSync(pathFor(entry))) {
765
774
  shaByEntryId.set(entry.id, null);
766
775
  unchanged++;
767
776
  if (entry.size_bytes)
@@ -776,6 +785,11 @@ async function runOnce(opts, runOpts = {}) {
776
785
  previousCloudSha256,
777
786
  lastPushedDiskSha256: synced[entry.key]?.diskSha256,
778
787
  inWorkingSet,
788
+ releasedAt: entry.released_at,
789
+ previousReleasedAt,
790
+ releasedVersion: entry.released_version,
791
+ previousReleasedVersion: synced[entry.key]?.releasedVersion,
792
+ lastSync: state.lastSync,
779
793
  });
780
794
  if (decision === 'conflict') {
781
795
  conflicts.push(entry.key);
@@ -1130,6 +1144,7 @@ async function runOnce(opts, runOpts = {}) {
1130
1144
  }
1131
1145
  catch (e) {
1132
1146
  result.failed++;
1147
+ failedDownloadKeys.add(entry.key);
1133
1148
  progress?.stop();
1134
1149
  // Multi-line messages come from friendlyFsError — print all lines
1135
1150
  // so the user sees the fix hint. Single-line errors stay terse.
@@ -1270,9 +1285,21 @@ async function runOnce(opts, runOpts = {}) {
1270
1285
  // Conflicted keys keep their old baselines: recording the new cloud sha
1271
1286
  // (with the locally-edited disk sha) would make both halves believe they
1272
1287
  // are in sync and freeze the divergence forever.
1273
- if (conflictSet.has(entry.key))
1288
+ if (conflictSet.has(entry.key)) {
1289
+ const previous = nextSynced[entry.key];
1290
+ if (previous && entry.sha256) {
1291
+ nextSynced[entry.key] = { ...previous, pendingCloudSha256: entry.sha256 };
1292
+ }
1293
+ continue;
1294
+ }
1295
+ if (failedDownloadKeys.has(entry.key))
1274
1296
  continue;
1275
- const sha = entry.sha256 ?? (await fileSha256(pathFor(entry)));
1297
+ // After a write, the actual downloaded bytes are the disk/cloud baseline.
1298
+ // The manifest hash may be null or represent a cached composite while the
1299
+ // download endpoint serves storage bytes for a single-artboard document.
1300
+ const sha = writtenKeys.has(entry.key)
1301
+ ? await fileSha256(pathFor(entry))
1302
+ : (entry.sha256 ?? (await fileSha256(pathFor(entry))));
1276
1303
  if (!sha)
1277
1304
  continue;
1278
1305
  const prev = nextSynced[entry.key];
@@ -1284,7 +1311,16 @@ async function runOnce(opts, runOpts = {}) {
1284
1311
  // them anyway, flagged, so `push` skips them with a hint instead of
1285
1312
  // adopting the same art as a second document.
1286
1313
  if (typeof entry.layer_idx !== 'number' || entry.layer_idx < 0) {
1287
- nextSynced[entry.key] = { assetId: entry.asset_id, layerIdx: 0, sha256: sha, diskSha256, legacy: true, ...(sourceRel ? { sourceRel } : {}) };
1314
+ nextSynced[entry.key] = {
1315
+ assetId: entry.asset_id,
1316
+ layerIdx: 0,
1317
+ sha256: sha,
1318
+ diskSha256,
1319
+ legacy: true,
1320
+ ...(sourceRel ? { sourceRel } : {}),
1321
+ ...(entry.released_at ? { releasedAt: entry.released_at } : {}),
1322
+ ...(entry.released_version != null ? { releasedVersion: entry.released_version } : {}),
1323
+ };
1288
1324
  continue;
1289
1325
  }
1290
1326
  nextSynced[entry.key] = {
@@ -1294,6 +1330,8 @@ async function runOnce(opts, runOpts = {}) {
1294
1330
  diskSha256,
1295
1331
  ...(typeof entry.layer_count === 'number' ? { layers: entry.layer_count } : {}),
1296
1332
  ...(sourceRel ? { sourceRel } : {}),
1333
+ ...(entry.released_at ? { releasedAt: entry.released_at } : {}),
1334
+ ...(entry.released_version != null ? { releasedVersion: entry.released_version } : {}),
1297
1335
  };
1298
1336
  }
1299
1337
  const nextState = {
@@ -1372,6 +1410,8 @@ async function runOnce(opts, runOpts = {}) {
1372
1410
  if (result.failed > 0 && verbose) {
1373
1411
  console.log(kleur.yellow(`\n${result.failed} download${result.failed === 1 ? '' : 's'} failed — lastSync not advanced. Re-run to retry.`));
1374
1412
  }
1413
+ onStatus?.('Checking your game files…');
1414
+ const pushed = await maybePushLocalSprites(config.push, verbose, gameIndex, live && !!runOpts.watchMode, onStatus, conflictSet);
1375
1415
  if (verbose) {
1376
1416
  console.log();
1377
1417
  const summary = `downloaded ${downloaded} (added ${result.added.length}, modified ${result.modified.length}), ` +
@@ -1380,25 +1420,23 @@ async function runOnce(opts, runOpts = {}) {
1380
1420
  (shouldPrune ? `, pruned ${result.removed.length}` : '') +
1381
1421
  (renamed.length ? `, renamed ${renamed.length}` : '') +
1382
1422
  (result.failed ? `, failed ${result.failed}` : '');
1383
- console.log(result.failed ? kleur.yellow(`done with errors. ${summary}`) : kleur.green(`✓ done. ${summary}`));
1384
- // Per-file change list. Suppress renames here — printRenames below owns
1385
- // the rename block (it adds the import-update hints). Letting both fire
1386
- // would print each rename twice.
1423
+ const pushFailed = (pushed?.conflict ?? 0) + (pushed?.error ?? 0);
1424
+ console.log(result.failed || pushFailed
1425
+ ? kleur.yellow(`Sync finished with issues. ${summary}`)
1426
+ : kleur.green(`✓ Sync complete. ${summary}`));
1387
1427
  printChanges(result, ' ', { includeRenames: renamed.length === 0 });
1388
1428
  if (renamed.length > 0)
1389
1429
  printRenames(renamed, { withHints: true });
1390
1430
  }
1391
- onStatus?.('Checking your game files…');
1392
- const pushed = await maybePushLocalSprites(config.push, verbose, gameIndex, live && !!runOpts.watchMode, onStatus);
1393
1431
  if (result.failed)
1394
1432
  process.exitCode = 1;
1395
1433
  return { ...result, pushed };
1396
1434
  }
1397
- async function maybePushLocalSprites(pushEnabled, verbose, gameIndex, announceErrors = verbose, onStatus) {
1435
+ async function maybePushLocalSprites(pushEnabled, verbose, gameIndex, announceErrors = verbose, onStatus, excludeKeys) {
1398
1436
  if (pushEnabled === false)
1399
1437
  return null;
1400
1438
  try {
1401
- return await runPush({ quiet: !verbose, gameIndex, onStatus });
1439
+ return await runPush({ quiet: !verbose, nestedSync: true, gameIndex, onStatus, excludeKeys });
1402
1440
  }
1403
1441
  catch (e) {
1404
1442
  if (announceErrors) {
@@ -1474,11 +1512,13 @@ function humanTime(iso) {
1474
1512
  function printConflicts(keys, removedRemotely = new Set()) {
1475
1513
  console.log();
1476
1514
  console.log(kleur.yellow(`${keys.length} sprite${keys.length === 1 ? '' : 's'} changed in both places — not overwritten:`));
1477
- for (const key of keys) {
1515
+ for (const key of keys.slice(0, 10)) {
1478
1516
  const note = removedRemotely.has(key) ? kleur.dim(' (no longer synced from MagicPixel)') : '';
1479
1517
  console.log(` ${kleur.yellow('~')} ${key}${note}`);
1480
1518
  }
1481
- console.log(kleur.dim(` Keep the local edit: run \`${cmd('push')}\` (it will report the cloud change), or delete the PNG to accept MagicPixel's copy.`));
1519
+ if (keys.length > 10)
1520
+ console.log(kleur.dim(` …and ${keys.length - 10} more`));
1521
+ console.log(kleur.dim(` Use MagicPixel's copy: press Sync on that artboard. Keep the local edit: run \`${cmd('push --force')}\`.`));
1482
1522
  if (removedRemotely.size > 0) {
1483
1523
  console.log(kleur.dim(` Marked (no longer synced): re-check "Sync to Unity" (or restore the document) to push the edit up, or delete the PNG to accept the removal.`));
1484
1524
  }
package/dist/index.js CHANGED
@@ -97,7 +97,8 @@ program
97
97
  .description('Upload local sprite edits back to MagicPixel (two-way sync)')
98
98
  .option('--dry-run', 'Print what would be pushed without sending anything')
99
99
  .option('--flatten', 'Allow replacing multi-layer artboards with the flat local image')
100
- .addHelpText('after', '\nExamples:\n $ magicpixel push --dry-run # see what changed on disk\n $ magicpixel push # send local edits + new sprites\n')
100
+ .option('--force', 'Keep your local copy when a sprite also changed in MagicPixel')
101
+ .addHelpText('after', '\nExamples:\n $ magicpixel push --dry-run # see what changed on disk\n $ magicpixel push # send local edits + new sprites\n $ magicpixel push --force # your local copy wins on both-sides-changed\n')
101
102
  .action(wrap("push", async (opts) => pushCommand(opts)));
102
103
  program
103
104
  .command('connect <glob>')
@@ -14,10 +14,30 @@
14
14
  * that file, the entry is reported instead of downloaded.
15
15
  */
16
16
  import { isLocalEdit } from './localEdit.js';
17
+ /** A newer editor Sync press explicitly chooses MagicPixel for this sprite. */
18
+ export function hasNewExplicitRelease(opts) {
19
+ if (opts.releasedVersion != null && opts.previousReleasedVersion != null) {
20
+ return opts.releasedVersion > opts.previousReleasedVersion;
21
+ }
22
+ if (!opts.releasedAt)
23
+ return false;
24
+ const released = Date.parse(opts.releasedAt);
25
+ if (!Number.isFinite(released))
26
+ return false;
27
+ const baseline = opts.previousReleasedAt ?? opts.lastSync;
28
+ if (!baseline)
29
+ return false;
30
+ const previous = Date.parse(baseline);
31
+ return Number.isFinite(previous) && released > previous;
32
+ }
17
33
  export function decidePull(opts) {
18
34
  const cloud = opts.cloudSha256 ?? null;
19
35
  const local = opts.localSha256;
20
36
  const prev = opts.previousCloudSha256;
37
+ // A fresh explicit release is the user's scoped instruction for the cloud
38
+ // copy to replace this PNG, even if the PNG also changed locally.
39
+ if (hasNewExplicitRelease(opts))
40
+ return 'pull';
21
41
  // Missing on disk → restore from cloud (MagicPixel-only art and deletions).
22
42
  if (!local)
23
43
  return 'pull';
@@ -145,7 +145,10 @@ export function planPush(candidates, synced, opts = {}) {
145
145
  key: c.key,
146
146
  assetId: known.assetId,
147
147
  layerIdx: known.layerIdx,
148
- baseSha256: known.sha256,
148
+ // `sync` records the live cloud hash when it protects a dual edit.
149
+ // A later explicit `push` can then safely keep the local copy without
150
+ // bypassing the server's optimistic-concurrency check.
151
+ baseSha256: known.pendingCloudSha256 ?? known.sha256,
149
152
  });
150
153
  continue;
151
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magicpixelart/cli",
3
- "version": "0.5.22",
3
+ "version": "0.5.24",
4
4
  "description": "Sync MagicPixel pixel-art assets to your local project.",
5
5
  "type": "module",
6
6
  "bin": {