@magicpixelart/cli 0.3.3 → 0.4.0
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 +415 -0
- package/README.md +31 -2
- package/dist/api.js +135 -72
- package/dist/commands/doctor.js +201 -56
- package/dist/commands/init.js +25 -15
- package/dist/commands/list.js +6 -5
- package/dist/commands/login.js +3 -34
- package/dist/commands/repair.js +157 -0
- package/dist/commands/start.js +56 -14
- package/dist/commands/status.js +41 -13
- package/dist/commands/sync.js +279 -144
- package/dist/commands/whoami.js +52 -12
- package/dist/config.js +88 -20
- package/dist/index.js +38 -23
- package/dist/util/atomicWrite.js +25 -0
- package/dist/util/auth.js +41 -0
- package/dist/util/authHeaders.js +24 -0
- package/dist/util/credentials.js +27 -10
- package/dist/util/emitIndex.js +78 -70
- package/dist/util/errors.js +49 -0
- package/dist/util/flagValidators.js +49 -0
- package/dist/util/format.js +14 -0
- package/dist/util/framework.js +10 -0
- package/dist/util/iso.js +28 -0
- package/dist/util/paths.js +99 -0
- package/dist/util/security.js +39 -2
- package/dist/util/telemetry.js +182 -0
- package/package.json +11 -4
package/dist/commands/list.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
2
|
import { loadConfig } from '../config.js';
|
|
3
3
|
import { fetchAllManifest } from '../api.js';
|
|
4
|
+
import { formatBytes } from '../util/format.js';
|
|
4
5
|
export async function listCommand() {
|
|
5
6
|
const config = await loadConfig();
|
|
6
7
|
const entries = await fetchAllManifest(config);
|
|
@@ -9,15 +10,15 @@ export async function listCommand() {
|
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
11
12
|
const widthKey = Math.max(3, ...entries.map((e) => e.key.length));
|
|
12
|
-
const
|
|
13
|
+
const sizes = entries.map((e) => (e.size_bytes != null ? formatBytes(e.size_bytes) : '-'));
|
|
14
|
+
const widthSize = Math.max(4, ...sizes.map((s) => s.length));
|
|
13
15
|
console.log(kleur.bold('KEY'.padEnd(widthKey)) + ' ' +
|
|
14
16
|
kleur.bold('SIZE'.padEnd(widthSize)) + ' ' +
|
|
15
17
|
kleur.bold('SHA256'));
|
|
16
|
-
|
|
17
|
-
const size = e.size_bytes != null ? `${e.size_bytes}B` : '-';
|
|
18
|
+
entries.forEach((e, i) => {
|
|
18
19
|
const sha = e.sha256 ? e.sha256.slice(0, 12) : '-';
|
|
19
|
-
console.log(`${e.key.padEnd(widthKey)} ${
|
|
20
|
-
}
|
|
20
|
+
console.log(`${e.key.padEnd(widthKey)} ${sizes[i].padEnd(widthSize)} ${sha}`);
|
|
21
|
+
});
|
|
21
22
|
console.log();
|
|
22
23
|
console.log(kleur.dim(`${entries.length} asset${entries.length === 1 ? '' : 's'}`));
|
|
23
24
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import {
|
|
5
|
-
import { writeCredentials
|
|
6
|
-
import {
|
|
7
|
-
import { CLI_USER_AGENT } from '../version.js';
|
|
8
|
-
import { existsSync } from 'node:fs';
|
|
4
|
+
import { loadConfig, defaultConfig } from '../config.js';
|
|
5
|
+
import { writeCredentials } from '../util/credentials.js';
|
|
6
|
+
import { assertKeyValid } from '../util/auth.js';
|
|
9
7
|
import { relative } from 'node:path';
|
|
10
8
|
const KEY_RE = /^mp_(live|test)_[a-f0-9]{64}$/;
|
|
11
9
|
export async function loginCommand(opts = {}) {
|
|
@@ -67,32 +65,3 @@ async function promptForKey(maxAttempts, config) {
|
|
|
67
65
|
rl.close();
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
|
-
async function assertKeyValid(key, config) {
|
|
71
|
-
const url = new URL(`${resolveEndpoint(config)}/manifest`);
|
|
72
|
-
url.searchParams.set('limit', '1');
|
|
73
|
-
let res;
|
|
74
|
-
try {
|
|
75
|
-
res = await safeFetch(url.href, {
|
|
76
|
-
headers: { Authorization: `Bearer ${key}`, 'User-Agent': CLI_USER_AGENT },
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
catch (e) {
|
|
80
|
-
throw new Error(`Could not reach MagicPixel (${e.message}). Try again in a moment.`);
|
|
81
|
-
}
|
|
82
|
-
if (res.status === 401 || res.status === 403) {
|
|
83
|
-
await res.body?.cancel();
|
|
84
|
-
throw new Error('Key was rejected (401). Generate a fresh one at https://magicpixel.art/settings.');
|
|
85
|
-
}
|
|
86
|
-
if (!res.ok) {
|
|
87
|
-
const body = await res.text();
|
|
88
|
-
throw new Error(`Server returned ${res.status}: ${body.slice(0, 120)}`);
|
|
89
|
-
}
|
|
90
|
-
await res.body?.cancel();
|
|
91
|
-
}
|
|
92
|
-
// Kept here so `start` can reuse the same exact prompt-and-validate flow.
|
|
93
|
-
export async function promptAndStoreKey(config) {
|
|
94
|
-
if (existsSync(credentialsPath()) && !stdin.isTTY)
|
|
95
|
-
return;
|
|
96
|
-
const key = await promptForKey(3, config);
|
|
97
|
-
await writeCredentials(key);
|
|
98
|
-
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { rename } from 'node:fs/promises';
|
|
4
|
+
import { resolve, relative } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { stdin, stdout } from 'node:process';
|
|
7
|
+
import { loadConfig, statePath, getApiKey } from '../config.js';
|
|
8
|
+
import { assertKeyValid } from '../util/auth.js';
|
|
9
|
+
import { listEmptyDirs, pruneEmptyDirs } from '../util/paths.js';
|
|
10
|
+
import { syncCommand } from './sync.js';
|
|
11
|
+
/**
|
|
12
|
+
* One-shot self-healing command. Runs the standard "turn it off and on
|
|
13
|
+
* again" recovery sequence so support can say "run `npx magicpixel repair`":
|
|
14
|
+
*
|
|
15
|
+
* 1. Validate the stored API key.
|
|
16
|
+
* 2. Quarantine `state.json` so the next sync re-derives everything.
|
|
17
|
+
* 3. Prune empty subdirectories under `outDir`.
|
|
18
|
+
* 4. Run a full sync (`--full --prune`).
|
|
19
|
+
*
|
|
20
|
+
* Idempotent. `--dry-run` reports the plan without writing. `--yes` skips the
|
|
21
|
+
* confirmation prompt for step 2.
|
|
22
|
+
*/
|
|
23
|
+
export async function repairCommand(opts = {}) {
|
|
24
|
+
console.log(kleur.bold('MagicPixel repair'));
|
|
25
|
+
console.log(kleur.dim(' Validates your setup, clears local sync state, and re-fetches everything.'));
|
|
26
|
+
console.log();
|
|
27
|
+
const config = await loadConfig();
|
|
28
|
+
const sPath = statePath();
|
|
29
|
+
// --- Step 1: validate API key --------------------------------------------
|
|
30
|
+
console.log(kleur.bold('1/4 ') + 'Validating API key…');
|
|
31
|
+
try {
|
|
32
|
+
const key = getApiKey();
|
|
33
|
+
await assertKeyValid(key, config);
|
|
34
|
+
console.log(` ${kleur.green('✓')} key accepted`);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
// Preserve multi-line "Fix:" guidance from getApiKey()/assertKeyValid —
|
|
38
|
+
// truncating to the first line strips the exact instructions a
|
|
39
|
+
// first-time `repair` user needs.
|
|
40
|
+
const msg = e.message ?? String(e);
|
|
41
|
+
const [head, ...rest] = msg.split('\n');
|
|
42
|
+
console.log(` ${kleur.red('✗')} ${head}`);
|
|
43
|
+
for (const line of rest)
|
|
44
|
+
console.log(` ${line}`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// --- Step 2: quarantine state.json ---------------------------------------
|
|
49
|
+
console.log(kleur.bold('2/4 ') + 'Resetting local sync state…');
|
|
50
|
+
const stateExists = existsSync(sPath);
|
|
51
|
+
// In dry-run we predict whether a real run *would* skip step 2: that
|
|
52
|
+
// happens only when there's no --yes AND no TTY for the prompt to come
|
|
53
|
+
// from (CI / piped input). Otherwise the prompt would run interactively
|
|
54
|
+
// and we can't know the answer from here — assume "would proceed" for
|
|
55
|
+
// the dry-run summary, matching the optimistic "plan looks good" line.
|
|
56
|
+
let skippedStep2 = false;
|
|
57
|
+
if (!stateExists) {
|
|
58
|
+
console.log(` ${kleur.dim('— no state.json on disk; nothing to reset')}`);
|
|
59
|
+
}
|
|
60
|
+
else if (opts.dryRun) {
|
|
61
|
+
const dest = `${sPath}.repair-<ts>`;
|
|
62
|
+
console.log(` ${kleur.dim(`would move: ${relative(process.cwd(), sPath)}`)}`);
|
|
63
|
+
console.log(` ${kleur.dim(` → ${relative(process.cwd(), dest)}`)}`);
|
|
64
|
+
if (!opts.yes && !stdin.isTTY) {
|
|
65
|
+
// Predict the skip a real run would hit, so step 4's note and the
|
|
66
|
+
// closing summary stay honest.
|
|
67
|
+
skippedStep2 = true;
|
|
68
|
+
console.log(` ${kleur.yellow('note')} ${kleur.dim('non-TTY without --yes — a real run would skip this step. Re-run with --yes.')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
if (!opts.yes && !(await confirm(`Move ${relative(process.cwd(), sPath)} aside? (y/N) `))) {
|
|
73
|
+
console.log(` ${kleur.yellow('skipped')} (re-run with --yes to skip the prompt)`);
|
|
74
|
+
skippedStep2 = true;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const dest = `${sPath}.repair-${Date.now()}`;
|
|
78
|
+
try {
|
|
79
|
+
await rename(sPath, dest);
|
|
80
|
+
console.log(` ${kleur.green('✓')} moved to ${relative(process.cwd(), dest)}`);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
console.log(` ${kleur.red('✗')} ${e.message}`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// --- Step 3: prune empty dirs --------------------------------------------
|
|
90
|
+
console.log(kleur.bold('3/4 ') + 'Pruning empty directories under outDir…');
|
|
91
|
+
const outRoot = resolve(process.cwd(), config.outDir);
|
|
92
|
+
if (!existsSync(outRoot)) {
|
|
93
|
+
console.log(` ${kleur.dim(`— outDir doesn't exist yet (${config.outDir})`)}`);
|
|
94
|
+
}
|
|
95
|
+
else if (opts.dryRun) {
|
|
96
|
+
const empties = await listEmptyDirs(outRoot);
|
|
97
|
+
if (empties.length === 0) {
|
|
98
|
+
console.log(` ${kleur.dim('— no empty subdirs under outDir')}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(` ${kleur.dim(`would remove ${empties.length} empty subdir${empties.length === 1 ? '' : 's'}:`)}`);
|
|
102
|
+
for (const p of empties.slice(0, 10)) {
|
|
103
|
+
console.log(` ${kleur.dim(` - ${relative(process.cwd(), p)}`)}`);
|
|
104
|
+
}
|
|
105
|
+
if (empties.length > 10) {
|
|
106
|
+
console.log(` ${kleur.dim(` …and ${empties.length - 10} more`)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
await pruneEmptyDirs(outRoot);
|
|
112
|
+
console.log(` ${kleur.green('✓')} done`);
|
|
113
|
+
}
|
|
114
|
+
// --- Step 4: full sync ---------------------------------------------------
|
|
115
|
+
console.log(kleur.bold('4/4 ') + 'Running full sync…');
|
|
116
|
+
if (skippedStep2) {
|
|
117
|
+
console.log(kleur.dim(' (state was not reset — this will run a normal full sync, not a recovery)'));
|
|
118
|
+
}
|
|
119
|
+
if (opts.dryRun) {
|
|
120
|
+
console.log(` ${kleur.dim('would run `magicpixel sync --full` (skipped in --dry-run)')}`);
|
|
121
|
+
console.log();
|
|
122
|
+
if (skippedStep2) {
|
|
123
|
+
console.log(kleur.yellow('! repair plan would skip the state reset. Re-run with --yes to apply it.'));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
console.log(kleur.green('✓ repair plan looks good. Re-run without --dry-run to apply.'));
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
console.log();
|
|
131
|
+
// syncCommand sets `process.exitCode = 1` on download failures but does not
|
|
132
|
+
// throw. Snapshot the value before/after so we can honour it in the final
|
|
133
|
+
// message — otherwise we'd print a misleading green "complete" line over a
|
|
134
|
+
// run that exited non-zero. Default to 0 so an already-failed prior step
|
|
135
|
+
// doesn't mask a fresh failure (`1 === 1` would look like success).
|
|
136
|
+
const exitBefore = process.exitCode ?? 0;
|
|
137
|
+
await syncCommand({ full: true, prune: true });
|
|
138
|
+
console.log();
|
|
139
|
+
if ((process.exitCode ?? 0) > exitBefore) {
|
|
140
|
+
console.log(kleur.yellow('! repair completed with errors — re-run `magicpixel sync` to retry the failed downloads.'));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.log(kleur.green('✓ repair complete.'));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function confirm(prompt) {
|
|
147
|
+
if (!stdin.isTTY)
|
|
148
|
+
return false; // CI/non-interactive: default to NO; require --yes.
|
|
149
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
150
|
+
try {
|
|
151
|
+
const answer = (await rl.question(` ${prompt}`)).trim().toLowerCase();
|
|
152
|
+
return answer === 'y' || answer === 'yes';
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
rl.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
package/dist/commands/start.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { createInterface } from 'node:readline/promises';
|
|
4
5
|
import { stdin, stdout } from 'node:process';
|
|
5
6
|
import { resolve } from 'node:path';
|
|
6
|
-
import { configPath } from '../config.js';
|
|
7
|
+
import { configPath, loadConfig } from '../config.js';
|
|
7
8
|
import { hasPackageJson } from '../util/framework.js';
|
|
8
9
|
import { findKeyInDotenv, readCredentialsSync, writeCredentials } from '../util/credentials.js';
|
|
9
10
|
import { initCommand } from './init.js';
|
|
@@ -25,10 +26,23 @@ export async function startCommand(opts = {}) {
|
|
|
25
26
|
' Run this inside your project folder — the one with package.json.'));
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
|
-
// 2. Run init non-interactively unless config already exists.
|
|
29
|
+
// 2. Run init non-interactively unless config already exists AND is valid.
|
|
30
|
+
// A broken `magicpixel.json` (hand-edited / truncated) used to slip past
|
|
31
|
+
// the existsSync check here and then explode deep inside `syncCommand`.
|
|
29
32
|
const cfgPath = configPath();
|
|
30
33
|
if (existsSync(cfgPath) && !opts.force) {
|
|
31
|
-
|
|
34
|
+
try {
|
|
35
|
+
await loadConfig();
|
|
36
|
+
console.log(kleur.dim(` Found existing magicpixel.json — skipping init.`));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
const msg = e.message ?? String(e);
|
|
40
|
+
console.log(kleur.yellow(` Existing magicpixel.json is invalid:`));
|
|
41
|
+
for (const line of msg.split('\n'))
|
|
42
|
+
console.log(kleur.yellow(` ${line}`));
|
|
43
|
+
console.log(kleur.dim(' Re-run `npx magicpixel start --force` to overwrite it, or fix the file by hand.'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
32
46
|
}
|
|
33
47
|
else {
|
|
34
48
|
await initCommand({ yes: true, force: opts.force });
|
|
@@ -49,35 +63,63 @@ export async function startCommand(opts = {}) {
|
|
|
49
63
|
else {
|
|
50
64
|
console.log(kleur.dim(' Using stored credentials from .magicpixel/credentials.'));
|
|
51
65
|
}
|
|
52
|
-
// 5. First sync.
|
|
66
|
+
// 5. First sync. syncCommand sets `process.exitCode = 1` on download
|
|
67
|
+
// failures without throwing — snapshot around the call so we don't
|
|
68
|
+
// print a misleading green "you're set up" over a half-failed run.
|
|
53
69
|
console.log();
|
|
54
70
|
console.log(kleur.bold('Step: first sync'));
|
|
71
|
+
const exitBefore = process.exitCode ?? 0;
|
|
55
72
|
await syncCommand({ full: true });
|
|
56
|
-
|
|
73
|
+
const firstSyncFailed = (process.exitCode ?? 0) > exitBefore;
|
|
74
|
+
// 6. Tell the user how to run the watch loop. If `init` couldn't patch
|
|
75
|
+
// package.json (non-standard layout, write-protected, etc.) the
|
|
76
|
+
// `magicpixel:watch` npm script won't exist — fall back to the
|
|
77
|
+
// `npx` form so we never instruct users to run a script they don't have.
|
|
57
78
|
console.log();
|
|
58
|
-
|
|
79
|
+
if (firstSyncFailed) {
|
|
80
|
+
console.log(kleur.yellow('! first sync completed with errors.'));
|
|
81
|
+
console.log(kleur.dim(' Re-run `npx magicpixel sync` to retry the failed downloads, or `npx magicpixel doctor` to diagnose.'));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log(kleur.bold('You\'re set up. ✨'));
|
|
85
|
+
}
|
|
59
86
|
console.log();
|
|
60
|
-
|
|
87
|
+
const hasWatch = await hasWatchScript();
|
|
88
|
+
if (hasWatch) {
|
|
89
|
+
console.log(` ${kleur.green('▶')} ${kleur.bold('npm run magicpixel:watch')} ${kleur.dim('# keeps sprites fresh while you edit them in MagicPixel')}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(` ${kleur.green('▶')} ${kleur.bold('npx magicpixel sync --watch')} ${kleur.dim('# keeps sprites fresh while you edit them in MagicPixel')}`);
|
|
93
|
+
}
|
|
61
94
|
console.log();
|
|
62
95
|
if (await hasDevScript()) {
|
|
96
|
+
const watchCmd = hasWatch ? 'npm run magicpixel:watch' : 'npx magicpixel sync --watch';
|
|
63
97
|
console.log(kleur.dim(' Tip: run your dev server and the watcher together with'));
|
|
64
|
-
console.log(kleur.dim(
|
|
98
|
+
console.log(kleur.dim(` \`npx concurrently "npm run dev" "${watchCmd}"\``));
|
|
65
99
|
console.log();
|
|
66
100
|
}
|
|
67
101
|
}
|
|
68
|
-
async function
|
|
102
|
+
async function readPkgJson() {
|
|
69
103
|
try {
|
|
70
104
|
const path = resolve(process.cwd(), 'package.json');
|
|
71
105
|
if (!existsSync(path))
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
const pkg = JSON.parse(await readFile(path, 'utf8'));
|
|
75
|
-
return typeof pkg.scripts?.dev === 'string';
|
|
106
|
+
return null;
|
|
107
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
76
108
|
}
|
|
77
109
|
catch {
|
|
78
|
-
return
|
|
110
|
+
return null;
|
|
79
111
|
}
|
|
80
112
|
}
|
|
113
|
+
async function hasWatchScript() {
|
|
114
|
+
const pkg = await readPkgJson();
|
|
115
|
+
const scripts = pkg?.scripts;
|
|
116
|
+
return !!scripts && typeof scripts === 'object' && typeof scripts['magicpixel:watch'] === 'string';
|
|
117
|
+
}
|
|
118
|
+
async function hasDevScript() {
|
|
119
|
+
const pkg = await readPkgJson();
|
|
120
|
+
const scripts = pkg?.scripts;
|
|
121
|
+
return !!scripts && typeof scripts === 'object' && typeof scripts.dev === 'string';
|
|
122
|
+
}
|
|
81
123
|
async function maybeMigrateDotenvKey() {
|
|
82
124
|
if (process.env.MAGICPIXEL_API_KEY)
|
|
83
125
|
return;
|
package/dist/commands/status.js
CHANGED
|
@@ -3,33 +3,61 @@ import { loadConfig, loadState, resolveEndpoint } from '../config.js';
|
|
|
3
3
|
import { fetchAllManifest } from '../api.js';
|
|
4
4
|
import { fileSha256 } from '../util/hash.js';
|
|
5
5
|
import { assetDiskPath } from '../util/paths.js';
|
|
6
|
+
import { readKeyForDisplay } from '../util/credentials.js';
|
|
7
|
+
import { createLimit } from '../util/limit.js';
|
|
6
8
|
export async function statusCommand() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
// Config is optional — a brand-new user can `magicpixel status` to inspect
|
|
10
|
+
// their key/env before bothering with init. Mirrors `whoami` / `login`.
|
|
11
|
+
let config = null;
|
|
12
|
+
let configErr = null;
|
|
13
|
+
try {
|
|
14
|
+
config = await loadConfig();
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
configErr = e.message.split('\n')[0];
|
|
18
|
+
}
|
|
9
19
|
console.log(kleur.bold('Config'));
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
if (config) {
|
|
21
|
+
console.log(` outDir: ${config.outDir}`);
|
|
22
|
+
console.log(` include: ${config.include.join(', ') || '(none)'}`);
|
|
23
|
+
console.log(` exclude: ${config.exclude.join(', ') || '(none)'}`);
|
|
24
|
+
console.log(` endpoint: ${resolveEndpoint(config)}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(kleur.yellow(` magicpixel.json: ${configErr ?? 'missing'}`));
|
|
28
|
+
console.log(kleur.dim(` Run \`npx magicpixel init\` to create one.`));
|
|
29
|
+
}
|
|
14
30
|
console.log();
|
|
31
|
+
const state = await loadState();
|
|
15
32
|
console.log(kleur.bold('State'));
|
|
16
33
|
console.log(` lastSync: ${state.lastSync ?? kleur.dim('never')}`);
|
|
34
|
+
if (state.lastError)
|
|
35
|
+
console.log(` lastError: ${kleur.yellow(state.lastError)}`);
|
|
17
36
|
console.log();
|
|
18
37
|
console.log(kleur.bold('Auth'));
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
// Honors the same precedence as `getApiKey()` (env wins, then file written
|
|
39
|
+
// by `magicpixel login`). The pre-Batch-B implementation only looked at the
|
|
40
|
+
// env var and falsely reported "key not set" for logged-in users.
|
|
41
|
+
const stored = readKeyForDisplay();
|
|
42
|
+
if (!stored) {
|
|
43
|
+
console.log(kleur.red(' no API key — run `magicpixel login` or set MAGICPIXEL_API_KEY'));
|
|
22
44
|
return;
|
|
23
45
|
}
|
|
24
|
-
|
|
46
|
+
const sourceLabel = stored.source === 'env' ? 'env (MAGICPIXEL_API_KEY)' : 'file (.magicpixel/credentials)';
|
|
47
|
+
console.log(` key: ${kleur.green('set')} (${maskKey(stored.value)}) ${kleur.dim(`— ${sourceLabel}`)}`);
|
|
25
48
|
console.log();
|
|
49
|
+
if (!config)
|
|
50
|
+
return; // No config, no diff to compute.
|
|
26
51
|
console.log(kleur.bold('Diff vs remote'));
|
|
27
52
|
try {
|
|
28
53
|
const manifest = await fetchAllManifest(config);
|
|
54
|
+
// Parallelize the SHA pre-check through the same concurrency pool sync
|
|
55
|
+
// uses — on a 1k-asset project a serial loop dominates wall-clock time.
|
|
56
|
+
const limit = createLimit(6);
|
|
29
57
|
let changed = 0;
|
|
30
58
|
let unchanged = 0;
|
|
31
59
|
let missing = 0;
|
|
32
|
-
|
|
60
|
+
await Promise.all(manifest.map((entry) => limit(async () => {
|
|
33
61
|
const local = await fileSha256(assetDiskPath(config.outDir, entry));
|
|
34
62
|
if (!local)
|
|
35
63
|
missing++;
|
|
@@ -37,14 +65,14 @@ export async function statusCommand() {
|
|
|
37
65
|
unchanged++;
|
|
38
66
|
else
|
|
39
67
|
changed++;
|
|
40
|
-
}
|
|
68
|
+
})));
|
|
41
69
|
console.log(` remote assets: ${manifest.length}`);
|
|
42
70
|
console.log(` ${kleur.dim('=')} unchanged: ${unchanged}`);
|
|
43
71
|
console.log(` ${kleur.yellow('~')} changed: ${changed}`);
|
|
44
72
|
console.log(` ${kleur.green('+')} missing: ${missing}`);
|
|
45
73
|
}
|
|
46
74
|
catch (e) {
|
|
47
|
-
console.log(kleur.red(` manifest fetch failed: ${e.message}`));
|
|
75
|
+
console.log(kleur.red(` manifest fetch failed: ${e.message.split('\n')[0]}`));
|
|
48
76
|
}
|
|
49
77
|
}
|
|
50
78
|
function maskKey(k) {
|