@magicpixelart/cli 0.5.34 → 0.5.36
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 +11 -0
- package/README.md +15 -1
- package/dist/commands/connect.js +4 -12
- package/dist/commands/push.js +9 -12
- package/dist/commands/resync.js +2 -7
- package/dist/commands/search.js +1 -4
- package/dist/commands/sync.js +5 -12
- package/dist/index.js +2 -1
- package/dist/util/gameScan.js +10 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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.36] - 2026-10-07
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- `push --force` no longer replaces files whose newer copy was saved in the MagicPixel editor; they are listed as "edited in MagicPixel — skipped". Add `--overwrite-editor-changes` to replace them too.
|
|
12
|
+
|
|
13
|
+
## [0.5.35] - 2026-10-07
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `exclude` now also stops uploads: game PNGs matching an exclude glob are never pushed, even when `connect` is `**`.
|
|
17
|
+
- Removed the 10,000-PNG game index / connect cap: every sprite in the project is now indexed, connected and pushed. The server's daily allowance still applies; re-running resumes where it stopped.
|
|
18
|
+
|
|
8
19
|
## [0.5.34] - 2026-10-06
|
|
9
20
|
|
|
10
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -150,13 +150,27 @@ npx @magicpixelart/cli sync --watch --only Runtime/Sprites/Entities/decorations
|
|
|
150
150
|
|
|
151
151
|
Only that folder is pushed, pulled, or pruned. Every other file stays exactly as it is. Repeat `--only` for more folders, or set `"syncOnly"` in `magicpixel.json`. Changing the folder list triggers one full check. `sync <folder>` without `--only` stops with an error instead of syncing everything.
|
|
152
152
|
|
|
153
|
+
## Forced pushes and editor edits
|
|
154
|
+
|
|
155
|
+
`push --force` keeps your local copy when a sprite changed on both sides, except for files last saved in the MagicPixel editor: those are skipped and listed, so a forced push never silently undoes editor work. Add `--overwrite-editor-changes` to replace them as well.
|
|
156
|
+
|
|
157
|
+
## Keep a folder out of MagicPixel
|
|
158
|
+
|
|
159
|
+
To leave game files on disk but out of your library, add them to `exclude` in `magicpixel.json`:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{ "exclude": [".SpineRaw/rabbit_nft/**"] }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Excluded files are never uploaded, even when `connect` is `**`, and the CLI never deletes them from your game. Then delete the folder in the MagicPixel library (it goes to Trash) and it stays gone.
|
|
166
|
+
|
|
153
167
|
## Config (`magicpixel.json`)
|
|
154
168
|
|
|
155
169
|
| Field | Type | Default | Meaning |
|
|
156
170
|
| ---------- | ---------- | ----------------------- | ------------------------------------------------ |
|
|
157
171
|
| `outDir` | `string` | framework-dependent | Where PNGs (and `index.ts`) are written. |
|
|
158
172
|
| `include` | `string[]` | `["**/*"]` | Globs (picomatch) matched against `folder/slug`. |
|
|
159
|
-
| `exclude` | `string[]` | `[]` | Globs to
|
|
173
|
+
| `exclude` | `string[]` | `[]` | Globs to skip in both directions: never downloaded, and game PNGs matching them are never uploaded (wins over `connect`). |
|
|
160
174
|
| `connect` | `string[]` | `["**"]` on engines | Game PNG globs to keep in Connected. Empty on JS projects. |
|
|
161
175
|
| `emitIndex`| `boolean` | `true` | Emit `<outDir>/index.ts` with typed asset map. |
|
|
162
176
|
| `unityPpu` | `number?` | `32` | Unity only: pixels-per-unit in generated `.meta`. |
|
package/dist/commands/connect.js
CHANGED
|
@@ -3,7 +3,7 @@ import ora from 'ora';
|
|
|
3
3
|
import { loadConfig, saveConfig } from '../config.js';
|
|
4
4
|
import { assertSafeGlob } from '../util/security.js';
|
|
5
5
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
6
|
-
import { indexGamePngs, matchConnectGlobs,
|
|
6
|
+
import { indexGamePngs, matchConnectGlobs, countingSpritesText } from '../util/gameScan.js';
|
|
7
7
|
import { isAllSpritesGlob, nextConnectGlobs } from '../util/engineConnect.js';
|
|
8
8
|
import { cmd } from '../util/invoke.js';
|
|
9
9
|
import { runPush } from './push.js';
|
|
@@ -36,23 +36,15 @@ export async function connectCommand(glob) {
|
|
|
36
36
|
onProgress: (p) => { spinner.text = countingSpritesText(p); },
|
|
37
37
|
});
|
|
38
38
|
spinner.stop();
|
|
39
|
-
|
|
40
|
-
console.log(kleur.yellow(`! ${GAME_INDEX_CAP_HINT}`));
|
|
41
|
-
}
|
|
42
|
-
const matched = matchConnectGlobs(index, config.connect);
|
|
43
|
-
if (matched.capped) {
|
|
44
|
-
console.log(kleur.yellow(`! ${connectCapMessage(matched.total, matched.entries.length)}`));
|
|
45
|
-
}
|
|
39
|
+
const matched = matchConnectGlobs(index, config.connect, config.exclude);
|
|
46
40
|
if (matched.entries.length === 0) {
|
|
47
41
|
console.log(kleur.yellow(` No PNGs matched. Try \`${cmd('search')} <name>\` to see indexed paths.`));
|
|
48
42
|
return;
|
|
49
43
|
}
|
|
50
44
|
const n = matched.entries.length;
|
|
51
|
-
console.log(kleur.dim(` ${n} sprite${n === 1 ? '' : 's'} to sync (of ${index.files.length} in your game
|
|
52
|
-
(matched.capped ? `, ${matched.total} matched` : '') +
|
|
53
|
-
`).`));
|
|
45
|
+
console.log(kleur.dim(` ${n} sprite${n === 1 ? '' : 's'} to sync (of ${index.files.length} in your game).`));
|
|
54
46
|
if (n >= 200) {
|
|
55
|
-
console.log(kleur.dim(`
|
|
47
|
+
console.log(kleur.dim(` Found ${n} sprites. Only new or changed files upload.`));
|
|
56
48
|
}
|
|
57
49
|
await runPush({ gameIndex: index });
|
|
58
50
|
}
|
package/dist/commands/push.js
CHANGED
|
@@ -19,7 +19,7 @@ import { assetDiskPathFromKey, walkOutDirPngs } from '../util/paths.js';
|
|
|
19
19
|
import { applyDiskFingerprints, indexSyncedBySourceRel, planPush, resolveSyncedKey } from '../util/pushPlan.js';
|
|
20
20
|
import { cmd } from '../util/invoke.js';
|
|
21
21
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
22
|
-
import { indexGamePngs, matchConnectGlobs,
|
|
22
|
+
import { indexGamePngs, matchConnectGlobs, countingSpritesText } from '../util/gameScan.js';
|
|
23
23
|
import { collectSourceRelMap, remapAbsToCloudKeys } from '../util/syncPath.js';
|
|
24
24
|
import { assertSafeIoPath } from '../util/security.js';
|
|
25
25
|
/** Parallel hashing / path checks — enough to saturate disk, low enough to stay polite. */
|
|
@@ -64,8 +64,8 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
64
64
|
? await indexGamePngs(kind, process.cwd(), config.outDir, {
|
|
65
65
|
onProgress: spinner ? (p) => { spinner.text = countingSpritesText(p); } : undefined,
|
|
66
66
|
})
|
|
67
|
-
: { files: []
|
|
68
|
-
const matched = matchConnectGlobs(index, config.connect ?? []);
|
|
67
|
+
: { files: [] });
|
|
68
|
+
const matched = matchConnectGlobs(index, config.connect ?? [], config.exclude);
|
|
69
69
|
const sourceByKey = collectSourceRelMap(matched.entries, state.synced);
|
|
70
70
|
const disk = await walkOutDirPngs(config.outDir);
|
|
71
71
|
const absByKey = new Map(disk.map((a) => [a.key, a.abs]));
|
|
@@ -151,12 +151,6 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
151
151
|
const sendable = actions.filter((a) => a.kind === 'update' || a.kind === 'adopt');
|
|
152
152
|
spinner?.succeed(`Local sprites: ${candidates.length} · to push ${sendable.length} · unchanged ${skipped}` +
|
|
153
153
|
(matched.entries.length ? ` · connected ${matched.entries.length}` : ''));
|
|
154
|
-
if (index.capped && !quiet) {
|
|
155
|
-
console.log(kleur.yellow(`! ${GAME_INDEX_CAP_HINT}`));
|
|
156
|
-
}
|
|
157
|
-
if (matched.capped && !quiet) {
|
|
158
|
-
console.log(kleur.yellow(`! ${connectCapMessage(matched.total, matched.entries.length)}`));
|
|
159
|
-
}
|
|
160
154
|
if (skippedUnsafe > 0 && !quiet) {
|
|
161
155
|
console.log(kleur.yellow(`! skipped ${skippedUnsafe} path${skippedUnsafe === 1 ? '' : 's'} that were not regular files inside the project.`));
|
|
162
156
|
}
|
|
@@ -261,7 +255,7 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
261
255
|
// without a live sha, and the retry as an update is what makes the server
|
|
262
256
|
// composite the artboard and report one.
|
|
263
257
|
for (let round = 0; round < 2; round++) {
|
|
264
|
-
const retry = forcedRetrySprites(results, spriteByKey, opts.flatten === true);
|
|
258
|
+
const retry = forcedRetrySprites(results, spriteByKey, opts.flatten === true, opts.overwriteEditorChanges === true);
|
|
265
259
|
if (retry.length === 0)
|
|
266
260
|
break;
|
|
267
261
|
const retryResults = await pushSpritesAdaptive(retry);
|
|
@@ -306,7 +300,7 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
306
300
|
for (const r of results) {
|
|
307
301
|
if (r.status === 'conflict') {
|
|
308
302
|
if (printedIssues++ < 10)
|
|
309
|
-
console.log(` ${kleur.yellow('!')} ${r.key}: ${conflictHint(r.reason)}`);
|
|
303
|
+
console.log(` ${kleur.yellow('!')} ${r.key}: ${opts.force && r.lastWriteSource === 'editor' && !opts.overwriteEditorChanges ? `edited in MagicPixel — skipped. Add --overwrite-editor-changes to replace it.` : conflictHint(r.reason)}`);
|
|
310
304
|
}
|
|
311
305
|
else if (r.status === 'error') {
|
|
312
306
|
if (printedIssues++ < 10)
|
|
@@ -350,11 +344,14 @@ export function collectKeptAssetIds(candidates, synced, results) {
|
|
|
350
344
|
* reported. Not a bypass — the server still compares, so a save that lands in
|
|
351
345
|
* between refuses again.
|
|
352
346
|
*/
|
|
353
|
-
export function forcedRetrySprites(results, spriteByKey, flatten) {
|
|
347
|
+
export function forcedRetrySprites(results, spriteByKey, flatten, overwriteEditorChanges = false) {
|
|
354
348
|
const out = [];
|
|
355
349
|
for (const r of results) {
|
|
356
350
|
if (r.status !== 'conflict' || r.reason !== 'cloud-changed' || !r.assetId)
|
|
357
351
|
continue;
|
|
352
|
+
// A newer save made in the MagicPixel editor is never silently undone.
|
|
353
|
+
if (r.lastWriteSource === 'editor' && !overwriteEditorChanges)
|
|
354
|
+
continue;
|
|
358
355
|
const original = spriteByKey.get(r.key);
|
|
359
356
|
if (!original)
|
|
360
357
|
continue;
|
package/dist/commands/resync.js
CHANGED
|
@@ -77,8 +77,7 @@ export async function runResync(segments, opts = {}) {
|
|
|
77
77
|
if (plan.scanned === 0) {
|
|
78
78
|
throw new Error(`No local sprites found in "${label}". Nothing was changed. Check the folder name with \`magicpixel search\`.`);
|
|
79
79
|
}
|
|
80
|
-
const
|
|
81
|
-
const preview = pruneAllowed && plan.keptAssetIds.length > 0
|
|
80
|
+
const preview = plan.keptAssetIds.length > 0
|
|
82
81
|
? await pruneResyncFolder(segments, plan.keptAssetIds, true)
|
|
83
82
|
: null;
|
|
84
83
|
log(kleur.bold(`Resync ${label} from your game`));
|
|
@@ -90,8 +89,6 @@ export async function runResync(segments, opts = {}) {
|
|
|
90
89
|
if (preview.wouldTrash.length > 10)
|
|
91
90
|
log(kleur.dim(` …and ${preview.wouldTrash.length - 10} more`));
|
|
92
91
|
}
|
|
93
|
-
if (!pruneAllowed)
|
|
94
|
-
log(kleur.yellow(' ! game scan hit its file cap — nothing will be trashed.'));
|
|
95
92
|
log(kleur.dim(' Local files win: MagicPixel copies are overwritten and resized to match.'));
|
|
96
93
|
if (opts.dryRun) {
|
|
97
94
|
log(kleur.dim('--dry-run: nothing sent.'));
|
|
@@ -105,9 +102,7 @@ export async function runResync(segments, opts = {}) {
|
|
|
105
102
|
});
|
|
106
103
|
let pruneSkipped = null;
|
|
107
104
|
let trashed = 0;
|
|
108
|
-
if (
|
|
109
|
-
pruneSkipped = 'game scan capped';
|
|
110
|
-
else if (push.error > 0 || push.conflict > 0)
|
|
105
|
+
if (push.error > 0 || push.conflict > 0)
|
|
111
106
|
pruneSkipped = 'some sprites failed to upload';
|
|
112
107
|
else if (push.keptAssetIds.length === 0)
|
|
113
108
|
pruneSkipped = 'no files confirmed';
|
package/dist/commands/search.js
CHANGED
|
@@ -2,7 +2,7 @@ import kleur from 'kleur';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { loadConfig } from '../config.js';
|
|
4
4
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
5
|
-
import { indexGamePngs, searchGameIndex,
|
|
5
|
+
import { indexGamePngs, searchGameIndex, countingSpritesText } from '../util/gameScan.js';
|
|
6
6
|
import { cmd } from '../util/invoke.js';
|
|
7
7
|
const PRINT_CAP = 50;
|
|
8
8
|
/**
|
|
@@ -31,9 +31,6 @@ export async function searchCommand(query) {
|
|
|
31
31
|
onProgress: (p) => { spinner.text = countingSpritesText(p); },
|
|
32
32
|
});
|
|
33
33
|
spinner.stop();
|
|
34
|
-
if (index.capped) {
|
|
35
|
-
console.log(kleur.yellow(`! ${GAME_INDEX_CAP_HINT}`));
|
|
36
|
-
}
|
|
37
34
|
const hits = searchGameIndex(index, q);
|
|
38
35
|
if (hits.length === 0) {
|
|
39
36
|
console.log(kleur.dim(` No indexed PNGs matching "${q}" (${index.files.length} scanned).`));
|
package/dist/commands/sync.js
CHANGED
|
@@ -12,7 +12,7 @@ import { createLimit } from '../util/limit.js';
|
|
|
12
12
|
import { emitTypedIndex, ensureAgentsDoc, scanDiskAssets } from '../util/emitIndex.js';
|
|
13
13
|
import { assertPathInsideRoot, assertSafeIoPath } from '../util/security.js';
|
|
14
14
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
15
|
-
import { indexGamePngs, matchConnectGlobs,
|
|
15
|
+
import { indexGamePngs, matchConnectGlobs, countingSpritesText } from '../util/gameScan.js';
|
|
16
16
|
import { aliasCollisionKeys, collectSourceRelMap, isPathInside, syncDiskPathFromKey } from '../util/syncPath.js';
|
|
17
17
|
import { DEFAULT_UNITY_PPU, writeMissingUnityMetas } from '../util/unityMeta.js';
|
|
18
18
|
import { filterUnityManifest, isWorkingSetEntry, partitionWithheldEntries, shouldPruneDeselectedEntry, workingSetPullKeys } from '../util/unityFilter.js';
|
|
@@ -510,7 +510,7 @@ async function loadWatchHeader(onProgress) {
|
|
|
510
510
|
const kind = await detectProjectKind();
|
|
511
511
|
if (isEngineKind(kind) && (config.connect?.length ?? 0) > 0) {
|
|
512
512
|
gameIndex = await indexGamePngs(kind, process.cwd(), config.outDir, { onProgress });
|
|
513
|
-
workingSet = matchConnectGlobs(gameIndex, config.connect).
|
|
513
|
+
workingSet = matchConnectGlobs(gameIndex, config.connect, config.exclude).entries.length;
|
|
514
514
|
}
|
|
515
515
|
else if (state.synced) {
|
|
516
516
|
workingSet = Object.keys(state.synced).length;
|
|
@@ -649,14 +649,14 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
649
649
|
: `Looking through your game sprites… ${n}`);
|
|
650
650
|
},
|
|
651
651
|
})
|
|
652
|
-
: { files: []
|
|
652
|
+
: { files: [] });
|
|
653
653
|
if (!runOpts.gameIndex)
|
|
654
654
|
runOpts.onGameIndex?.(gameIndex);
|
|
655
655
|
const fullGameIndex = gameIndex;
|
|
656
656
|
const scopedGameIndex = scope
|
|
657
657
|
? { ...fullGameIndex, files: fullGameIndex.files.filter((f) => pathInScope(scope, f.sourceRel)) }
|
|
658
658
|
: fullGameIndex;
|
|
659
|
-
const connected = matchConnectGlobs(scopedGameIndex, config.connect ?? []);
|
|
659
|
+
const connected = matchConnectGlobs(scopedGameIndex, config.connect ?? [], config.exclude);
|
|
660
660
|
const sourceByKey = collectSourceRelMap(connected.entries, state.synced);
|
|
661
661
|
if (scope) {
|
|
662
662
|
// Out-of-scope rows become hold-only: never pulled, and their folders are
|
|
@@ -686,15 +686,8 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
686
686
|
}
|
|
687
687
|
}
|
|
688
688
|
aliasCollisionKeys(sourceByKey, manifest.map((e) => e.key));
|
|
689
|
-
if (verbose && gameIndex.capped) {
|
|
690
|
-
console.log(kleur.yellow(`! ${GAME_INDEX_CAP_HINT}`));
|
|
691
|
-
}
|
|
692
|
-
if (verbose && connected.capped) {
|
|
693
|
-
console.log(kleur.yellow(`! ${connectCapMessage(connected.total, connected.entries.length)}`));
|
|
694
|
-
}
|
|
695
689
|
if (verbose && isEngineKind(projectKind)) {
|
|
696
|
-
console.log(kleur.dim(` game index → ${gameIndex.files.length} PNG${gameIndex.files.length === 1 ? '' : 's'} · working set ${connected.entries.length}`
|
|
697
|
-
(connected.capped ? ` of ${connected.total} matched` : '')));
|
|
690
|
+
console.log(kleur.dim(` game index → ${gameIndex.files.length} PNG${gameIndex.files.length === 1 ? '' : 's'} · working set ${connected.entries.length}`));
|
|
698
691
|
}
|
|
699
692
|
const pathForKey = (key) => syncDiskPathFromKey(config.outDir, key, sourceByKey);
|
|
700
693
|
// Every folder on disk that belongs to a withheld document. Anything already
|
package/dist/index.js
CHANGED
|
@@ -108,7 +108,8 @@ program
|
|
|
108
108
|
.description('Upload local sprite edits back to MagicPixel (two-way sync)')
|
|
109
109
|
.option('--dry-run', 'Print what would be pushed without sending anything')
|
|
110
110
|
.option('--flatten', 'Allow replacing multi-layer artboards with the flat local image')
|
|
111
|
-
.option('--force', 'Keep your local copy when a sprite also changed in MagicPixel')
|
|
111
|
+
.option('--force', 'Keep your local copy when a sprite also changed in MagicPixel (skips files saved in the MagicPixel editor)')
|
|
112
|
+
.option('--overwrite-editor-changes', 'With --force: also replace files saved in the MagicPixel editor')
|
|
112
113
|
.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')
|
|
113
114
|
.action(wrap("push", async (opts) => pushCommand(opts)));
|
|
114
115
|
program
|
package/dist/util/gameScan.js
CHANGED
|
@@ -47,15 +47,6 @@ export const GAME_SCAN_SKIP_HIDDEN = new Set([
|
|
|
47
47
|
]);
|
|
48
48
|
/** Parallel directory reads while walking the game tree. */
|
|
49
49
|
const SCAN_WALK_CONCURRENCY = 8;
|
|
50
|
-
/** Cap on the search index (whole tree). Connect uses the same ceiling. */
|
|
51
|
-
export const MAX_GAME_INDEX = 10_000;
|
|
52
|
-
/** Cap on PNGs ingested for one working-set connect / push. */
|
|
53
|
-
export const MAX_GAME_CONNECT = MAX_GAME_INDEX;
|
|
54
|
-
export const GAME_INDEX_CAP_HINT = `Game index hit ${MAX_GAME_INDEX} PNGs — later files aren't searchable or connectable.`;
|
|
55
|
-
export function connectCapMessage(total, ingested) {
|
|
56
|
-
return (`${total} sprites match — syncing the first ${ingested} (cap ${MAX_GAME_CONNECT}). ` +
|
|
57
|
-
`Connect a smaller folder if you don't need all of them.`);
|
|
58
|
-
}
|
|
59
50
|
/** True for a full Unity game project (not an embedded UPM package). */
|
|
60
51
|
function isUnityGameProject(cwd, assetRoot) {
|
|
61
52
|
return Boolean(assetRoot && existsSync(resolve(cwd, 'ProjectSettings')));
|
|
@@ -175,7 +166,7 @@ function samePath(a, b) {
|
|
|
175
166
|
}
|
|
176
167
|
/** Walk the engine tree and return index entries. Does not copy files. */
|
|
177
168
|
export async function indexGamePngs(kind, cwd = process.cwd(), outDir = '', opts = {}) {
|
|
178
|
-
const empty = { files: []
|
|
169
|
+
const empty = { files: [] };
|
|
179
170
|
if (!isEngineKind(kind))
|
|
180
171
|
return empty;
|
|
181
172
|
const scanRoot = resolve(cwd);
|
|
@@ -192,30 +183,27 @@ export async function indexGamePngs(kind, cwd = process.cwd(), outDir = '', opts
|
|
|
192
183
|
// Library/Packages/cloud placeholders and looks hung at "1 folder".
|
|
193
184
|
const startAt = isUnityGameProject(cwd, assetRoot) ? assetRoot : scanRoot;
|
|
194
185
|
const preferFirst = startAt === scanRoot ? assetRoot : null;
|
|
195
|
-
await walkPngs(startAt, scanRoot, destNorm, found,
|
|
186
|
+
await walkPngs(startAt, scanRoot, destNorm, found, Infinity, io, report, stats, preferFirst);
|
|
196
187
|
report.flush?.();
|
|
197
188
|
opts.onProgress?.({ pngs: found.length, folders: stats.folders });
|
|
198
189
|
found.sort((a, b) => a.localeCompare(b));
|
|
199
|
-
const capped = found.length > MAX_GAME_INDEX;
|
|
200
|
-
if (capped)
|
|
201
|
-
found.length = MAX_GAME_INDEX;
|
|
202
190
|
const files = [];
|
|
203
191
|
for (const abs of found) {
|
|
204
192
|
const entry = toEntry(abs, scanRoot, assetRoot);
|
|
205
193
|
if (entry)
|
|
206
194
|
files.push(entry);
|
|
207
195
|
}
|
|
208
|
-
return { files
|
|
196
|
+
return { files };
|
|
209
197
|
}
|
|
210
|
-
/** Filter the index by connect globs (cwd
|
|
211
|
-
export function matchConnectGlobs(index, globs,
|
|
198
|
+
/** Filter the index by connect globs minus exclude globs (cwd-, asset-root-relative or manifest key). */
|
|
199
|
+
export function matchConnectGlobs(index, globs, exclude = []) {
|
|
212
200
|
if (globs.length === 0)
|
|
213
|
-
return { entries: []
|
|
214
|
-
const
|
|
201
|
+
return { entries: [] };
|
|
202
|
+
const hit = (e, g) => matchGlob(e.sourceRel, g) || matchGlob(e.adoptRel, g) || matchGlob(e.key, g);
|
|
203
|
+
// `exclude` wins over `connect`: excluded game files are never uploaded.
|
|
204
|
+
const matched = index.files.filter((e) => globs.some((g) => hit(e, g)) && !exclude.some((g) => hit(e, g)));
|
|
215
205
|
matched.sort((a, b) => a.sourceRel.localeCompare(b.sourceRel));
|
|
216
|
-
|
|
217
|
-
const capped = total > max;
|
|
218
|
-
return { entries: capped ? matched.slice(0, max) : matched, total, capped };
|
|
206
|
+
return { entries: matched };
|
|
219
207
|
}
|
|
220
208
|
export function searchGameIndex(index, query) {
|
|
221
209
|
const q = query.trim().toLowerCase();
|