@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 +14 -0
- package/README.md +1 -0
- package/dist/commands/push.js +60 -36
- package/dist/commands/start.js +7 -12
- package/dist/commands/sync.js +25 -15
- package/dist/config.js +1 -0
- package/dist/util/gameScan.js +119 -0
- package/dist/util/pushPlan.js +9 -3
- package/package.json +1 -1
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).
|
package/dist/commands/push.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
console.log(
|
|
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) {
|
package/dist/commands/start.js
CHANGED
|
@@ -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
|
|
18
|
-
*
|
|
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
|
|
22
|
+
console.log(kleur.dim(' This connects your game project so existing sprites show up in MagicPixel.'));
|
|
23
23
|
console.log();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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:
|
|
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;
|
package/dist/commands/sync.js
CHANGED
|
@@ -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
|
-
//
|
|
377
|
-
//
|
|
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
|
|
382
|
-
console.log(kleur.dim('
|
|
383
|
-
console.log(kleur.dim('
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
@@ -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
|
+
}
|
package/dist/util/pushPlan.js
CHANGED
|
@@ -55,9 +55,15 @@ export function planPush(candidates, synced, opts = {}) {
|
|
|
55
55
|
});
|
|
56
56
|
continue;
|
|
57
57
|
}
|
|
58
|
-
// New on disk → adopt.
|
|
59
|
-
|
|
60
|
-
const
|
|
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;
|