@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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire-and-forget CLI error reporter. POSTs to the `log-cli-error` edge
|
|
3
|
+
* function so unexpected failures show up on `/admin/errors` alongside
|
|
4
|
+
* browser crashes.
|
|
5
|
+
*
|
|
6
|
+
* Design contract (matches `src/utils/errorReporter.ts` semantics):
|
|
7
|
+
* - Never throws. Never blocks. 2s hard timeout via AbortController.
|
|
8
|
+
* - Skipped when no API key, when env opt-out is set, or when the endpoint
|
|
9
|
+
* is not the canonical magicpixel host (avoids spamming during local
|
|
10
|
+
* edge-function dev).
|
|
11
|
+
* - Per-category 5s throttle + per-message 60s LRU dedupe (capacity 50).
|
|
12
|
+
* - Reports a single `category='cli_error'`; meaningful taxonomy lives in
|
|
13
|
+
* `context.command`.
|
|
14
|
+
*/
|
|
15
|
+
import { resolveEndpoint, loadConfig } from '../config.js';
|
|
16
|
+
import { readCredentialsSync } from './credentials.js';
|
|
17
|
+
import { CLI_VERSION, CLI_USER_AGENT } from '../version.js';
|
|
18
|
+
import { ApiError } from '../api.js';
|
|
19
|
+
const THROTTLE_MS = 5_000;
|
|
20
|
+
const DEDUPE_MS = 60_000;
|
|
21
|
+
const DEDUPE_LRU_MAX = 50;
|
|
22
|
+
const REPORT_TIMEOUT_MS = 2_000;
|
|
23
|
+
let lastReportedAt = 0;
|
|
24
|
+
const dedupeMap = new Map(); // insertion order = LRU order
|
|
25
|
+
function dedupeKey(message) {
|
|
26
|
+
return message.slice(0, 80);
|
|
27
|
+
}
|
|
28
|
+
function isCanonicalEndpoint(endpoint) {
|
|
29
|
+
try {
|
|
30
|
+
const u = new URL(endpoint);
|
|
31
|
+
// Only report when hitting the production Supabase edge host. Local
|
|
32
|
+
// `supabase functions serve` and forks fall through silently.
|
|
33
|
+
return u.hostname === 'sddsilidjhvtvejzvolx.supabase.co';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Decide whether an error is worth reporting. Pure — exported for tests. */
|
|
40
|
+
export function shouldReportCliError(err) {
|
|
41
|
+
if (!(err instanceof Error))
|
|
42
|
+
return false;
|
|
43
|
+
if (err instanceof ApiError) {
|
|
44
|
+
// User/config issues have actionable fix hints already; don't add noise.
|
|
45
|
+
return err.status >= 500;
|
|
46
|
+
}
|
|
47
|
+
const msg = err.message ?? '';
|
|
48
|
+
// Commander argument validation — user typed something wrong.
|
|
49
|
+
if (err.name === 'InvalidArgumentError' || err.name === 'CommanderError')
|
|
50
|
+
return false;
|
|
51
|
+
// Known user-fixable surfaces (config missing, perms, not logged in, key
|
|
52
|
+
// shape) — these come out of `loadConfig`/`getApiKey` with friendly "Fix:"
|
|
53
|
+
// hints; logging them just floods /admin/errors with self-inflicted noise.
|
|
54
|
+
if (/No magicpixel\.json found|No MagicPixel API key found|API key does not look right/.test(msg))
|
|
55
|
+
return false;
|
|
56
|
+
if (/\b(ENOENT|EACCES|EPERM|EISDIR|ENOTDIR)\b/.test(msg))
|
|
57
|
+
return false;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve API key without touching `getApiKey()` (which throws when missing).
|
|
62
|
+
* Mirrors its precedence: env first, then credentials file.
|
|
63
|
+
*/
|
|
64
|
+
function readApiKeySilently() {
|
|
65
|
+
const env = process.env.MAGICPIXEL_API_KEY?.trim();
|
|
66
|
+
if (env)
|
|
67
|
+
return env;
|
|
68
|
+
const stored = readCredentialsSync();
|
|
69
|
+
return stored?.apiKey ?? null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Report an unexpected CLI error. Safe to call from any catch — silently
|
|
73
|
+
* skips when telemetry can't or shouldn't run. Returns a promise only so
|
|
74
|
+
* tests can await; production callers may ignore it.
|
|
75
|
+
*/
|
|
76
|
+
export async function reportCliError(err, context, config) {
|
|
77
|
+
try {
|
|
78
|
+
if (process.env.MAGICPIXEL_TELEMETRY === '0')
|
|
79
|
+
return;
|
|
80
|
+
if (!shouldReportCliError(err))
|
|
81
|
+
return;
|
|
82
|
+
const apiKey = readApiKeySilently();
|
|
83
|
+
if (!apiKey)
|
|
84
|
+
return;
|
|
85
|
+
// Decide the report URL:
|
|
86
|
+
// - No config OR canonical endpoint → POST to canonical log-cli-error.
|
|
87
|
+
// - Config explicitly points at a non-canonical host → user is on a
|
|
88
|
+
// fork / proxy / local edge stack; don't silently leak errors back
|
|
89
|
+
// to magicpixel.art.
|
|
90
|
+
let reportUrl;
|
|
91
|
+
if (!config) {
|
|
92
|
+
reportUrl = 'https://sddsilidjhvtvejzvolx.supabase.co/functions/v1/log-cli-error';
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const endpoint = resolveEndpoint(config);
|
|
96
|
+
if (!isCanonicalEndpoint(endpoint))
|
|
97
|
+
return;
|
|
98
|
+
reportUrl = endpoint.replace(/\/integration-assets\/?$/, '/log-cli-error');
|
|
99
|
+
}
|
|
100
|
+
if (!/\/log-cli-error$/.test(reportUrl))
|
|
101
|
+
return;
|
|
102
|
+
const message = err.message ?? String(err);
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
// Per-message dedupe.
|
|
105
|
+
const key = dedupeKey(message);
|
|
106
|
+
const lastSeen = dedupeMap.get(key);
|
|
107
|
+
if (lastSeen !== undefined && now - lastSeen < DEDUPE_MS) {
|
|
108
|
+
dedupeMap.delete(key);
|
|
109
|
+
dedupeMap.set(key, lastSeen);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Per-category throttle (single category, so global).
|
|
113
|
+
if (now - lastReportedAt < THROTTLE_MS)
|
|
114
|
+
return;
|
|
115
|
+
lastReportedAt = now;
|
|
116
|
+
dedupeMap.set(key, now);
|
|
117
|
+
if (dedupeMap.size > DEDUPE_LRU_MAX) {
|
|
118
|
+
const oldest = dedupeMap.keys().next().value;
|
|
119
|
+
if (oldest !== undefined)
|
|
120
|
+
dedupeMap.delete(oldest);
|
|
121
|
+
}
|
|
122
|
+
const apiErr = err instanceof ApiError ? err : null;
|
|
123
|
+
const payload = {
|
|
124
|
+
category: 'cli_error',
|
|
125
|
+
message: message.slice(0, 500),
|
|
126
|
+
context: {
|
|
127
|
+
command: context.command,
|
|
128
|
+
cli_version: CLI_VERSION,
|
|
129
|
+
node_version: process.version,
|
|
130
|
+
platform: process.platform,
|
|
131
|
+
request_id: context.request_id ?? apiErr?.requestId,
|
|
132
|
+
status: context.status ?? apiErr?.status,
|
|
133
|
+
stack: (context.stack ?? err.stack)?.slice(0, 2000),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
const ctrl = new AbortController();
|
|
137
|
+
const timer = setTimeout(() => ctrl.abort(), REPORT_TIMEOUT_MS);
|
|
138
|
+
try {
|
|
139
|
+
await fetch(reportUrl, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${apiKey}`,
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
'User-Agent': CLI_USER_AGENT,
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify(payload),
|
|
147
|
+
signal: ctrl.signal,
|
|
148
|
+
}).catch(() => { });
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Telemetry must never affect the user-visible failure.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Shared "report this error then exit" helper. Used by:
|
|
160
|
+
* - `wrap()` in `src/index.ts` (top-level command failure, exit 1)
|
|
161
|
+
* - the watch-loop give-up branch in `sync.ts` (persistent auth fail, exit 2)
|
|
162
|
+
*
|
|
163
|
+
* Loads config best-effort so the fork-endpoint guard in `reportCliError`
|
|
164
|
+
* can keep non-canonical deployments off the canonical /admin/errors stream.
|
|
165
|
+
* Awaits the report so it flushes before process exit. Never throws —
|
|
166
|
+
* telemetry must not change the user-visible failure mode.
|
|
167
|
+
*/
|
|
168
|
+
export async function reportAndExit(err, command, exitCode) {
|
|
169
|
+
try {
|
|
170
|
+
const config = await loadConfig().catch(() => undefined);
|
|
171
|
+
await reportCliError(err, { command }, config);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// never block exit
|
|
175
|
+
}
|
|
176
|
+
process.exit(exitCode);
|
|
177
|
+
}
|
|
178
|
+
/** Test-only: reset module-singleton state between vitest runs. */
|
|
179
|
+
export function __resetTelemetryForTesting() {
|
|
180
|
+
lastReportedAt = 0;
|
|
181
|
+
dedupeMap.clear();
|
|
182
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magicpixelart/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Sync MagicPixel pixel-art assets to your local project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"magicpixel": "dist/index.js"
|
|
8
8
|
},
|
|
9
|
-
"files": [
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"CHANGELOG.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
10
15
|
"scripts": {
|
|
11
|
-
"build": "tsc &&
|
|
16
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
12
17
|
"dev": "tsx src/index.ts",
|
|
18
|
+
"test": "vitest run",
|
|
13
19
|
"prepublishOnly": "npm run build"
|
|
14
20
|
},
|
|
15
21
|
"keywords": [
|
|
@@ -45,6 +51,7 @@
|
|
|
45
51
|
"devDependencies": {
|
|
46
52
|
"@types/node": "^20.14.0",
|
|
47
53
|
"tsx": "^4.19.0",
|
|
48
|
-
"typescript": "^5.5.0"
|
|
54
|
+
"typescript": "^5.5.0",
|
|
55
|
+
"vitest": "^4.1.8"
|
|
49
56
|
}
|
|
50
57
|
}
|