@magicpixelart/cli 0.5.38 → 0.6.1
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 +41 -0
- package/README.md +24 -1
- package/dist/commands/add.js +2 -1
- package/dist/commands/cleanStrays.js +92 -0
- package/dist/commands/connect.js +3 -3
- package/dist/commands/consolidate.js +308 -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 +51 -17
- 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 +97 -16
- package/dist/commands/sync.js +125 -60
- package/dist/commands/whoami.js +4 -3
- package/dist/commands/why.js +91 -0
- package/dist/config.js +3 -0
- package/dist/index.js +46 -4
- 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 +156 -0
- package/dist/util/workspace.js +82 -0
- package/package.json +1 -1
package/dist/commands/push.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `POST /api/public/integration/ingest`.
|
|
8
8
|
*/
|
|
9
9
|
import kleur from 'kleur';
|
|
10
|
-
import
|
|
10
|
+
import { icon, progressBar, spinner as makeSpinner, ui } 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';
|
|
@@ -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)
|
|
@@ -90,12 +103,11 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
90
103
|
let hashed = 0;
|
|
91
104
|
const reportHash = () => {
|
|
92
105
|
hashed++;
|
|
93
|
-
const
|
|
94
|
-
const t = toHash.length.toLocaleString('en-US');
|
|
106
|
+
const line = `Checking your game files ${progressBar(hashed, toHash.length)}`;
|
|
95
107
|
if (spinner)
|
|
96
|
-
spinner.text =
|
|
108
|
+
spinner.text = line;
|
|
97
109
|
else if (opts.onStatus && (hashed === 1 || hashed === toHash.length || hashed % 25 === 0)) {
|
|
98
|
-
opts.onStatus(
|
|
110
|
+
opts.onStatus(`${icon('work')} ${line}`);
|
|
99
111
|
}
|
|
100
112
|
};
|
|
101
113
|
await Promise.all(toHash.map(([key, abs]) => scan(async () => {
|
|
@@ -149,31 +161,33 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
149
161
|
const legacy = actions.filter((a) => a.kind === 'skip' && a.reason === 'legacy');
|
|
150
162
|
const flattenBlocked = actions.filter((a) => a.kind === 'needs-flatten');
|
|
151
163
|
const sendable = actions.filter((a) => a.kind === 'update' || a.kind === 'adopt');
|
|
164
|
+
const preIssues = skipIssues(legacy, flattenBlocked.map((a) => ({ key: a.key, ...('layers' in a && typeof a.layers === 'number' ? { layers: a.layers } : {}) })));
|
|
152
165
|
spinner?.succeed(`Local sprites: ${candidates.length} · to push ${sendable.length} · unchanged ${skipped}` +
|
|
153
166
|
(matched.entries.length ? ` · connected ${matched.entries.length}` : ''));
|
|
154
167
|
if (skippedUnsafe > 0 && !quiet) {
|
|
155
168
|
console.log(kleur.yellow(`! skipped ${skippedUnsafe} path${skippedUnsafe === 1 ? '' : 's'} that were not regular files inside the project.`));
|
|
156
169
|
}
|
|
157
|
-
if (legacy.length > 0 && !quiet
|
|
170
|
+
if (legacy.length > 0 && !quiet) {
|
|
158
171
|
console.log();
|
|
159
172
|
console.log(kleur.yellow(`! ${legacy.length} sprite${legacy.length === 1 ? '' : 's'} came from a legacy single-image file — skipped.`));
|
|
160
|
-
console.log(
|
|
173
|
+
console.log(ui.fixText(`open the file in MagicPixel and save it once, then re-run \`${cmd('sync')}\`.`));
|
|
161
174
|
}
|
|
162
|
-
if (flattenBlocked.length > 0 && !quiet
|
|
175
|
+
if (flattenBlocked.length > 0 && !quiet) {
|
|
163
176
|
console.log();
|
|
164
177
|
console.log(kleur.yellow(`! ${flattenBlocked.length} sprite${flattenBlocked.length === 1 ? '' : 's'} would flatten a multi-layer artboard — skipped.`));
|
|
165
178
|
for (const a of flattenBlocked.slice(0, 10)) {
|
|
166
179
|
console.log(kleur.dim(` ${a.key} (${'layers' in a ? a.layers : '?'} layers)`));
|
|
167
180
|
}
|
|
168
|
-
console.log(
|
|
181
|
+
console.log(ui.fixText('re-run with --flatten to replace them with the flat disk image.'));
|
|
169
182
|
}
|
|
170
183
|
if (sendable.length === 0) {
|
|
171
184
|
await persistDiskFingerprints(state, candidates);
|
|
172
185
|
if (!quiet)
|
|
173
|
-
console.log(
|
|
186
|
+
console.log(ui.ok('nothing to push.'));
|
|
174
187
|
return {
|
|
175
188
|
...empty, imported: matched.entries.length, unchanged: skipped,
|
|
176
189
|
keptAssetIds: collectKeptAssetIds(candidates, state.synced ?? {}, []), scanned: candidates.length,
|
|
190
|
+
issues: preIssues, needsFlatten: flattenBlocked.map((a) => a.key), gameIndex: index,
|
|
177
191
|
};
|
|
178
192
|
}
|
|
179
193
|
if (opts.dryRun) {
|
|
@@ -188,6 +202,7 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
188
202
|
return {
|
|
189
203
|
...empty, imported: matched.entries.length,
|
|
190
204
|
keptAssetIds: collectKeptAssetIds(candidates, state.synced ?? {}, []), scanned: candidates.length,
|
|
205
|
+
issues: preIssues, needsFlatten: flattenBlocked.map((a) => a.key), gameIndex: index,
|
|
191
206
|
};
|
|
192
207
|
}
|
|
193
208
|
const outRoot = resolve(process.cwd(), config.outDir);
|
|
@@ -221,7 +236,8 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
221
236
|
});
|
|
222
237
|
}
|
|
223
238
|
}
|
|
224
|
-
const progress = quiet ? null :
|
|
239
|
+
const progress = quiet ? null : makeSpinner(`Uploading ${progressBar(0, sprites.length)}`).start();
|
|
240
|
+
opts.onStatus?.(`${icon('up')} Uploading ${sprites.length.toLocaleString('en-US')} sprite${sprites.length === 1 ? '' : 's'}…`);
|
|
225
241
|
const results = [];
|
|
226
242
|
let synced = { ...(state.synced ?? {}) };
|
|
227
243
|
try {
|
|
@@ -239,8 +255,11 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
239
255
|
outDir: config.outDir,
|
|
240
256
|
});
|
|
241
257
|
await saveState({ ...state, synced });
|
|
258
|
+
const doneN = Math.min(i + batch.length, sprites.length);
|
|
242
259
|
if (progress)
|
|
243
|
-
progress.text = `
|
|
260
|
+
progress.text = `Uploading ${progressBar(doneN, sprites.length)} ${kleur.dim(batch[batch.length - 1]?.name ?? '')}`;
|
|
261
|
+
else
|
|
262
|
+
opts.onStatus?.(`${icon('up')} Uploading ${doneN}/${sprites.length} ${batch[batch.length - 1]?.name ?? ''}…`);
|
|
244
263
|
}
|
|
245
264
|
}
|
|
246
265
|
catch (e) {
|
|
@@ -300,11 +319,11 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
300
319
|
for (const r of results) {
|
|
301
320
|
if (r.status === 'conflict') {
|
|
302
321
|
if (printedIssues++ < 10)
|
|
303
|
-
console.log(` ${
|
|
322
|
+
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
323
|
}
|
|
305
324
|
else if (r.status === 'error') {
|
|
306
325
|
if (printedIssues++ < 10)
|
|
307
|
-
console.log(` ${
|
|
326
|
+
console.log(` ${icon('fail')} ${r.key}: ${r.message ?? 'push failed'}`);
|
|
308
327
|
}
|
|
309
328
|
}
|
|
310
329
|
if (printedIssues > 10)
|
|
@@ -316,9 +335,24 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
316
335
|
}
|
|
317
336
|
if (failed)
|
|
318
337
|
process.exitCode = 1;
|
|
338
|
+
const issues = [...preIssues];
|
|
339
|
+
for (const r of results) {
|
|
340
|
+
if (r.status === 'conflict') {
|
|
341
|
+
issues.push({
|
|
342
|
+
key: r.key,
|
|
343
|
+
text: opts.force && r.lastWriteSource === 'editor' && !opts.overwriteEditorChanges
|
|
344
|
+
? 'edited in MagicPixel — skipped. Add --overwrite-editor-changes to replace it.'
|
|
345
|
+
: conflictHint(r.reason),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else if (r.status === 'error') {
|
|
349
|
+
issues.push({ key: r.key, text: r.message ?? 'push failed' });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
319
352
|
return {
|
|
320
353
|
...counts, imported: matched.entries.length,
|
|
321
354
|
keptAssetIds: collectKeptAssetIds(candidates, synced, results), scanned: candidates.length,
|
|
355
|
+
issues, needsFlatten: flattenBlocked.map((a) => a.key),
|
|
322
356
|
};
|
|
323
357
|
}
|
|
324
358
|
/**
|
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';
|
|
@@ -46,6 +56,35 @@ export async function startCommand(opts = {}) {
|
|
|
46
56
|
console.log(kleur.bold('🪄 MagicPixel — first-run setup'));
|
|
47
57
|
console.log(kleur.dim(' This links your game project and syncs sprites both ways.'));
|
|
48
58
|
console.log();
|
|
59
|
+
// Started inside one game folder of a monorepo: set up once at the root.
|
|
60
|
+
if (!existsSync(configPath()) && stdin.isTTY) {
|
|
61
|
+
const parent = dirname(process.cwd());
|
|
62
|
+
const siblings = detectWorkspaceMembers(parent);
|
|
63
|
+
if (siblings.length >= 2 && siblings.includes(basename(process.cwd()))) {
|
|
64
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
65
|
+
let ans = '';
|
|
66
|
+
try {
|
|
67
|
+
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();
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
rl.close();
|
|
71
|
+
}
|
|
72
|
+
if (ans !== 'n' && ans !== 'no')
|
|
73
|
+
process.chdir(parent);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Monorepo with several game folders: merge their old setups into one here.
|
|
77
|
+
let consolidated = false;
|
|
78
|
+
const merging = needsConsolidation();
|
|
79
|
+
stepNo = 0;
|
|
80
|
+
stepTotal = merging ? 3 : 2;
|
|
81
|
+
if (merging) {
|
|
82
|
+
step('One setup for all your game folders');
|
|
83
|
+
consolidated = await consolidateCommand({});
|
|
84
|
+
if (!consolidated && !readWorkspaceMembers(process.cwd()))
|
|
85
|
+
return;
|
|
86
|
+
console.log();
|
|
87
|
+
}
|
|
49
88
|
const kind = await detectProjectKind();
|
|
50
89
|
// Engine projects (including Unity UPM packages) have no package.json
|
|
51
90
|
// requirement. JS projects still need one.
|
|
@@ -85,36 +124,35 @@ export async function startCommand(opts = {}) {
|
|
|
85
124
|
const config = await loadConfig();
|
|
86
125
|
const envKey = process.env.MAGICPIXEL_API_KEY?.trim();
|
|
87
126
|
const stored = readCredentialsSync();
|
|
127
|
+
console.log();
|
|
128
|
+
step('Your MagicPixel account');
|
|
88
129
|
if (!envKey && !stored) {
|
|
89
|
-
console.log();
|
|
90
|
-
console.log(kleur.bold('Step: connect your account'));
|
|
91
130
|
await loginCommand();
|
|
92
131
|
}
|
|
93
132
|
else if (envKey) {
|
|
94
133
|
if ((await checkKey(envKey, config)) === 'rejected') {
|
|
95
|
-
console.log(
|
|
96
|
-
console.log(
|
|
134
|
+
console.log(ui.fail('The MAGICPIXEL_API_KEY in your environment was rejected.'));
|
|
135
|
+
console.log(ui.fix('set MAGICPIXEL_API_KEY to a fresh key from https://magicpixel.art/settings'));
|
|
97
136
|
process.exitCode = 1;
|
|
98
137
|
return;
|
|
99
138
|
}
|
|
100
|
-
console.log(
|
|
139
|
+
console.log(ui.ok('Using MAGICPIXEL_API_KEY from your environment.'));
|
|
101
140
|
}
|
|
102
141
|
else if (stored && (await checkKey(stored.apiKey, config)) === 'rejected') {
|
|
103
|
-
console.log(
|
|
142
|
+
console.log(ui.warn('Your saved key was rejected (revoked, or from another account).'));
|
|
104
143
|
if (!stdin.isTTY) {
|
|
105
|
-
console.log(
|
|
144
|
+
console.log(ui.fix(`${cmd('login')} --key mp_live_…`));
|
|
106
145
|
process.exitCode = 1;
|
|
107
146
|
return;
|
|
108
147
|
}
|
|
109
|
-
console.log(kleur.bold('Step: connect your account'));
|
|
110
148
|
await loginCommand();
|
|
111
149
|
}
|
|
112
150
|
else {
|
|
113
|
-
console.log(
|
|
151
|
+
console.log(ui.ok('Signed in (saved key in .magicpixel/credentials).'));
|
|
114
152
|
}
|
|
115
153
|
// 4b. Don't push a whole game by surprise — offer one folder first.
|
|
116
154
|
let folder = opts.folder?.trim();
|
|
117
|
-
if (!folder && shouldAskFolder(config.connect, !!stdin.isTTY, isEngineKind(kind))) {
|
|
155
|
+
if (!folder && !config.workspace && shouldAskFolder(config.connect, !!stdin.isTTY, isEngineKind(kind))) {
|
|
118
156
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
119
157
|
try {
|
|
120
158
|
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 +163,7 @@ export async function startCommand(opts = {}) {
|
|
|
125
163
|
}
|
|
126
164
|
if (folder) {
|
|
127
165
|
console.log();
|
|
128
|
-
|
|
166
|
+
step('Sync one folder');
|
|
129
167
|
await connectCommand(folderToGlob(folder));
|
|
130
168
|
console.log();
|
|
131
169
|
console.log(kleur.dim(` Add another folder later with \`${cmd('connect')} "<folder>/**"\`.`));
|
|
@@ -137,11 +175,14 @@ export async function startCommand(opts = {}) {
|
|
|
137
175
|
// print a misleading green "you're set up" over a half-failed run.
|
|
138
176
|
// Manifest 546s throw after still importing local sprites; catch so
|
|
139
177
|
// the watch instructions below aren't swallowed.
|
|
178
|
+
if (consolidated)
|
|
179
|
+
await removeLeftoverStrays();
|
|
180
|
+
const gameIndex = await maybeFlattenLayered();
|
|
140
181
|
console.log();
|
|
141
|
-
|
|
182
|
+
step('First sync');
|
|
142
183
|
const exitBefore = process.exitCode ?? 0;
|
|
143
184
|
try {
|
|
144
|
-
await syncCommand({ full: true });
|
|
185
|
+
await syncCommand({ full: true }, { gameIndex });
|
|
145
186
|
}
|
|
146
187
|
catch (e) {
|
|
147
188
|
const err = e;
|
|
@@ -157,7 +198,7 @@ export async function startCommand(opts = {}) {
|
|
|
157
198
|
// `npx` form so we never instruct users to run a script they don't have.
|
|
158
199
|
console.log();
|
|
159
200
|
if (firstSyncFailed) {
|
|
160
|
-
console.log(
|
|
201
|
+
console.log(ui.warn('first sync completed with errors.'));
|
|
161
202
|
console.log(kleur.dim(` Re-run \`${cmd('sync')}\` to retry the failed downloads, or \`${cmd('doctor')}\` to diagnose.`));
|
|
162
203
|
}
|
|
163
204
|
else {
|
|
@@ -181,6 +222,46 @@ export async function startCommand(opts = {}) {
|
|
|
181
222
|
console.log();
|
|
182
223
|
}
|
|
183
224
|
}
|
|
225
|
+
/** After a merge: drop copies an older sync wrote for another folder's sprites. */
|
|
226
|
+
async function removeLeftoverStrays() {
|
|
227
|
+
try {
|
|
228
|
+
await cleanStraysCommand({ yes: true });
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
console.log(ui.warn(`couldn't check for stray copies: ${e.message.split('\n')[0]}`));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Local files win: offer to replace multi-layer MagicPixel artboards that a
|
|
236
|
+
* normal upload refuses, instead of silently skipping them.
|
|
237
|
+
*/
|
|
238
|
+
async function maybeFlattenLayered() {
|
|
239
|
+
if (!stdin.isTTY)
|
|
240
|
+
return undefined;
|
|
241
|
+
let keys = [];
|
|
242
|
+
let gameIndex;
|
|
243
|
+
try {
|
|
244
|
+
const plan = await runPush({ dryRun: true, quiet: true, nestedSync: true });
|
|
245
|
+
keys = plan.needsFlatten ?? [];
|
|
246
|
+
gameIndex = plan.gameIndex;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
if (keys.length === 0)
|
|
252
|
+
return gameIndex;
|
|
253
|
+
console.log();
|
|
254
|
+
console.log(ui.warn(`${keys.length} of your files match a MagicPixel artboard with several layers:`));
|
|
255
|
+
for (const k of keys.slice(0, 10))
|
|
256
|
+
console.log(kleur.dim(` ${k}`));
|
|
257
|
+
if (keys.length > 10)
|
|
258
|
+
console.log(kleur.dim(` …and ${keys.length - 10} more`));
|
|
259
|
+
if (!(await confirm(`${kleur.cyan('?')} Replace them with your local files (layers are flattened)? ${kleur.dim('(y/N)')} `)))
|
|
260
|
+
return gameIndex;
|
|
261
|
+
const only = new Set(keys);
|
|
262
|
+
await runPush({ flatten: true, keyFilter: (k) => only.has(k), gameIndex });
|
|
263
|
+
return gameIndex;
|
|
264
|
+
}
|
|
184
265
|
async function readPkgJson() {
|
|
185
266
|
try {
|
|
186
267
|
const path = resolve(process.cwd(), 'package.json');
|
|
@@ -220,7 +301,7 @@ async function maybeMigrateDotenvKey() {
|
|
|
220
301
|
if (ans === 'n' || ans === 'no')
|
|
221
302
|
return;
|
|
222
303
|
await writeCredentials(found.value);
|
|
223
|
-
console.log(
|
|
304
|
+
console.log(ui.ok(`migrated key from ${found.file} → .magicpixel/credentials`));
|
|
224
305
|
console.log(kleur.dim(` You can now remove the MAGICPIXEL_API_KEY line from ${found.file}.`));
|
|
225
306
|
}
|
|
226
307
|
finally {
|