@magicpixelart/cli 0.5.7 → 0.5.8

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,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.8] — 2026-08-26
11
+
12
+ ### Added
13
+
14
+ - **Connect imports game sprites.** `sync` (and `sync --watch`) now runs `push`
15
+ automatically so connecting a game project imports existing sprites into the
16
+ MagicPixel library, flagged Sync to Unity so they appear under Connected.
17
+ `"push": false` in `magicpixel.json` opts out.
18
+ - **First-connect import.** On Unity/Godot/GameMaker, PNGs under `Assets/` /
19
+ `assets/` / `datafiles/` are copied into `outDir` if they aren't there yet
20
+ (skipping Library/Temp/Packages, never overwriting) and then adopted.
21
+ Root-level PNGs (`hero.png`) adopt as a single-artboard document instead of
22
+ being skipped.
23
+
10
24
  ## [0.5.7] — 2026-08-26
11
25
 
12
26
  ### Added
package/README.md CHANGED
@@ -138,6 +138,7 @@ Sync is built to be cheap: a no-op run is one small manifest request, zero PNG b
138
138
  | `emitIndex`| `boolean` | `true` | Emit `<outDir>/index.ts` with typed asset map. |
139
139
  | `unityPpu` | `number?` | `32` | Unity only: pixels-per-unit in generated `.meta`. |
140
140
  | `unitySyncAll` | `boolean?` | `false` | Unity only: sync every artboard, not just those flagged in the editor. |
141
+ | `push` | `boolean?` | `true` | Upload local PNG changes to MagicPixel on every `sync`. Set `false` for pull-only. |
141
142
  | `endpoint` | `string?` | production URL | Override the API base (testing only). Must be **HTTPS**. |
142
143
 
143
144
  State (`.magicpixel/state.json`) tracks `lastSync` (file mode `0600`). Add `.magicpixel/` to `.gitignore` (init offers to do this).
@@ -22,11 +22,30 @@ import { fileSha256 } from '../util/hash.js';
22
22
  import { assetDiskPathFromKey, walkOutDirPngs } from '../util/paths.js';
23
23
  import { planPush } from '../util/pushPlan.js';
24
24
  import { cmd } from '../util/invoke.js';
