@magicpixelart/cli 0.5.38 → 0.6.2
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 +53 -0
- package/README.md +26 -1
- package/dist/commands/add.js +2 -1
- package/dist/commands/cleanStrays.js +92 -0
- package/dist/commands/connect.js +9 -6
- package/dist/commands/consolidate.js +309 -0
- package/dist/commands/doctor.js +3 -2
- package/dist/commands/init.js +6 -5
- package/dist/commands/login.js +2 -1
- package/dist/commands/logout.js +2 -1
- package/dist/commands/push.js +64 -19
- package/dist/commands/remove.js +2 -1
- package/dist/commands/repair.js +13 -23
- package/dist/commands/resync.js +11 -20
- package/dist/commands/search.js +2 -2
- package/dist/commands/start.js +100 -16
- package/dist/commands/sync.js +128 -60
- package/dist/commands/whoami.js +4 -3
- package/dist/commands/why.js +105 -0
- package/dist/config.js +3 -0
- package/dist/index.js +49 -5
- package/dist/util/commandTips.js +33 -0
- package/dist/util/engineConnect.js +39 -0
- package/dist/util/framework.js +20 -2
- package/dist/util/gameScan.js +51 -8
- package/dist/util/prompt.js +15 -0
- package/dist/util/strayGuard.js +76 -0
- package/dist/util/ui.js +175 -0
- package/dist/util/workspace.js +82 -0
- package/package.json +1 -1
package/dist/commands/logout.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
|
+
import { ui } from '../util/ui.js';
|
|
2
3
|
import { deleteCredentials, credentialsPath } from '../util/credentials.js';
|
|
3
4
|
import { relative } from 'node:path';
|
|
4
5
|
export async function logoutCommand() {
|
|
5
6
|
const path = credentialsPath();
|
|
6
7
|
const removed = await deleteCredentials();
|
|
7
8
|
if (removed) {
|
|
8
|
-
console.log(
|
|
9
|
+
console.log(ui.ok(`logged out`));
|
|
9
10
|
console.log(kleur.dim(` Removed ${relative(process.cwd(), path)}.`));
|
|
10
11
|
}
|
|
11
12
|
else {
|
package/dist/commands/push.js
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* `POST /api/public/integration/ingest`.
|
|
8
8
|
*/
|
|
9
9
|
import kleur from 'kleur';
|
|
10
|
-
import
|
|
10
|
+
import { icon, progressBar, shortenPath, spinner as makeSpinner, ui, visibleWidth } from '../util/ui.js';
|
|
11
11
|
import { mkdir, readFile, rename } from 'node:fs/promises';
|
|
12
12
|
import { dirname, relative, resolve } from 'node:path';
|
|
13
13
|
import { loadConfig, loadState, saveState, resetStateIfProjectChanged, PROJECT_CHANGED_MESSAGE } from '../config.js';
|
|
14
|
-
import { ensureEngineConnect } from '../util/engineConnect.js';
|
|
14
|
+
import { ensureEngineConnect, unwatchedHiddenFolders } from '../util/engineConnect.js';
|
|
15
15
|
import { PUSH_BATCH_SIZE, pushSpritesAdaptive, fetchManifestPage, getLastProjectInfo } from '../api.js';
|
|
16
16
|
import { hashFile } from '../util/hash.js';
|
|
17
17
|
import { createLimit } from '../util/limit.js';
|
|
@@ -26,6 +26,19 @@ import { assertSafeIoPath } from '../util/security.js';
|
|
|
26
26
|
const PUSH_SCAN_CONCURRENCY = 8;
|
|
27
27
|
/** Paths already verified this process; hashFile still refuses a later symlink swap. */
|
|
28
28
|
const verifiedIoPaths = new Set();
|
|
29
|
+
/** Plain-language skip reasons for the quiet (nested-in-sync) push path. */
|
|
30
|
+
export function skipIssues(legacy, flattenBlocked) {
|
|
31
|
+
return [
|
|
32
|
+
...legacy.map((a) => ({
|
|
33
|
+
key: a.key,
|
|
34
|
+
text: 'MagicPixel file is a legacy single image — open it in MagicPixel and save once, then sync again.',
|
|
35
|
+
})),
|
|
36
|
+
...flattenBlocked.map((a) => ({
|
|
37
|
+
key: a.key,
|
|
38
|
+
text: `MagicPixel artboard has ${a.layers ?? 'several'} layers — run \`${cmd('push')} --flatten\` to replace it with your file.`,
|
|
39
|
+
})),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
29
42
|
export async function pushCommand(opts = {}) {
|
|
30
43
|
await runPush(opts);
|
|
31
44
|
}
|
|
@@ -45,7 +58,7 @@ export async function runPush(opts = {}) {
|
|
|
45
58
|
if (!opts.dryRun)
|
|
46
59
|
await saveState(state);
|
|
47
60
|
if (!opts.quiet)
|
|
48
|
-
console.log(
|
|
61
|
+
console.log(ui.warn(`${PROJECT_CHANGED_MESSAGE}`));
|
|
49
62
|
}
|
|
50
63
|
}
|
|
51
64
|
return runPushWith(config, state, opts);
|
|
@@ -55,9 +68,9 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
55
68
|
if (opts.replace)
|
|
56
69
|
opts = { ...opts, flatten: true };
|
|
57
70
|
const empty = {
|
|
58
|
-
created: 0, updated: 0, unchanged: 0, conflict: 0, error: 0, imported: 0, keptAssetIds: [], scanned: 0,
|
|
71
|
+
created: 0, updated: 0, unchanged: 0, conflict: 0, error: 0, imported: 0, keptAssetIds: [], scanned: 0, issues: [],
|
|
59
72
|
};
|
|
60
|
-
const spinner = quiet ? null :
|
|
73
|
+
const spinner = quiet ? null : makeSpinner(countingSpritesText(0)).start();
|
|
61
74
|
const kind = await detectProjectKind();
|
|
62
75
|
const index = opts.gameIndex
|
|
63
76
|
?? (isEngineKind(kind)
|
|
@@ -67,6 +80,13 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
67
80
|
: { files: [] });
|
|
68
81
|
const matched = matchConnectGlobs(index, config.connect ?? [], config.exclude);
|
|
69
82
|
const sourceByKey = collectSourceRelMap(matched.entries, state.synced);
|
|
83
|
+
if (!quiet) {
|
|
84
|
+
const watched = new Set(matched.entries.map((e) => e.key));
|
|
85
|
+
for (const [folder, n] of unwatchedHiddenFolders(index.files, watched, config.exclude ?? []).slice(0, 5)) {
|
|
86
|
+
console.log(ui.warn(`${n.toLocaleString('en-US')} PNGs in ${folder} aren't watched.`));
|
|
87
|
+
console.log(ui.fix(`${cmd('connect')} '${folder}/**'`));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
70
90
|
const disk = await walkOutDirPngs(config.outDir);
|
|
71
91
|
const absByKey = new Map(disk.map((a) => [a.key, a.abs]));
|
|
72
92
|
const entryByKey = new Map(matched.entries.map((e) => [e.key, e]));
|
|
@@ -90,12 +110,11 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
90
110
|
let hashed = 0;
|
|
91
111
|
const reportHash = () => {
|
|
92
112
|
hashed++;
|
|
93
|
-
const
|
|
94
|
-
const t = toHash.length.toLocaleString('en-US');
|
|
113
|
+
const line = `Checking your game files ${progressBar(hashed, toHash.length)}`;
|
|
95
114
|
if (spinner)
|
|
96
|
-
spinner.text =
|
|
115
|
+
spinner.text = line;
|
|
97
116
|
else if (opts.onStatus && (hashed === 1 || hashed === toHash.length || hashed % 25 === 0)) {
|
|
98
|
-
opts.onStatus(
|
|
117
|
+
opts.onStatus(`${icon('work')} ${line}`);
|
|
99
118
|
}
|
|
100
119
|
};
|
|
101
120
|
await Promise.all(toHash.map(([key, abs]) => scan(async () => {
|
|
@@ -149,31 +168,33 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
149
168
|
const legacy = actions.filter((a) => a.kind === 'skip' && a.reason === 'legacy');
|
|
150
169
|
const flattenBlocked = actions.filter((a) => a.kind === 'needs-flatten');
|
|
151
170
|
const sendable = actions.filter((a) => a.kind === 'update' || a.kind === 'adopt');
|
|
171
|
+
const preIssues = skipIssues(legacy, flattenBlocked.map((a) => ({ key: a.key, ...('layers' in a && typeof a.layers === 'number' ? { layers: a.layers } : {}) })));
|
|
152
172
|
spinner?.succeed(`Local sprites: ${candidates.length} · to push ${sendable.length} · unchanged ${skipped}` +
|
|
153
173
|
(matched.entries.length ? ` · connected ${matched.entries.length}` : ''));
|
|
154
174
|
if (skippedUnsafe > 0 && !quiet) {
|
|
155
175
|
console.log(kleur.yellow(`! skipped ${skippedUnsafe} path${skippedUnsafe === 1 ? '' : 's'} that were not regular files inside the project.`));
|
|
156
176
|
}
|
|
157
|
-
if (legacy.length > 0 && !quiet
|
|
177
|
+
if (legacy.length > 0 && !quiet) {
|
|
158
178
|
console.log();
|
|
159
179
|
console.log(kleur.yellow(`! ${legacy.length} sprite${legacy.length === 1 ? '' : 's'} came from a legacy single-image file — skipped.`));
|
|
160
|
-
console.log(
|
|
180
|
+
console.log(ui.fixText(`open the file in MagicPixel and save it once, then re-run \`${cmd('sync')}\`.`));
|
|
161
181
|
}
|
|
162
|
-
if (flattenBlocked.length > 0 && !quiet
|
|
182
|
+
if (flattenBlocked.length > 0 && !quiet) {
|
|
163
183
|
console.log();
|
|
164
184
|
console.log(kleur.yellow(`! ${flattenBlocked.length} sprite${flattenBlocked.length === 1 ? '' : 's'} would flatten a multi-layer artboard — skipped.`));
|
|
165
185
|
for (const a of flattenBlocked.slice(0, 10)) {
|
|
166
186
|
console.log(kleur.dim(` ${a.key} (${'layers' in a ? a.layers : '?'} layers)`));
|
|
167
187
|
}
|
|
168
|
-
console.log(
|
|
188
|
+
console.log(ui.fixText('re-run with --flatten to replace them with the flat disk image.'));
|
|
169
189
|
}
|
|
170
190
|
if (sendable.length === 0) {
|
|
171
191
|
await persistDiskFingerprints(state, candidates);
|
|
172
192
|
if (!quiet)
|
|
173
|
-
console.log(
|
|
193
|
+
console.log(ui.ok('nothing to push.'));
|
|
174
194
|
return {
|
|
175
195
|
...empty, imported: matched.entries.length, unchanged: skipped,
|
|
176
196
|
keptAssetIds: collectKeptAssetIds(candidates, state.synced ?? {}, []), scanned: candidates.length,
|
|
197
|
+
issues: preIssues, needsFlatten: flattenBlocked.map((a) => a.key), gameIndex: index,
|
|
177
198
|
};
|
|
178
199
|
}
|
|
179
200
|
if (opts.dryRun) {
|
|
@@ -188,6 +209,7 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
188
209
|
return {
|
|
189
210
|
...empty, imported: matched.entries.length,
|
|
190
211
|
keptAssetIds: collectKeptAssetIds(candidates, state.synced ?? {}, []), scanned: candidates.length,
|
|
212
|
+
issues: preIssues, needsFlatten: flattenBlocked.map((a) => a.key), gameIndex: index,
|
|
191
213
|
};
|
|
192
214
|
}
|
|
193
215
|
const outRoot = resolve(process.cwd(), config.outDir);
|
|
@@ -221,7 +243,8 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
221
243
|
});
|
|
222
244
|
}
|
|
223
245
|
}
|
|
224
|
-
const progress = quiet ? null :
|
|
246
|
+
const progress = quiet ? null : makeSpinner(`Uploading ${progressBar(0, sprites.length)}`).start();
|
|
247
|
+
opts.onStatus?.(`${icon('up')} Uploading ${sprites.length.toLocaleString('en-US')} sprite${sprites.length === 1 ? '' : 's'}…`);
|
|
225
248
|
const results = [];
|
|
226
249
|
let synced = { ...(state.synced ?? {}) };
|
|
227
250
|
try {
|
|
@@ -239,8 +262,15 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
239
262
|
outDir: config.outDir,
|
|
240
263
|
});
|
|
241
264
|
await saveState({ ...state, synced });
|
|
242
|
-
|
|
243
|
-
|
|
265
|
+
const doneN = Math.min(i + batch.length, sprites.length);
|
|
266
|
+
const last = batch[batch.length - 1];
|
|
267
|
+
const where = last ? fpByKey.get(last.key)?.sourceRel ?? last.key : '';
|
|
268
|
+
if (progress) {
|
|
269
|
+
const head = `Uploading ${progressBar(doneN, sprites.length)} `;
|
|
270
|
+
progress.text = head + kleur.dim(shortenPath(where, (process.stdout.columns ?? 80) - visibleWidth(head) - 4));
|
|
271
|
+
}
|
|
272
|
+
else
|
|
273
|
+
opts.onStatus?.(`${icon('up')} Uploading ${doneN}/${sprites.length} ${shortenPath(where, 60)}…`);
|
|
244
274
|
}
|
|
245
275
|
}
|
|
246
276
|
catch (e) {
|
|
@@ -300,11 +330,11 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
300
330
|
for (const r of results) {
|
|
301
331
|
if (r.status === 'conflict') {
|
|
302
332
|
if (printedIssues++ < 10)
|
|
303
|
-
console.log(` ${
|
|
333
|
+
console.log(` ${icon('warn')} ${r.key}: ${opts.force && r.lastWriteSource === 'editor' && !opts.overwriteEditorChanges ? `edited in MagicPixel — skipped. Add --overwrite-editor-changes to replace it.` : conflictHint(r.reason)}`);
|
|
304
334
|
}
|
|
305
335
|
else if (r.status === 'error') {
|
|
306
336
|
if (printedIssues++ < 10)
|
|
307
|
-
console.log(` ${
|
|
337
|
+
console.log(` ${icon('fail')} ${r.key}: ${r.message ?? 'push failed'}`);
|
|
308
338
|
}
|
|
309
339
|
}
|
|
310
340
|
if (printedIssues > 10)
|
|
@@ -316,9 +346,24 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
316
346
|
}
|
|
317
347
|
if (failed)
|
|
318
348
|
process.exitCode = 1;
|
|
349
|
+
const issues = [...preIssues];
|
|
350
|
+
for (const r of results) {
|
|
351
|
+
if (r.status === 'conflict') {
|
|
352
|
+
issues.push({
|
|
353
|
+
key: r.key,
|
|
354
|
+
text: opts.force && r.lastWriteSource === 'editor' && !opts.overwriteEditorChanges
|
|
355
|
+
? 'edited in MagicPixel — skipped. Add --overwrite-editor-changes to replace it.'
|
|
356
|
+
: conflictHint(r.reason),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else if (r.status === 'error') {
|
|
360
|
+
issues.push({ key: r.key, text: r.message ?? 'push failed' });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
319
363
|
return {
|
|
320
364
|
...counts, imported: matched.entries.length,
|
|
321
365
|
keptAssetIds: collectKeptAssetIds(candidates, synced, results), scanned: candidates.length,
|
|
366
|
+
issues, needsFlatten: flattenBlocked.map((a) => a.key),
|
|
322
367
|
};
|
|
323
368
|
}
|
|
324
369
|
/**
|
package/dist/commands/remove.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
|
+
import { ui } from '../util/ui.js';
|
|
2
3
|
import { loadConfig, saveConfig } from '../config.js';
|
|
3
4
|
import { assertSafeGlob } from '../util/security.js';
|
|
4
5
|
export async function removeCommand(glob) {
|
|
@@ -11,5 +12,5 @@ export async function removeCommand(glob) {
|
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
14
|
await saveConfig(config);
|
|
14
|
-
console.log(
|
|
15
|
+
console.log(ui.ok(`removed include pattern: ${pattern}`));
|
|
15
16
|
}
|
package/dist/commands/repair.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
|
+
import { icon, ui } from '../util/ui.js';
|
|
2
3
|
import { existsSync } from 'node:fs';
|
|
3
4
|
import { rename } from 'node:fs/promises';
|
|
4
5
|
import { resolve, relative } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import { stdin
|
|
6
|
+
import { confirm as askYesNo } from '../util/prompt.js';
|
|
7
|
+
import { stdin } from 'node:process';
|
|
7
8
|
import { loadConfig, statePath, getApiKey } from '../config.js';
|
|
8
9
|
import { assertKeyValid } from '../util/auth.js';
|
|
9
10
|
import { listEmptyDirs, pruneEmptyDirs } from '../util/paths.js';
|
|
@@ -32,7 +33,7 @@ export async function repairCommand(opts = {}) {
|
|
|
32
33
|
try {
|
|
33
34
|
const key = getApiKey();
|
|
34
35
|
await assertKeyValid(key, config);
|
|
35
|
-
console.log(` ${
|
|
36
|
+
console.log(` ${icon('ok')} key accepted`);
|
|
36
37
|
}
|
|
37
38
|
catch (e) {
|
|
38
39
|
// Preserve multi-line "Fix:" guidance from getApiKey()/assertKeyValid —
|
|
@@ -40,7 +41,7 @@ export async function repairCommand(opts = {}) {
|
|
|
40
41
|
// first-time `repair` user needs.
|
|
41
42
|
const msg = e.message ?? String(e);
|
|
42
43
|
const [head, ...rest] = msg.split('\n');
|
|
43
|
-
console.log(` ${
|
|
44
|
+
console.log(` ${icon('fail')} ${head}`);
|
|
44
45
|
for (const line of rest)
|
|
45
46
|
console.log(` ${line}`);
|
|
46
47
|
process.exitCode = 1;
|
|
@@ -78,10 +79,10 @@ export async function repairCommand(opts = {}) {
|
|
|
78
79
|
const dest = `${sPath}.repair-${Date.now()}`;
|
|
79
80
|
try {
|
|
80
81
|
await rename(sPath, dest);
|
|
81
|
-
console.log(` ${
|
|
82
|
+
console.log(` ${icon('ok')} moved to ${relative(process.cwd(), dest)}`);
|
|
82
83
|
}
|
|
83
84
|
catch (e) {
|
|
84
|
-
console.log(` ${
|
|
85
|
+
console.log(` ${icon('fail')} ${e.message}`);
|
|
85
86
|
process.exitCode = 1;
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
@@ -110,7 +111,7 @@ export async function repairCommand(opts = {}) {
|
|
|
110
111
|
}
|
|
111
112
|
else {
|
|
112
113
|
await pruneEmptyDirs(outRoot);
|
|
113
|
-
console.log(` ${
|
|
114
|
+
console.log(` ${icon('ok')} done`);
|
|
114
115
|
}
|
|
115
116
|
// --- Step 4: full sync ---------------------------------------------------
|
|
116
117
|
console.log(kleur.bold('4/4 ') + 'Running full sync…');
|
|
@@ -121,10 +122,10 @@ export async function repairCommand(opts = {}) {
|
|
|
121
122
|
console.log(` ${kleur.dim(`would run \`${cmd('sync')} --full\` (skipped in --dry-run)`)}`);
|
|
122
123
|
console.log();
|
|
123
124
|
if (skippedStep2) {
|
|
124
|
-
console.log(
|
|
125
|
+
console.log(ui.warn('repair plan would skip the state reset. Re-run with --yes to apply it.'));
|
|
125
126
|
}
|
|
126
127
|
else {
|
|
127
|
-
console.log(
|
|
128
|
+
console.log(ui.ok('repair plan looks good. Re-run without --dry-run to apply.'));
|
|
128
129
|
}
|
|
129
130
|
return;
|
|
130
131
|
}
|
|
@@ -138,21 +139,10 @@ export async function repairCommand(opts = {}) {
|
|
|
138
139
|
await syncCommand({ full: true, prune: true });
|
|
139
140
|
console.log();
|
|
140
141
|
if ((process.exitCode ?? 0) > exitBefore) {
|
|
141
|
-
console.log(
|
|
142
|
+
console.log(ui.warn(`repair completed with errors — re-run \`${cmd('sync')}\` to retry the failed downloads.`));
|
|
142
143
|
}
|
|
143
144
|
else {
|
|
144
|
-
console.log(
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
async function confirm(prompt) {
|
|
148
|
-
if (!stdin.isTTY)
|
|
149
|
-
return false; // CI/non-interactive: default to NO; require --yes.
|
|
150
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
151
|
-
try {
|
|
152
|
-
const answer = (await rl.question(` ${prompt}`)).trim().toLowerCase();
|
|
153
|
-
return answer === 'y' || answer === 'yes';
|
|
154
|
-
}
|
|
155
|
-
finally {
|
|
156
|
-
rl.close();
|
|
145
|
+
console.log(ui.ok('repair complete.'));
|
|
157
146
|
}
|
|
158
147
|
}
|
|
148
|
+
const confirm = (prompt) => askYesNo(` ${prompt}`);
|
package/dist/commands/resync.js
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* - MagicPixel files in that folder with no local file go to Trash.
|
|
8
8
|
*/
|
|
9
9
|
import kleur from 'kleur';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { ui } from '../util/ui.js';
|
|
11
|
+
import { confirm } from '../util/prompt.js';
|
|
12
|
+
import { canRedraw } from '../util/ui.js';
|
|
13
|
+
import { stripMemberSegment } from '../util/workspace.js';
|
|
14
|
+
import { stdout } from 'node:process';
|
|
12
15
|
import { loadConfig, loadState } from '../config.js';
|
|
13
16
|
import { ensureEngineConnect } from '../util/engineConnect.js';
|
|
14
17
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
@@ -20,11 +23,11 @@ export function slugifySegment(input) {
|
|
|
20
23
|
return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
21
24
|
}
|
|
22
25
|
export function parseFolderArg(folder) {
|
|
23
|
-
return folder
|
|
26
|
+
return stripMemberSegment(folder
|
|
24
27
|
.replace(/\\/g, '/')
|
|
25
28
|
.split('/')
|
|
26
29
|
.map((s) => slugifySegment(s.trim()))
|
|
27
|
-
.filter(Boolean);
|
|
30
|
+
.filter(Boolean));
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
33
|
* True when `key` (`<folders…>/<doc>/<artboard>`) lives inside `segments`.
|
|
@@ -38,18 +41,6 @@ export function keyInFolder(key, segments) {
|
|
|
38
41
|
return false;
|
|
39
42
|
return segments.every((s, i) => folderParts[i] === s);
|
|
40
43
|
}
|
|
41
|
-
async function confirm(prompt) {
|
|
42
|
-
if (!stdin.isTTY)
|
|
43
|
-
return false;
|
|
44
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
45
|
-
try {
|
|
46
|
-
const a = (await rl.question(prompt)).trim().toLowerCase();
|
|
47
|
-
return a === 'y' || a === 'yes';
|
|
48
|
-
}
|
|
49
|
-
finally {
|
|
50
|
-
rl.close();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
44
|
export async function resyncCommand(folder, opts = {}) {
|
|
54
45
|
await runResync(parseFolderArg(folder), opts);
|
|
55
46
|
}
|
|
@@ -64,9 +55,9 @@ export async function runResync(segments, opts = {}) {
|
|
|
64
55
|
if (!isEngineKind(kind))
|
|
65
56
|
throw new Error('This folder is not a Unity/Godot/GameMaker project.');
|
|
66
57
|
const gameIndex = await indexGamePngs(kind, process.cwd(), config.outDir, {
|
|
67
|
-
onProgress: opts.quiet ? undefined : (p) => { stdout.write(`\r${countingSpritesText(p)}`); },
|
|
58
|
+
onProgress: opts.quiet || !canRedraw() ? undefined : (p) => { stdout.write(`\r${countingSpritesText(p)}`); },
|
|
68
59
|
});
|
|
69
|
-
if (!opts.quiet)
|
|
60
|
+
if (!opts.quiet && canRedraw())
|
|
70
61
|
stdout.write('\n');
|
|
71
62
|
const keyFilter = (key) => keyInFolder(key, segments);
|
|
72
63
|
const label = segments.join('/');
|
|
@@ -111,8 +102,8 @@ export async function runResync(segments, opts = {}) {
|
|
|
111
102
|
trashed = res.trashed;
|
|
112
103
|
}
|
|
113
104
|
if (pruneSkipped)
|
|
114
|
-
log(
|
|
105
|
+
log(ui.warn(`Nothing moved to Trash (${pruneSkipped}). Re-run once fixed.`));
|
|
115
106
|
else
|
|
116
|
-
log(
|
|
107
|
+
log(ui.ok(`${label} resynced. ${trashed} file${trashed === 1 ? '' : 's'} moved to Trash.`));
|
|
117
108
|
return { push, trashed, pruneSkipped };
|
|
118
109
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
|
-
import
|
|
2
|
+
import { spinner as makeSpinner } from '../util/ui.js';
|
|
3
3
|
import { loadConfig } from '../config.js';
|
|
4
4
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
5
5
|
import { indexGamePngs, searchGameIndex, countingSpritesText } from '../util/gameScan.js';
|
|
@@ -26,7 +26,7 @@ export async function searchCommand(query) {
|
|
|
26
26
|
catch {
|
|
27
27
|
// Config is optional for search — skip MagicPixel outDir when missing.
|
|
28
28
|
}
|
|
29
|
-
const spinner =
|
|
29
|
+
const spinner = makeSpinner({ text: countingSpritesText(0), spinner: 'dots' }).start();
|
|
30
30
|
const index = await indexGamePngs(kind, process.cwd(), outDir, {
|
|
31
31
|
onProgress: (p) => { spinner.text = countingSpritesText(p); },
|
|
32
32
|
});
|
package/dist/commands/start.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
|
+
import { ui } from '../util/ui.js';
|
|
3
|
+
import { confirm } from '../util/prompt.js';
|
|
4
|
+
let stepNo = 0;
|
|
5
|
+
let stepTotal = 0;
|
|
6
|
+
const step = (title) => console.log(ui.step(++stepNo, title, stepTotal));
|
|
2
7
|
import { existsSync } from 'node:fs';
|
|
3
8
|
import { readFile } from 'node:fs/promises';
|
|
4
9
|
import { createInterface } from 'node:readline/promises';
|
|
5
10
|
import { stdin, stdout } from 'node:process';
|
|
6
|
-
import { resolve } from 'node:path';
|
|
11
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
7
12
|
import { configPath, loadConfig } from '../config.js';
|
|
8
13
|
import { detectProjectKind, hasPackageJson, isEngineKind } from '../util/framework.js';
|
|
9
14
|
import { findKeyInDotenv, readCredentialsSync, writeCredentials } from '../util/credentials.js';
|
|
10
15
|
import { initCommand } from './init.js';
|
|
16
|
+
import { consolidateCommand, needsConsolidation } from './consolidate.js';
|
|
17
|
+
import { cleanStraysCommand } from './cleanStrays.js';
|
|
18
|
+
import { runPush } from './push.js';
|
|
19
|
+
import { readWorkspaceMembers } from '../util/framework.js';
|
|
20
|
+
import { detectWorkspaceMembers } from '../util/workspace.js';
|
|
11
21
|
import { loginCommand } from './login.js';
|
|
12
22
|
import { syncCommand } from './sync.js';
|
|
13
23
|
import { cmd } from '../util/invoke.js';
|
|
@@ -15,6 +25,7 @@ import { assertKeyValid } from '../util/auth.js';
|
|
|
15
25
|
import { ApiError } from '../api.js';
|
|
16
26
|
import { connectCommand } from './connect.js';
|
|
17
27
|
import { isAllSpritesGlob } from '../util/engineConnect.js';
|
|
28
|
+
import { COMMAND_TIPS, formatTipsBlock } from '../util/commandTips.js';
|
|
18
29
|
/** Validate a key; only auth rejections map to 'rejected' — other failures throw. */
|
|
19
30
|
export async function checkKey(key, config) {
|
|
20
31
|
try {
|
|
@@ -46,6 +57,35 @@ export async function startCommand(opts = {}) {
|
|
|
46
57
|
console.log(kleur.bold('🪄 MagicPixel — first-run setup'));
|
|
47
58
|
console.log(kleur.dim(' This links your game project and syncs sprites both ways.'));
|
|
48
59
|
console.log();
|
|
60
|
+
// Started inside one game folder of a monorepo: set up once at the root.
|
|
61
|
+
if (!existsSync(configPath()) && stdin.isTTY) {
|
|
62
|
+
const parent = dirname(process.cwd());
|
|
63
|
+
const siblings = detectWorkspaceMembers(parent);
|
|
64
|
+
if (siblings.length >= 2 && siblings.includes(basename(process.cwd()))) {
|
|
65
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
66
|
+
let ans = '';
|
|
67
|
+
try {
|
|
68
|
+
ans = (await rl.question(`${kleur.cyan('?')} ${basename(process.cwd())} is one of ${siblings.length} game folders in ${parent}. Set up one MagicPixel for all of them there? ${kleur.dim('(Y/n)')} `)).trim().toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
rl.close();
|
|
72
|
+
}
|
|
73
|
+
if (ans !== 'n' && ans !== 'no')
|
|
74
|
+
process.chdir(parent);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Monorepo with several game folders: merge their old setups into one here.
|
|
78
|
+
let consolidated = false;
|
|
79
|
+
const merging = needsConsolidation();
|
|
80
|
+
stepNo = 0;
|
|
81
|
+
stepTotal = merging ? 3 : 2;
|
|
82
|
+
if (merging) {
|
|
83
|
+
step('One setup for all your game folders');
|
|
84
|
+
consolidated = await consolidateCommand({});
|
|
85
|
+
if (!consolidated && !readWorkspaceMembers(process.cwd()))
|
|
86
|
+
return;
|
|
87
|
+
console.log();
|
|
88
|
+
}
|
|
49
89
|
const kind = await detectProjectKind();
|
|
50
90
|
// Engine projects (including Unity UPM packages) have no package.json
|
|
51
91
|
// requirement. JS projects still need one.
|
|
@@ -85,36 +125,35 @@ export async function startCommand(opts = {}) {
|
|
|
85
125
|
const config = await loadConfig();
|
|
86
126
|
const envKey = process.env.MAGICPIXEL_API_KEY?.trim();
|
|
87
127
|
const stored = readCredentialsSync();
|
|
128
|
+
console.log();
|
|
129
|
+
step('Your MagicPixel account');
|
|
88
130
|
if (!envKey && !stored) {
|
|
89
|
-
console.log();
|
|
90
|
-
console.log(kleur.bold('Step: connect your account'));
|
|
91
131
|
await loginCommand();
|
|
92
132
|
}
|
|
93
133
|
else if (envKey) {
|
|
94
134
|
if ((await checkKey(envKey, config)) === 'rejected') {
|
|
95
|
-
console.log(
|
|
96
|
-
console.log(
|
|
135
|
+
console.log(ui.fail('The MAGICPIXEL_API_KEY in your environment was rejected.'));
|
|
136
|
+
console.log(ui.fix('set MAGICPIXEL_API_KEY to a fresh key from https://magicpixel.art/settings'));
|
|
97
137
|
process.exitCode = 1;
|
|
98
138
|
return;
|
|
99
139
|
}
|
|
100
|
-
console.log(
|
|
140
|
+
console.log(ui.ok('Using MAGICPIXEL_API_KEY from your environment.'));
|
|
101
141
|
}
|
|
102
142
|
else if (stored && (await checkKey(stored.apiKey, config)) === 'rejected') {
|
|
103
|
-
console.log(
|
|
143
|
+
console.log(ui.warn('Your saved key was rejected (revoked, or from another account).'));
|
|
104
144
|
if (!stdin.isTTY) {
|
|
105
|
-
console.log(
|
|
145
|
+
console.log(ui.fix(`${cmd('login')} --key mp_live_…`));
|
|
106
146
|
process.exitCode = 1;
|
|
107
147
|
return;
|
|
108
148
|
}
|
|
109
|
-
console.log(kleur.bold('Step: connect your account'));
|
|
110
149
|
await loginCommand();
|
|
111
150
|
}
|
|
112
151
|
else {
|
|
113
|
-
console.log(
|
|
152
|
+
console.log(ui.ok('Signed in (saved key in .magicpixel/credentials).'));
|
|
114
153
|
}
|
|
115
154
|
// 4b. Don't push a whole game by surprise — offer one folder first.
|
|
116
155
|
let folder = opts.folder?.trim();
|
|
117
|
-
if (!folder && shouldAskFolder(config.connect, !!stdin.isTTY, isEngineKind(kind))) {
|
|
156
|
+
if (!folder && !config.workspace && shouldAskFolder(config.connect, !!stdin.isTTY, isEngineKind(kind))) {
|
|
118
157
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
119
158
|
try {
|
|
120
159
|
folder = (await rl.question(`${kleur.cyan('?')} Sync all sprites, or one folder? ${kleur.dim('(type a folder like Sprites/Enemies, or press Enter for all)')} `)).trim();
|
|
@@ -125,7 +164,7 @@ export async function startCommand(opts = {}) {
|
|
|
125
164
|
}
|
|
126
165
|
if (folder) {
|
|
127
166
|
console.log();
|
|
128
|
-
|
|
167
|
+
step('Sync one folder');
|
|
129
168
|
await connectCommand(folderToGlob(folder));
|
|
130
169
|
console.log();
|
|
131
170
|
console.log(kleur.dim(` Add another folder later with \`${cmd('connect')} "<folder>/**"\`.`));
|
|
@@ -137,11 +176,14 @@ export async function startCommand(opts = {}) {
|
|
|
137
176
|
// print a misleading green "you're set up" over a half-failed run.
|
|
138
177
|
// Manifest 546s throw after still importing local sprites; catch so
|
|
139
178
|
// the watch instructions below aren't swallowed.
|
|
179
|
+
if (consolidated)
|
|
180
|
+
await removeLeftoverStrays();
|
|
181
|
+
const gameIndex = await maybeFlattenLayered();
|
|
140
182
|
console.log();
|
|
141
|
-
|
|
183
|
+
step('First sync');
|
|
142
184
|
const exitBefore = process.exitCode ?? 0;
|
|
143
185
|
try {
|
|
144
|
-
await syncCommand({ full: true });
|
|
186
|
+
await syncCommand({ full: true }, { gameIndex });
|
|
145
187
|
}
|
|
146
188
|
catch (e) {
|
|
147
189
|
const err = e;
|
|
@@ -157,7 +199,7 @@ export async function startCommand(opts = {}) {
|
|
|
157
199
|
// `npx` form so we never instruct users to run a script they don't have.
|
|
158
200
|
console.log();
|
|
159
201
|
if (firstSyncFailed) {
|
|
160
|
-
console.log(
|
|
202
|
+
console.log(ui.warn('first sync completed with errors.'));
|
|
161
203
|
console.log(kleur.dim(` Re-run \`${cmd('sync')}\` to retry the failed downloads, or \`${cmd('doctor')}\` to diagnose.`));
|
|
162
204
|
}
|
|
163
205
|
else {
|
|
@@ -174,6 +216,8 @@ export async function startCommand(opts = {}) {
|
|
|
174
216
|
console.log();
|
|
175
217
|
console.log(kleur.dim(' Game sprites write back to their original path. New MagicPixel art lands in outDir.'));
|
|
176
218
|
console.log();
|
|
219
|
+
console.log(formatTipsBlock(COMMAND_TIPS.slice(1)));
|
|
220
|
+
console.log();
|
|
177
221
|
if (await hasDevScript()) {
|
|
178
222
|
const watchCmd = hasWatch ? 'npm run magicpixel:watch' : `${cmd('sync')} --watch`;
|
|
179
223
|
console.log(kleur.dim(' Tip: run your dev server and the watcher together with'));
|
|
@@ -181,6 +225,46 @@ export async function startCommand(opts = {}) {
|
|
|
181
225
|
console.log();
|
|
182
226
|
}
|
|
183
227
|
}
|
|
228
|
+
/** After a merge: drop copies an older sync wrote for another folder's sprites. */
|
|
229
|
+
async function removeLeftoverStrays() {
|
|
230
|
+
try {
|
|
231
|
+
await cleanStraysCommand({ yes: true });
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
console.log(ui.warn(`couldn't check for stray copies: ${e.message.split('\n')[0]}`));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Local files win: offer to replace multi-layer MagicPixel artboards that a
|
|
239
|
+
* normal upload refuses, instead of silently skipping them.
|
|
240
|
+
*/
|
|
241
|
+
async function maybeFlattenLayered() {
|
|
242
|
+
if (!stdin.isTTY)
|
|
243
|
+
return undefined;
|
|
244
|
+
let keys = [];
|
|
245
|
+
let gameIndex;
|
|
246
|
+
try {
|
|
247
|
+
const plan = await runPush({ dryRun: true, quiet: true, nestedSync: true });
|
|
248
|
+
keys = plan.needsFlatten ?? [];
|
|
249
|
+
gameIndex = plan.gameIndex;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
if (keys.length === 0)
|
|
255
|
+
return gameIndex;
|
|
256
|
+
console.log();
|
|
257
|
+
console.log(ui.warn(`${keys.length} of your files match a MagicPixel artboard with several layers:`));
|
|
258
|
+
for (const k of keys.slice(0, 10))
|
|
259
|
+
console.log(kleur.dim(` ${k}`));
|
|
260
|
+
if (keys.length > 10)
|
|
261
|
+
console.log(kleur.dim(` …and ${keys.length - 10} more`));
|
|
262
|
+
if (!(await confirm(`${kleur.cyan('?')} Replace them with your local files (layers are flattened)? ${kleur.dim('(y/N)')} `)))
|
|
263
|
+
return gameIndex;
|
|
264
|
+
const only = new Set(keys);
|
|
265
|
+
await runPush({ flatten: true, keyFilter: (k) => only.has(k), gameIndex });
|
|
266
|
+
return gameIndex;
|
|
267
|
+
}
|
|
184
268
|
async function readPkgJson() {
|
|
185
269
|
try {
|
|
186
270
|
const path = resolve(process.cwd(), 'package.json');
|
|
@@ -220,7 +304,7 @@ async function maybeMigrateDotenvKey() {
|
|
|
220
304
|
if (ans === 'n' || ans === 'no')
|
|
221
305
|
return;
|
|
222
306
|
await writeCredentials(found.value);
|
|
223
|
-
console.log(
|
|
307
|
+
console.log(ui.ok(`migrated key from ${found.file} → .magicpixel/credentials`));
|
|
224
308
|
console.log(kleur.dim(` You can now remove the MAGICPIXEL_API_KEY line from ${found.file}.`));
|
|
225
309
|
}
|
|
226
310
|
finally {
|