@magicpixelart/cli 0.4.0 → 0.5.0
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 +16 -0
- package/dist/commands/sync.js +126 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] — 2026-06-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Legacy slug-folder sweep.** When a server-side slug rule change renames
|
|
14
|
+
a doc (e.g. `cards-2` → `cards`) and the prior id→key snapshot is missing
|
|
15
|
+
(fresh clone, CI runner, snapshot wipe), `sync` now detects top-level
|
|
16
|
+
`outDir` folders matching `<currentSlug>-<n>` for any slug known from the
|
|
17
|
+
manifest OR the prior persisted snapshot, and prunes them whole. Always
|
|
18
|
+
prints a `Legacy slug folders removed — update your imports:` block with
|
|
19
|
+
find/replace hints so users fix any source code references. Runs in both
|
|
20
|
+
full and incremental mode; respects `--no-prune`. Sibling slugs that are
|
|
21
|
+
themselves valid (e.g. a user-named `tiles-2/` next to `tiles/`) are
|
|
22
|
+
never deleted because the "known slugs" set unions the live manifest
|
|
23
|
+
with the persisted id→key map.
|
|
24
|
+
|
|
10
25
|
### Fixed
|
|
26
|
+
|
|
11
27
|
- **`sync --watch 1` silently coerced to 2.** The commander validator
|
|
12
28
|
accepted `1–3600` but the watch loop enforced a 2s floor via
|
|
13
29
|
`Math.max(2, …)`, so `--watch 1` quietly ran at 2s with no warning.
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { mkdir, writeFile, unlink, rename } from 'node:fs/promises';
|
|
4
|
+
import { mkdir, writeFile, unlink, rename, readdir, rm } from 'node:fs/promises';
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { dirname, relative, resolve } from 'node:path';
|
|
7
7
|
import { loadConfig, loadState, saveState } from '../config.js';
|
|
@@ -369,6 +369,38 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
369
369
|
for (const p of renameStalePaths)
|
|
370
370
|
orphanSet.add(p);
|
|
371
371
|
orphans = [...orphanSet];
|
|
372
|
+
// Legacy-suffix folder sweep — always runs, even in incremental mode.
|
|
373
|
+
//
|
|
374
|
+
// Background: server-side slug uniqueness rules have changed over time
|
|
375
|
+
// (e.g. per-user → per-project). A doc that used to live under
|
|
376
|
+
// `outDir/cards-2/` may now report slug `cards` in the manifest. If the
|
|
377
|
+
// CLI's prior snapshot doesn't have the old id→key mapping (fresh clone,
|
|
378
|
+
// CI runner, snapshot wipe), rename detection finds nothing and the stale
|
|
379
|
+
// folder lingers next to the current one, breaking the user's imports of
|
|
380
|
+
// `@/assets/.../cards-2/...`.
|
|
381
|
+
//
|
|
382
|
+
// We detect this by looking for top-level disk folders whose name matches
|
|
383
|
+
// `<currentSlug>-<n>` for any slug currently in the manifest. Those are
|
|
384
|
+
// unambiguously legacy suffix collisions. We prune the whole folder
|
|
385
|
+
// (when --prune is on) and surface a clear "update your imports" notice
|
|
386
|
+
// so the user knows their source code references must be migrated.
|
|
387
|
+
// Build the "known top-level slugs" set from BOTH the current manifest AND
|
|
388
|
+
// the prior id→key snapshot. In incremental (`--watch`) mode the manifest
|
|
389
|
+
// only contains rows changed since `lastSync`, so a user with two
|
|
390
|
+
// legitimate sibling slugs (`tiles/` + `tiles-2/`) would lose `tiles-2/`
|
|
391
|
+
// the moment only `tiles` appeared in a delta. Pulling from
|
|
392
|
+
// `previousAssets` (the persisted full snapshot) closes that hole.
|
|
393
|
+
const knownFolderSlugs = new Set();
|
|
394
|
+
for (const e of manifest) {
|
|
395
|
+
if (e.folder)
|
|
396
|
+
knownFolderSlugs.add(e.folder.split('/')[0]);
|
|
397
|
+
}
|
|
398
|
+
for (const key of Object.values(previousAssets)) {
|
|
399
|
+
const top = key.split('/')[0];
|
|
400
|
+
if (top)
|
|
401
|
+
knownFolderSlugs.add(top);
|
|
402
|
+
}
|
|
403
|
+
const legacyFolders = await findLegacySuffixFolders(config.outDir, knownFolderSlugs);
|
|
372
404
|
if (verbose) {
|
|
373
405
|
console.log();
|
|
374
406
|
console.log(kleur.bold('Plan:'));
|
|
@@ -376,6 +408,10 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
376
408
|
console.log(` ${kleur.dim('=')} unchanged ${unchanged}${bytesSaved ? kleur.dim(` (~${formatBytes(bytesSaved)} saved)`) : ''}`);
|
|
377
409
|
if (renamed.length)
|
|
378
410
|
console.log(` ${kleur.cyan('↪')} renamed ${renamed.length}`);
|
|
411
|
+
if (legacyFolders.length) {
|
|
412
|
+
const verb = shouldPrune ? kleur.red('delete') : kleur.yellow('keep');
|
|
413
|
+
console.log(` ${verb} ${legacyFolders.length} legacy slug folder${legacyFolders.length === 1 ? '' : 's'}`);
|
|
414
|
+
}
|
|
379
415
|
if (since && !renamed.length) {
|
|
380
416
|
console.log(` ${kleur.dim('orphan check skipped (incremental — use --full)')}`);
|
|
381
417
|
}
|
|
@@ -392,6 +428,8 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
392
428
|
printRenames(renamed, { withHints: false });
|
|
393
429
|
if (orphans.length > 0)
|
|
394
430
|
printOrphans(orphans);
|
|
431
|
+
if (legacyFolders.length > 0)
|
|
432
|
+
printLegacyFolders(legacyFolders);
|
|
395
433
|
}
|
|
396
434
|
return {
|
|
397
435
|
added: [],
|
|
@@ -523,6 +561,36 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
523
561
|
else if (verbose && orphans.length > 0) {
|
|
524
562
|
printOrphans(orphans);
|
|
525
563
|
}
|
|
564
|
+
// Legacy-suffix folder sweep. Runs in BOTH full and incremental mode so a
|
|
565
|
+
// user who is hot off a server-side slug rule change recovers without
|
|
566
|
+
// having to remember `--full`. Folders are deleted whole (children
|
|
567
|
+
// included) because every PNG inside is, by definition, stale; their
|
|
568
|
+
// canonical copies live under the de-suffixed folder we just downloaded.
|
|
569
|
+
if (shouldPrune && legacyFolders.length > 0) {
|
|
570
|
+
const outRoot = resolve(process.cwd(), config.outDir);
|
|
571
|
+
for (const lf of legacyFolders) {
|
|
572
|
+
assertPathInsideRoot(lf.abs, outRoot, 'outDir');
|
|
573
|
+
try {
|
|
574
|
+
await rm(lf.abs, { recursive: true, force: true });
|
|
575
|
+
// Push a synthetic key so the watch loop's changedCount picks this up
|
|
576
|
+
// (otherwise a tick that ONLY swept legacy folders reports "no changes").
|
|
577
|
+
result.removed.push(`${lf.legacyName}/ (legacy)`);
|
|
578
|
+
if (verbose) {
|
|
579
|
+
console.log(` ${kleur.red('-')} ${relative(process.cwd(), lf.abs)} ${kleur.dim(`(legacy slug — now ${lf.currentSlug}/)`)}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch (e) {
|
|
583
|
+
if (verbose)
|
|
584
|
+
console.log(` ${kleur.yellow('!')} failed to remove legacy folder ${relative(process.cwd(), lf.abs)}: ${e.message}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Always surface the import-update notice — even in --quiet/watch mode
|
|
588
|
+
// — because user source code now references paths that no longer exist.
|
|
589
|
+
printLegacyFolders(legacyFolders);
|
|
590
|
+
}
|
|
591
|
+
else if (verbose && legacyFolders.length > 0) {
|
|
592
|
+
printLegacyFolders(legacyFolders);
|
|
593
|
+
}
|
|
526
594
|
// Always persist the id → key snapshot so rename detection survives even
|
|
527
595
|
// when `emitIndex` is toggled off-then-on. `lastSync` only advances on a
|
|
528
596
|
// clean run, so partial-failure syncs are retried from the prior cursor.
|
|
@@ -687,5 +755,62 @@ function printOrphans(orphans) {
|
|
|
687
755
|
}
|
|
688
756
|
console.log(kleur.dim(' You passed --no-prune, so these were kept. Remove the flag to delete them.'));
|
|
689
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Find top-level disk folders whose name matches `<currentSlug>-<n>` for some
|
|
760
|
+
* slug present in the manifest. These are unambiguously legacy artifacts
|
|
761
|
+
* from a server-side slug rule change (e.g. per-user → per-project
|
|
762
|
+
* uniqueness) — the canonical copy now lives under `<currentSlug>/`, and the
|
|
763
|
+
* suffixed folder's PNGs are stale.
|
|
764
|
+
*
|
|
765
|
+
* Only scans the FIRST level under outDir; nested folders are user content.
|
|
766
|
+
* Returns `[]` when outDir doesn't exist or when no remote slugs are known
|
|
767
|
+
* (defensive — never wipe folders when we have no manifest to compare to).
|
|
768
|
+
*/
|
|
769
|
+
export async function findLegacySuffixFolders(outDir, knownFolderSlugs, cwd = process.cwd()) {
|
|
770
|
+
if (knownFolderSlugs.size === 0)
|
|
771
|
+
return [];
|
|
772
|
+
const root = resolve(cwd, outDir);
|
|
773
|
+
if (!existsSync(root))
|
|
774
|
+
return [];
|
|
775
|
+
let entries;
|
|
776
|
+
try {
|
|
777
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
return [];
|
|
781
|
+
}
|
|
782
|
+
const out = [];
|
|
783
|
+
for (const ent of entries) {
|
|
784
|
+
if (!ent.isDirectory())
|
|
785
|
+
continue;
|
|
786
|
+
if (ent.name.startsWith('.'))
|
|
787
|
+
continue;
|
|
788
|
+
// Skip folders that ARE known slugs — only suffixed siblings are suspect.
|
|
789
|
+
if (knownFolderSlugs.has(ent.name))
|
|
790
|
+
continue;
|
|
791
|
+
const m = /^(.+)-(\d+)$/.exec(ent.name);
|
|
792
|
+
if (!m)
|
|
793
|
+
continue;
|
|
794
|
+
const base = m[1];
|
|
795
|
+
if (!knownFolderSlugs.has(base))
|
|
796
|
+
continue;
|
|
797
|
+
out.push({
|
|
798
|
+
abs: resolve(root, ent.name),
|
|
799
|
+
legacyName: ent.name,
|
|
800
|
+
currentSlug: base,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
return out;
|
|
804
|
+
}
|
|
805
|
+
function printLegacyFolders(legacy) {
|
|
806
|
+
if (legacy.length === 0)
|
|
807
|
+
return;
|
|
808
|
+
console.log();
|
|
809
|
+
console.log(kleur.bold('Legacy slug folders removed — update your imports:'));
|
|
810
|
+
for (const lf of legacy) {
|
|
811
|
+
console.log(` ${kleur.red('-')} ${lf.legacyName}/ ${kleur.dim(`→ now ${lf.currentSlug}/`)}`);
|
|
812
|
+
console.log(` ${kleur.dim(`Find/replace in your project: ${lf.legacyName}/ → ${lf.currentSlug}/`)}`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
690
815
|
// Note: orphan scan delegates to `walkOutDirPngs` in util/paths.ts (shared
|
|
691
816
|
// with the typed-index emitter). `maxIsoTimestamp` lives in util/iso.ts.
|