@magicpixelart/cli 0.5.31 → 0.5.34

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
@@ -5,6 +5,24 @@ All notable changes to `@magicpixelart/cli` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [0.5.34] - 2026-10-06
9
+
10
+ ### Fixed
11
+ - `push --force` no longer fails with "Too many sprites (max 20 per request)" when more than 20 files are retried; the retry round is now sent in small batches.
12
+
13
+ ## [0.5.33] - 2026-10-06
14
+
15
+ ### Fixed
16
+ - Local edits to game files that were never changed in MagicPixel no longer fail as "also changed in MagicPixel". Push now sends the cloud timestamp it last saw, and sync no longer records the local file as MagicPixel's copy when the server has no cached hash.
17
+
18
+ ### Changed
19
+ - Watch header now reads "N game sprites connected" — it counts game PNGs matched by `connect`, not files waiting in MagicPixel.
20
+
21
+ ## [0.5.32] - 2026-10-05
22
+
23
+ ### Fixed
24
+ - `sync --watch` status lines are clipped to the terminal width, so a long sprite path no longer wraps and leaves the previous status piled on screen.
25
+
8
26
  ## [0.5.31] - 2026-10-01
9
27
 
10
28
  ### Added
package/dist/api.js CHANGED
@@ -423,6 +423,15 @@ function isTransientPushError(e) {
423
423
  export async function pushSpritesAdaptive(sprites, retryOpts) {
424
424
  if (sprites.length === 0)
425
425
  return [];
426
+ // Never send more than one batch per request — callers (e.g. the --force
427
+ // retry round) may hand over every refused sprite at once.
428
+ if (sprites.length > PUSH_BATCH_SIZE) {
429
+ const out = [];
430
+ for (let i = 0; i < sprites.length; i += PUSH_BATCH_SIZE) {
431
+ out.push(...(await pushSpritesAdaptive(sprites.slice(i, i + PUSH_BATCH_SIZE), retryOpts)));
432
+ }
433
+ return out;
434
+ }
426
435
  try {
427
436
  return await pushSprites(sprites, retryOpts);
428
437
  }
@@ -215,6 +215,7 @@ export async function runPushWith(config, state, opts = {}) {
215
215
  assetId: a.assetId,
216
216
  layerIdx: a.layerIdx,
217
217
  baseSha256: a.baseSha256,
218
+ ...(a.baseUpdatedAt ? { baseUpdatedAt: a.baseUpdatedAt } : {}),
218
219
  flatten: opts.flatten === true,
219
220
  ...(opts.replace ? { replace: true } : {}),
220
221
  });
@@ -23,7 +23,7 @@ import { maxIsoTimestamp } from '../util/iso.js';
23
23
  import { formatBytes } from '../util/format.js';
24
24
  import { computePreviousKeyOrphans } from '../util/previousKeyOrphans.js';
25
25
  import { cmd } from '../util/invoke.js';
26
- import { formatSlowTickLine, formatWatchSpriteLine, SLOW_TICK_HEARTBEAT_MS } from '../util/watchCopy.js';
26
+ import { fitStatusLine, formatSlowTickLine, formatWatchSpriteLine, SLOW_TICK_HEARTBEAT_MS } from '../util/watchCopy.js';
27
27
  import { canSkipWithoutHashing, decidePull, hasNewExplicitRelease } from '../util/pullDecision.js';
28
28
  import { hasUnpushedLocalEdit } from '../util/localEdit.js';
29
29
  import { shouldReconcile } from '../util/reconcile.js';
@@ -227,7 +227,7 @@ async function watchLoop(opts) {
227
227
  const onStatus = (msg) => {
228
228
  if (opts.quiet)
229
229
  return;
230
- process.stdout.write(`\r\x1b[2K${kleur.dim(`${timestamp()} ${msg}`)}`);
230
+ writeStatusLine(msg);
231
231
  };
232
232
  // Heartbeat: a tick that legitimately takes minutes (huge first pull, slow
233
233
  // link) must not look like the silent hang a timeout-less fetch used to
@@ -307,7 +307,7 @@ async function watchLoop(opts) {
307
307
  printChanges(r, /* indent */ ' ');
308
308
  }
309
309
  else {
310
- process.stdout.write(`\r\x1b[2K${kleur.dim(`${timestamp()} Waiting for edits… (${r.unchanged} up to date)`)}`);
310
+ writeStatusLine(`Waiting for edits… (${r.unchanged} up to date)`);
311
311
  }
312
312
  }
313
313
  catch (e) {
@@ -1377,12 +1377,14 @@ async function runOnce(opts, runOpts = {}) {
1377
1377
  // After a write, the actual downloaded bytes are the disk/cloud baseline.
1378
1378
  // The manifest hash may be null or represent a cached composite while the
1379
1379
  // download endpoint serves storage bytes for a single-artboard document.
1380
+ const prev = nextSynced[entry.key];
1381
+ // Never let the local game file stand in for MagicPixel's composite when
1382
+ // the manifest has no cached hash: keep the last real cloud baseline.
1380
1383
  const sha = writtenKeys.has(entry.key)
1381
1384
  ? await fileSha256(pathFor(entry))
1382
- : (entry.sha256 ?? (await fileSha256(pathFor(entry))));
1385
+ : (entry.sha256 ?? prev?.sha256 ?? (await fileSha256(pathFor(entry))));
1383
1386
  if (!sha)
1384
1387
  continue;
1385
- const prev = nextSynced[entry.key];
1386
1388
  const diskSha256 = writtenKeys.has(entry.key)
1387
1389
  ? sha
1388
1390
  : (shaByEntryId.get(entry.id) ?? prev?.diskSha256 ?? sha);
@@ -1575,6 +1577,11 @@ function progressText(done, total, bytes) {
1575
1577
  const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
1576
1578
  return `${bar} ${done}/${total} ${kleur.dim(formatBytes(bytes))}`;
1577
1579
  }
1580
+ /** Rewrite the watcher's single status row, clipped so it never wraps. */
1581
+ function writeStatusLine(msg) {
1582
+ const line = fitStatusLine(`${timestamp()} ${msg}`, process.stdout.columns);
1583
+ process.stdout.write(`\r\x1b[2K${kleur.dim(line)}`);
1584
+ }
1578
1585
  function timestamp() {
1579
1586
  const d = new Date();
1580
1587
  return `[${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}]`;
@@ -151,6 +151,7 @@ export function planPush(candidates, synced, opts = {}) {
151
151
  // A later explicit `push` can then safely keep the local copy without
152
152
  // bypassing the server's optimistic-concurrency check.
153
153
  baseSha256: known.pendingCloudSha256 ?? known.sha256,
154
+ ...(known.cloudUpdatedAt && !known.pendingCloudSha256 ? { baseUpdatedAt: known.cloudUpdatedAt } : {}),
154
155
  });
155
156
  continue;
156
157
  }
@@ -13,10 +13,10 @@ export function formatWatchSpriteLine(c) {
13
13
  if (set === 0 && pulled === 0)
14
14
  return null;
15
15
  if (set > 0 && pulled > 0 && set !== pulled) {
16
- return ` Sprites: ${fmt(set)} in your game · ${fmt(pulled)} last pulled from MagicPixel`;
16
+ return ` Sprites: ${fmt(set)} game sprites connected · ${fmt(pulled)} last pulled from MagicPixel`;
17
17
  }
18
18
  if (set > 0)
19
- return ` Sprites: ${fmt(set)} in your game`;
19
+ return ` Sprites: ${fmt(set)} game sprites connected`;
20
20
  return ` Sprites: ${fmt(pulled)}`;
21
21
  }
22
22
  /** How long a single watch tick may run before the heartbeat starts printing. */
@@ -39,3 +39,19 @@ export function formatSlowTickLine(elapsedSec, lastStatus) {
39
39
  ? `${what} — still working (${elapsed})`
40
40
  : `Still working (${elapsed})`;
41
41
  }
42
+ /**
43
+ * Fit a carriage-return status line to one terminal row. `\r\x1b[2K` only
44
+ * clears the row the cursor is on, so a line that wraps leaves its first rows
45
+ * behind and the next status prints after them (long "Looking through your
46
+ * game sprites… <folder>" lines piled up this way). No width (not a TTY) →
47
+ * unchanged.
48
+ */
49
+ export function fitStatusLine(line, columns) {
50
+ if (!columns || columns < 2)
51
+ return line;
52
+ const max = columns - 1; // leave the last cell free so the cursor can't wrap
53
+ const chars = Array.from(line);
54
+ if (chars.length <= max)
55
+ return line;
56
+ return `${chars.slice(0, max - 1).join('')}…`;
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magicpixelart/cli",
3
- "version": "0.5.31",
3
+ "version": "0.5.34",
4
4
  "description": "Sync MagicPixel pixel-art assets to your local project.",
5
5
  "type": "module",
6
6
  "bin": {