25
- export async function pushCommand(opts) {
25
+ import { detectProjectKind } from '../util/framework.js';
26
+ import { importNearbyGamePngs } from '../util/gameScan.js';
27
+ export async function pushCommand(opts = {}) {
28
+ await runPush(opts);
29
+ }
30
+ /**
31
+ * Scan local PNGs and upload new/changed ones into the MagicPixel library.
32
+ * Used by `magicpixel push` and by `sync` (so connect/watch imports game
33
+ * sprites without a second command).
34
+ */
35
+ export async function runPush(opts = {}) {
26
36
  const config = await loadConfig();
27
37
  const state = await loadState();
28
- const spinner = ora('Scanning local sprites…').start();
38
+ return runPushWith(config, state, opts);
39
+ }
40
+ export async function runPushWith(config, state, opts = {}) {
41
+ const quiet = opts.quiet === true;
42
+ const empty = { created: 0, updated: 0, unchanged: 0, conflict: 0, error: 0, imported: 0 };
43
+ const spinner = quiet ? null : ora('Scanning local sprites…').start();
44
+ const kind = await detectProjectKind();
45
+ const scan = await importNearbyGamePngs(config.outDir, kind);
46
+ const imported = scan.copied;
29
47
  const disk = await walkOutDirPngs(config.outDir);
48
+ const absByKey = new Map(disk.map((a) => [a.key, a.abs]));
30
49
  const candidates = [];
31
50
  for (const asset of disk) {
32
51
  const sha = await fileSha256(asset.abs);
@@ -43,13 +62,14 @@ export async function pushCommand(opts) {
43
62
  const legacy = actions.filter((a) => a.kind === 'skip' && a.reason === 'legacy');
44
63
  const flattenBlocked = actions.filter((a) => a.kind === 'needs-flatten');
45
64
  const sendable = actions.filter((a) => a.kind === 'update' || a.kind === 'adopt');
46
- spinner.succeed(`Local sprites: ${candidates.length} · to push ${sendable.length} · unchanged ${skipped}`);
47
- if (legacy.length > 0) {
65
+ spinner?.succeed(`Local sprites: ${candidates.length} · to push ${sendable.length} · unchanged ${skipped}` +
66
+ (imported ? ` · imported ${imported}` : ''));
67
+ if (legacy.length > 0 && !quiet) {
48
68
  console.log();
49
69
  console.log(kleur.yellow(`! ${legacy.length} sprite${legacy.length === 1 ? '' : 's'} came from a legacy single-image file — skipped.`));
50
70
  console.log(kleur.dim(` Fix: open the file in MagicPixel and save it once, then re-run \`${cmd('sync')}\`.`));
51
71
  }
52
- if (flattenBlocked.length > 0) {
72
+ if (flattenBlocked.length > 0 && !quiet) {
53
73
  console.log();
54
74
  console.log(kleur.yellow(`! ${flattenBlocked.length} sprite${flattenBlocked.length === 1 ? '' : 's'} would flatten a multi-layer artboard — skipped.`));
55
75
  for (const a of flattenBlocked.slice(0, 10)) {
@@ -58,23 +78,28 @@ export async function pushCommand(opts) {
58
78
  console.log(kleur.dim(' Fix: re-run with --flatten to replace them with the flat disk image.'));
59
79
  }
60
80
  if (sendable.length === 0) {
61
- console.log(kleur.green('✓ nothing to push.'));
62
- return;
81
+ if (!quiet)
82
+ console.log(kleur.green('✓ nothing to push.'));
83
+ return { ...empty, imported, unchanged: skipped };
63
84
  }
64
85
  if (opts.dryRun) {
65
- console.log();
66
- for (const a of sendable) {
67
- const verb = a.kind === 'update' ? kleur.yellow('~') : kleur.green('+');
68
- console.log(` ${verb} ${a.key}${a.kind === 'adopt' ? kleur.dim(' (new)') : ''}`);
86
+ if (!quiet) {
87
+ console.log();
88
+ for (const a of sendable) {
89
+ const verb = a.kind === 'update' ? kleur.yellow('~') : kleur.green('+');
90
+ console.log(` ${verb} ${a.key}${a.kind === 'adopt' ? kleur.dim(' (new)') : ''}`);
91
+ }
92
+ console.log(kleur.dim('--dry-run: nothing sent.'));
69
93
  }
70
- console.log(kleur.dim('--dry-run: nothing sent.'));
71
- return;
94
+ return { ...empty, imported };
72
95
  }
73
96
  const outRoot = resolve(process.cwd(), config.outDir);
74
97
  const shaByKey = new Map(candidates.map((c) => [c.key, c.diskSha256]));
75
98
  const sprites = [];
76
99
  for (const a of sendable) {
77
- const abs = resolve(outRoot, `${a.key}.png`);
100
+ const abs = absByKey.get(a.key);
101
+ if (!abs)
102
+ continue;
78
103
  const pngBase64 = (await readFile(abs)).toString('base64');
79
104
  const diskSha256 = shaByKey.get(a.key);
80
105
  if (a.kind === 'update') {
@@ -92,28 +117,23 @@ export async function pushCommand(opts) {
92
117
  sprites.push({ key: a.key, pngBase64, diskSha256, path: a.path, pathNames: a.pathNames, name: a.name });
93
118
  }
94
119
  }
95
- const progress = ora(`Pushing 0/${sprites.length}…`).start();
120
+ const progress = quiet ? null : ora(`Pushing 0/${sprites.length}…`).start();
96
121
  const results = [];
97
122
  for (let i = 0; i < sprites.length; i += PUSH_BATCH_SIZE) {
98
123
  const batch = sprites.slice(i, i + PUSH_BATCH_SIZE);
99
124
  const batchResults = await pushSprites(batch);
100
125
  results.push(...batchResults);
101
- progress.text = `Pushing ${Math.min(i + batch.length, sprites.length)}/${sprites.length}…`;
126
+ if (progress)
127
+ progress.text = `Pushing ${Math.min(i + batch.length, sprites.length)}/${sprites.length}…`;
102
128
  }
103
129
  const counts = { created: 0, updated: 0, unchanged: 0, conflict: 0, error: 0 };
104
130
  const nextSynced = { ...(state.synced ?? {}) };
105
131
  for (const r of results) {
106
132
  counts[r.status] = (counts[r.status] ?? 0) + 1;
107
- // The server may have stored the document under a different slug than the
108
- // disk folder (collision suffix, slugified name). Move the local files to
109
- // the key the manifest will report, or the next `sync` downloads a second
110
- // copy alongside this one.
111
133
  const stateKey = r.cloudKey && r.cloudKey !== r.key ? r.cloudKey : r.key;
112
134
  if (stateKey !== r.key && (r.status === 'created' || r.status === 'updated')) {
113
135
  await renameLocalSprite(config.outDir, r.key, stateKey);
114
136
  }
115
- // Store the sha the cloud now reports so the next `push` is a no-op and
116
- // the next `sync` doesn't see a phantom change.
117
137
  if ((r.status === 'created' || r.status === 'updated' || r.status === 'unchanged') && r.assetId && r.sha256) {
118
138
  if (stateKey !== r.key)
119
139
  delete nextSynced[r.key];
@@ -121,7 +141,6 @@ export async function pushCommand(opts) {
121
141
  assetId: r.assetId,
122
142
  layerIdx: typeof r.layerIdx === 'number' ? r.layerIdx : (nextSynced[stateKey]?.layerIdx ?? 0),
123
143
  sha256: r.sha256,
124
- // A push always leaves the artboard single-layer (the flat disk image).
125
144
  layers: 1,
126
145
  };
127
146
  }
@@ -131,24 +150,29 @@ export async function pushCommand(opts) {
131
150
  const summary = `created ${counts.created}, updated ${counts.updated}, unchanged ${counts.unchanged}` +
132
151
  (counts.conflict ? `, conflicts ${counts.conflict}` : '') +
133
152
  (counts.error ? `, errors ${counts.error}` : '');
134
- if (failed)
135
- progress.warn(`Push finished with issues. ${summary}`);
136
- else
137
- progress.succeed(`Pushed. ${summary}`);
138
- for (const r of results) {
139
- if (r.status === 'conflict') {
140
- console.log(` ${kleur.yellow('!')} ${r.key}: ${conflictHint(r.reason)}`);
153
+ if (progress) {
154
+ if (failed)
155
+ progress.warn(`Push finished with issues. ${summary}`);
156
+ else
157
+ progress.succeed(`Pushed. ${summary}`);
158
+ }
159
+ if (!quiet) {
160
+ for (const r of results) {
161
+ if (r.status === 'conflict') {
162
+ console.log(` ${kleur.yellow('!')} ${r.key}: ${conflictHint(r.reason)}`);
163
+ }
164
+ else if (r.status === 'error') {
165
+ console.log(` ${kleur.red('!')} ${r.key}: ${r.message ?? 'push failed'}`);
166
+ }
141
167
  }
142
- else if (r.status === 'error') {
143
- console.log(` ${kleur.red('!')} ${r.key}: ${r.message ?? 'push failed'}`);
168
+ if (counts.created > 0) {
169
+ console.log(kleur.dim(` ${counts.created} new sprite${counts.created === 1 ? '' : 's'} now live in your MagicPixel library ` +
170
+ `(folders mirrored from ${relative(process.cwd(), outRoot)}). Refresh the library to see them.`));
144
171
  }
145
172
  }
146
- if (counts.created > 0) {
147
- console.log(kleur.dim(` ${counts.created} new sprite${counts.created === 1 ? '' : 's'} now live in your MagicPixel library ` +
148
- `(folders mirrored from ${relative(process.cwd(), outRoot)}).`));
149
- }
150
173
  if (failed)
151
174
  process.exitCode = 1;
175
+ return { ...counts, imported };
152
176
  }
153
177
  function conflictHint(reason) {
154
178
  switch (reason) {
@@ -14,21 +14,16 @@ import { cmd } from '../util/invoke.js';
14
14
  /**
15
15
  * One-command bootstrap for new users. Runs init → key prompt → first sync →
16
16
  * prints copy-pasteable watch instructions. Designed so a non-technical user
17
- * can paste `npx @magicpixelart/cli start` and end up with sprites on disk and a watch
18
- * script ready to run.
17
+ * can paste `npx @magicpixelart/cli start` and end up with game sprites in
18
+ * MagicPixel (and flagged sprites back on disk) plus a watch script.
19
19
  */
20
20
  export async function startCommand(opts = {}) {
21
21
  console.log(kleur.bold('🪄 MagicPixel — first-run setup'));
22
- console.log(kleur.dim(' This walks you through getting your sprites onto disk.'));
22
+ console.log(kleur.dim(' This connects your game project so existing sprites show up in MagicPixel.'));
23
23
  console.log();
24
- // 1. `start` is JS-only — engine projects use init + login + sync --watch.
25
- if (!hasPackageJson()) {
26
- const kind = await detectProjectKind();
27
- if (isEngineKind(kind)) {
28
- console.log(kleur.yellow(` Detected ${kind}. \`${cmd('start')}\` is JS-only; run ` +
29
- `\`${cmd('init')} && ${cmd('login')} && ${cmd('sync')} --watch\` instead.`));
30
- return;
31
- }
24
+ const kind = await detectProjectKind();
25
+ // Engine projects have no package.json requirement. JS projects still need one.
26
+ if (!hasPackageJson() && !isEngineKind(kind)) {
32
27
  console.log(kleur.yellow(' MagicPixel needs a JavaScript project (package.json) to sync into.\n' +
33
28
  ' Run this inside your project folder — the one with package.json.'));
34
29
  return;
@@ -74,7 +69,7 @@ export async function startCommand(opts = {}) {
74
69
  // failures without throwing — snapshot around the call so we don't
75
70
  // print a misleading green "you're set up" over a half-failed run.
76
71
  console.log();
77
- console.log(kleur.bold('Step: first sync'));
72
+ console.log(kleur.bold('Step: import + sync'));
78
73
  const exitBefore = process.exitCode ?? 0;
79
74
  await syncCommand({ full: true });
80
75
  const firstSyncFailed = (process.exitCode ?? 0) > exitBefore;
@@ -21,6 +21,7 @@ import { maxIsoTimestamp } from '../util/iso.js';
21
21
  import { formatBytes } from '../util/format.js';
22
22
  import { computePreviousKeyOrphans } from '../util/previousKeyOrphans.js';
23
23
  import { cmd } from '../util/invoke.js';
24
+ import { runPush } from './push.js';
24
25
  export async function syncCommand(opts) {
25
26
  // Sweep any leaked `<file>.<pid>.<hex>.tmp` files left behind by a prior
26
27
  // crashed/killed CLI run before they pile up. Runs once per CLI invocation
@@ -373,16 +374,15 @@ async function runOnce(opts, runOpts = {}) {
373
374
  deselectedPaths.push(p);
374
375
  }
375
376
  if (filtered.noneFlagged) {
376
- // Every artboard is explicitly opted out. Pruning here would delete the
377
- // user's whole sprite folder off the back of an unchecked box, so bail
378
- // out with a hint instead.
377
+ // Nothing flagged for pull — don't dump the library, and don't prune.
378
+ // Local sprites still go up so they appear in MagicPixel on connect.
379
379
  if (verbose) {
380
380
  console.log();
381
- console.log(kleur.yellow('! No artboards are flagged "Sync to Unity" — nothing to sync.'));
382
- console.log(kleur.dim(' Fix: right-click an artboard in MagicPixel → "Sync to Unity",'));
383
- console.log(kleur.dim(' or set "unitySyncAll": true in magicpixel.json to sync everything.'));
381
+ console.log(kleur.yellow('! No artboards are flagged "Sync to Unity" — nothing to pull.'));
382
+ console.log(kleur.dim(' Existing game sprites will still be imported into MagicPixel.'));
383
+ console.log(kleur.dim(' Flag folders/artboards in the library to pull them back into Unity.'));
384
384
  }
385
- return {
385
+ const empty = {
386
386
  added: [],
387
387
  modified: [],
388
388
  removed: [],
@@ -392,6 +392,8 @@ async function runOnce(opts, runOpts = {}) {
392
392
  bytesSaved: 0,
393
393
  renamed: [],
394
394
  };
395
+ await maybePushLocalSprites(config.push, verbose);
396
+ return empty;
395
397
  }
396
398
  const skipped = manifest.length - filtered.entries.length;
397
399
  manifest = filtered.entries;
@@ -448,8 +450,7 @@ async function runOnce(opts, runOpts = {}) {
448
450
  // delete files outside what the current manifest delta touches).
449
451
  const localPngs = await walkOutDirPngs(config.outDir);
450
452
  // Sprites created in Unity (or any PNG we never pulled) are pending a
451
- // `magicpixel push`, not orphans — see prunePolicy.ts.
452
- let pendingPush = [];
453
+ // push into MagicPixel, not orphans — see prunePolicy.ts.
453
454
  if (!since) {
454
455
  const trackedPaths = new Set(Object.keys(state.synced ?? {})
455
456
  .concat(Object.values(previousAssets))
@@ -469,7 +470,6 @@ async function runOnce(opts, runOpts = {}) {
469
470
  isTracked: (p) => trackedPaths.has(p),
470
471
  });
471
472
  orphans = policy.orphans;
472
- pendingPush = policy.pendingPush;
473
473
  }
474
474
  // Heuristic-rename fallback for incremental mode.
475
475
  //
@@ -917,16 +917,26 @@ async function runOnce(opts, runOpts = {}) {
917
917
  printChanges(result, ' ', { includeRenames: renamed.length === 0 });
918
918
  if (renamed.length > 0)
919
919
  printRenames(renamed, { withHints: true });
920
- if (pendingPush.length > 0) {
921
- console.log();
922
- console.log(kleur.yellow(`! ${pendingPush.length} local sprite${pendingPush.length === 1 ? '' : 's'} ${pendingPush.length === 1 ? 'is' : 'are'} not in MagicPixel yet — kept on disk.`));
923
- console.log(kleur.dim(` Fix: run \`${cmd('push')}\` to add them to your library.`));
924
- }
925
920
  }
921
+ await maybePushLocalSprites(config.push, verbose);
926
922
  if (result.failed)
927
923
  process.exitCode = 1;
928
924
  return result;
929
925
  }
926
+ async function maybePushLocalSprites(pushEnabled, verbose) {
927
+ if (pushEnabled === false)
928
+ return;
929
+ try {
930
+ await runPush({ quiet: !verbose });
931
+ }
932
+ catch (e) {
933
+ if (verbose) {
934
+ console.log();
935
+ console.log(kleur.yellow(`! could not import local sprites into MagicPixel: ${e.message}`));
936
+ console.log(kleur.dim(` Fix: run \`${cmd('push')}\` once the connection is healthy.`));
937
+ }
938
+ }
939
+ }
930
940
  const CHANGE_PRINT_CAP = 50;
931
941
  function printChanges(r, indent, opts = { includeRenames: true }) {
932
942
  const lines = [];
package/dist/config.js CHANGED
@@ -86,6 +86,7 @@ export async function loadConfig(cwd = process.cwd()) {
86
86
  emitIndex,
87
87
  unityPpu,
88
88
  unitySyncAll: parsed.unitySyncAll === true ? true : undefined,
89
+ push: parsed.push === false ? false : undefined,
89
90
  };
90
91
  }
91
92
  const MAX_GLOBS = 64;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * First-connect import: when `outDir` has no PNGs yet, copy nearby game
3
+ * sprites into it so `push` can adopt them into the MagicPixel library.
4
+ *
5
+ * Only engine projects. Never walks Library/Temp/Packages. Never overwrites
6
+ * a file already in outDir. Capped so a huge Assets/ tree can't stampede
7
+ * the library.
8
+ */
9
+ import { existsSync } from 'node:fs';
10
+ import { copyFile, mkdir, readdir } from 'node:fs/promises';
11
+ import { dirname, relative, resolve } from 'node:path';
12
+ import { isEngineKind } from './framework.js';
13
+ import { assertPathInsideRoot } from './security.js';
14
+ /** Unity/engine folders that are never sprite sources. */
15
+ export const GAME_SCAN_SKIP_DIRS = new Set([
16
+ 'library',
17
+ 'temp',
18
+ 'obj',
19
+ 'logs',
20
+ 'packages',
21
+ 'projectsettings',
22
+ 'usersettings',
23
+ 'streamingassets',
24
+ ]);
25
+ export const MAX_GAME_IMPORT = 200;
26
+ export function gameScanRoot(kind) {
27
+ switch (kind) {
28
+ case 'Unity':
29
+ return 'Assets';
30
+ case 'Godot':
31
+ return 'assets';
32
+ case 'GameMaker':
33
+ return 'datafiles';
34
+ default:
35
+ return null;
36
+ }
37
+ }
38
+ /**
39
+ * Copy PNGs from the engine asset root into `outDir`, preserving relative
40
+ * paths. No-op when `outDir` already has PNGs, when the project isn't an
41
+ * engine, or when the scan root is missing.
42
+ */
43
+ export async function importNearbyGamePngs(outDir, kind, cwd = process.cwd()) {
44
+ const result = { copied: 0, skipped: 0, capped: false };
45
+ if (!isEngineKind(kind))
46
+ return result;
47
+ const scanRel = gameScanRoot(kind);
48
+ if (!scanRel)
49
+ return result;
50
+ const scanRoot = resolve(cwd, scanRel);
51
+ const destRoot = resolve(cwd, outDir);
52
+ if (!existsSync(scanRoot))
53
+ return result;
54
+ const destNorm = destRoot.replace(/\\/g, '/').toLowerCase();
55
+ const found = [];
56
+ await walkPngs(scanRoot, scanRoot, destNorm, found);
57
+ if (found.length >= MAX_GAME_IMPORT)
58
+ result.capped = true;
59
+ if (found.length > MAX_GAME_IMPORT)
60
+ found.length = MAX_GAME_IMPORT;
61
+ for (const abs of found) {
62
+ const rel = relative(scanRoot, abs).replace(/\\/g, '/');
63
+ const dest = resolve(destRoot, rel);
64
+ try {
65
+ assertPathInsideRoot(dest, destRoot, 'outDir');
66
+ }
67
+ catch {
68
+ result.skipped++;
69
+ continue;
70
+ }
71
+ if (existsSync(dest)) {
72
+ result.skipped++;
73
+ continue;
74
+ }
75
+ await mkdir(dirname(dest), { recursive: true });
76
+ await copyFile(abs, dest);
77
+ result.copied++;
78
+ }
79
+ return result;
80
+ }
81
+ async function walkPngs(dir, root, destNorm, out) {
82
+ if (out.length >= MAX_GAME_IMPORT)
83
+ return;
84
+ let entries;
85
+ try {
86
+ entries = await readdir(dir, { withFileTypes: true });
87
+ }
88
+ catch {
89
+ return;
90
+ }
91
+ for (const ent of entries) {
92
+ if (out.length >= MAX_GAME_IMPORT)
93
+ return;
94
+ if (ent.isSymbolicLink())
95
+ continue;
96
+ const full = resolve(dir, ent.name);
97
+ try {
98
+ assertPathInsideRoot(full, root, 'scan');
99
+ }
100
+ catch {
101
+ continue;
102
+ }
103
+ if (ent.isDirectory()) {
104
+ const name = ent.name.toLowerCase();
105
+ if (name.startsWith('.'))
106
+ continue;
107
+ if (GAME_SCAN_SKIP_DIRS.has(name))
108
+ continue;
109
+ // Don't re-import from outDir if it sits inside the scan root
110
+ // (Assets/MagicPixel under Assets/).
111
+ if (full.replace(/\\/g, '/').toLowerCase() === destNorm)
112
+ continue;
113
+ await walkPngs(full, root, destNorm, out);
114
+ }
115
+ else if (ent.isFile() && ent.name.toLowerCase().endsWith('.png')) {
116
+ out.push(full);
117
+ }
118
+ }
119
+ }
@@ -55,9 +55,15 @@ export function planPush(candidates, synced, opts = {}) {
55
55
  });
56
56
  continue;
57
57
  }
58
- // New on disk → adopt. Needs at least a document + artboard segment.
59
- const pathNames = c.segments.slice(-MAX_PUSH_PATH_SEGMENTS);
60
- const path = pathNames.map(pushSlug);
58
+ // New on disk → adopt. A single segment (outDir/hero.png) is a document
59
+ // with one artboard of the same name — the server already accepts that.
60
+ const rawNames = c.segments.slice(-MAX_PUSH_PATH_SEGMENTS);
61
+ let pathNames = rawNames;
62
+ let path = pathNames.map(pushSlug);
63
+ if (path.length === 1 && path[0]) {
64
+ pathNames = [pathNames[0], pathNames[0]];
65
+ path = [path[0], path[0]];
66
+ }
61
67
  if (path.length < 2 || path.some((s) => !s)) {
62
68
  out.push({ kind: 'skip', key: c.key, reason: 'unusable-path' });
63
69
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magicpixelart/cli",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "Sync MagicPixel pixel-art assets to your local project.",
5
5
  "type": "module",
6
6
  "bin": {