snapfail 0.0.24 → 0.0.26
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/dist/index.js +767 -410
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -34,8 +34,8 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
|
|
|
34
34
|
|
|
35
35
|
// ../../node_modules/.bun/sisteransi@1.0.5/node_modules/sisteransi/src/index.js
|
|
36
36
|
var require_src = __commonJS((exports, module) => {
|
|
37
|
-
var
|
|
38
|
-
var CSI2 = `${
|
|
37
|
+
var ESC2 = "\x1B";
|
|
38
|
+
var CSI2 = `${ESC2}[`;
|
|
39
39
|
var beep = "\x07";
|
|
40
40
|
var cursor = {
|
|
41
41
|
to(x, y) {
|
|
@@ -64,8 +64,8 @@ var require_src = __commonJS((exports, module) => {
|
|
|
64
64
|
left: `${CSI2}G`,
|
|
65
65
|
hide: `${CSI2}?25l`,
|
|
66
66
|
show: `${CSI2}?25h`,
|
|
67
|
-
save: `${
|
|
68
|
-
restore: `${
|
|
67
|
+
save: `${ESC2}7`,
|
|
68
|
+
restore: `${ESC2}8`
|
|
69
69
|
};
|
|
70
70
|
var scroll = {
|
|
71
71
|
up: (count = 1) => `${CSI2}S`.repeat(count),
|
|
@@ -93,362 +93,6 @@ var require_src = __commonJS((exports, module) => {
|
|
|
93
93
|
// src/config.ts
|
|
94
94
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
95
95
|
import { resolve } from "path";
|
|
96
|
-
var DEFAULT_ENDPOINT = "https://app.snapfail.com";
|
|
97
|
-
var ENV_FILE = ".env";
|
|
98
|
-
function loadConfig(cwd = process.cwd(), pk) {
|
|
99
|
-
if (pk)
|
|
100
|
-
return { projectKey: pk, endpoint: DEFAULT_ENDPOINT };
|
|
101
|
-
const fromEnv = process.env["SNAPFAIL_PROJECT_KEY"];
|
|
102
|
-
if (fromEnv)
|
|
103
|
-
return { projectKey: fromEnv, endpoint: DEFAULT_ENDPOINT };
|
|
104
|
-
const envPath = resolve(cwd, ENV_FILE);
|
|
105
|
-
if (existsSync(envPath)) {
|
|
106
|
-
const match = readFileSync(envPath, "utf-8").match(/^SNAPFAIL_PROJECT_KEY=(.+)$/m);
|
|
107
|
-
if (match)
|
|
108
|
-
return { projectKey: match[1].trim(), endpoint: DEFAULT_ENDPOINT };
|
|
109
|
-
}
|
|
110
|
-
throw new Error('No project key found. Run "snapfail init" or set SNAPFAIL_PROJECT_KEY.');
|
|
111
|
-
}
|
|
112
|
-
function writeProjectKey(projectKey, cwd = process.cwd(), keyName = "SNAPFAIL_PROJECT_KEY") {
|
|
113
|
-
const envPath = resolve(cwd, ENV_FILE);
|
|
114
|
-
const line = `${keyName}=${projectKey}`;
|
|
115
|
-
const pattern = new RegExp(`^${keyName}=.*`, "m");
|
|
116
|
-
if (existsSync(envPath)) {
|
|
117
|
-
let content = readFileSync(envPath, "utf-8");
|
|
118
|
-
if (pattern.test(content)) {
|
|
119
|
-
content = content.replace(pattern, line);
|
|
120
|
-
} else {
|
|
121
|
-
content = content.trimEnd() + `
|
|
122
|
-
${line}
|
|
123
|
-
`;
|
|
124
|
-
}
|
|
125
|
-
writeFileSync(envPath, content, "utf-8");
|
|
126
|
-
} else {
|
|
127
|
-
writeFileSync(envPath, `${line}
|
|
128
|
-
`, "utf-8");
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// src/session.ts
|
|
133
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2, rmSync } from "fs";
|
|
134
|
-
import { join } from "path";
|
|
135
|
-
import { homedir } from "os";
|
|
136
|
-
var SESSION_DIR = join(homedir(), ".snapfail");
|
|
137
|
-
var SESSION_FILE = join(SESSION_DIR, "auth.json");
|
|
138
|
-
function readSession() {
|
|
139
|
-
try {
|
|
140
|
-
if (!existsSync2(SESSION_FILE))
|
|
141
|
-
return null;
|
|
142
|
-
const raw = JSON.parse(readFileSync2(SESSION_FILE, "utf-8"));
|
|
143
|
-
if (!raw.token || !raw.endpoint)
|
|
144
|
-
return null;
|
|
145
|
-
return raw;
|
|
146
|
-
} catch {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
function writeSession(session) {
|
|
151
|
-
mkdirSync(SESSION_DIR, { recursive: true });
|
|
152
|
-
writeFileSync2(SESSION_FILE, JSON.stringify(session, null, 2) + `
|
|
153
|
-
`, "utf-8");
|
|
154
|
-
}
|
|
155
|
-
function requireSession() {
|
|
156
|
-
const session = readSession();
|
|
157
|
-
if (!session) {
|
|
158
|
-
throw new Error('No active session. Run "snapfail init" to authenticate.');
|
|
159
|
-
}
|
|
160
|
-
return session;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// src/api.ts
|
|
164
|
-
async function apiFetch(url, options) {
|
|
165
|
-
let res;
|
|
166
|
-
try {
|
|
167
|
-
res = await fetch(url, options);
|
|
168
|
-
} catch (err) {
|
|
169
|
-
throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
170
|
-
}
|
|
171
|
-
if (res.status === 401)
|
|
172
|
-
throw new Error("Unauthorized: check your project key.");
|
|
173
|
-
if (res.status === 404)
|
|
174
|
-
return null;
|
|
175
|
-
if (!res.ok) {
|
|
176
|
-
const body = await res.text().catch(() => "");
|
|
177
|
-
throw new Error(`API error ${res.status}: ${body}`);
|
|
178
|
-
}
|
|
179
|
-
return res.json();
|
|
180
|
-
}
|
|
181
|
-
async function fetchIncidents(config, opts = {}) {
|
|
182
|
-
const url = new URL(`${config.endpoint}/api/incidents`);
|
|
183
|
-
url.searchParams.set("projectKey", config.projectKey);
|
|
184
|
-
if (opts.status)
|
|
185
|
-
url.searchParams.set("status", opts.status);
|
|
186
|
-
if (opts.limit != null)
|
|
187
|
-
url.searchParams.set("limit", String(opts.limit));
|
|
188
|
-
if (opts.offset != null)
|
|
189
|
-
url.searchParams.set("offset", String(opts.offset));
|
|
190
|
-
const data = await apiFetch(url.toString());
|
|
191
|
-
return data;
|
|
192
|
-
}
|
|
193
|
-
async function fetchIncident(config, id) {
|
|
194
|
-
const url = `${config.endpoint}/api/incidents/${id}`;
|
|
195
|
-
const data = await apiFetch(url, {
|
|
196
|
-
headers: { "X-Project-Key": config.projectKey }
|
|
197
|
-
});
|
|
198
|
-
return data;
|
|
199
|
-
}
|
|
200
|
-
async function deleteGroup(config, id) {
|
|
201
|
-
const url = `${config.endpoint}/api/incidents/${id}`;
|
|
202
|
-
const data = await apiFetch(url, {
|
|
203
|
-
method: "DELETE",
|
|
204
|
-
headers: { "X-Project-Key": config.projectKey }
|
|
205
|
-
});
|
|
206
|
-
return data !== null;
|
|
207
|
-
}
|
|
208
|
-
async function fetchSample(config, groupId, sampleIndex) {
|
|
209
|
-
const url = `${config.endpoint}/api/incidents/${groupId}/samples/${sampleIndex}`;
|
|
210
|
-
const data = await apiFetch(url, {
|
|
211
|
-
headers: { "X-Project-Key": config.projectKey }
|
|
212
|
-
});
|
|
213
|
-
return data;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// src/format.ts
|
|
217
|
-
var ESC = "\x1B[";
|
|
218
|
-
var bold = (s) => `${ESC}1m${s}${ESC}0m`;
|
|
219
|
-
var dim = (s) => `${ESC}2m${s}${ESC}0m`;
|
|
220
|
-
var red = (s) => `${ESC}31m${s}${ESC}0m`;
|
|
221
|
-
var green = (s) => `${ESC}32m${s}${ESC}0m`;
|
|
222
|
-
var yellow = (s) => `${ESC}33m${s}${ESC}0m`;
|
|
223
|
-
var cyan = (s) => `${ESC}36m${s}${ESC}0m`;
|
|
224
|
-
function truncate(s, n) {
|
|
225
|
-
if (s.length <= n)
|
|
226
|
-
return s;
|
|
227
|
-
return s.slice(0, n - 1) + "\u2026";
|
|
228
|
-
}
|
|
229
|
-
function relativeTime(ts) {
|
|
230
|
-
const diff = Date.now() - ts;
|
|
231
|
-
const s = Math.floor(diff / 1000);
|
|
232
|
-
if (s < 60)
|
|
233
|
-
return `${s}s ago`;
|
|
234
|
-
const m = Math.floor(s / 60);
|
|
235
|
-
if (m < 60)
|
|
236
|
-
return `${m}m ago`;
|
|
237
|
-
const h = Math.floor(m / 60);
|
|
238
|
-
if (h < 24)
|
|
239
|
-
return `${h}h ago`;
|
|
240
|
-
const d = Math.floor(h / 24);
|
|
241
|
-
return `${d}d ago`;
|
|
242
|
-
}
|
|
243
|
-
function formatTable(rows, headers) {
|
|
244
|
-
const allRows = [headers, ...rows];
|
|
245
|
-
const widths = headers.map((_, i) => Math.max(...allRows.map((r) => (r[i] ?? "").length)));
|
|
246
|
-
const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
|
|
247
|
-
const header = " " + headers.map((h, i) => bold(pad(h, widths[i]))).join(" ");
|
|
248
|
-
const divider = " " + widths.map((w) => dim("\u2500".repeat(w))).join(" ");
|
|
249
|
-
const body = rows.map((r) => " " + r.map((cell, i) => pad(cell, widths[i])).join(" ")).join(`
|
|
250
|
-
`);
|
|
251
|
-
return [header, divider, body].join(`
|
|
252
|
-
`);
|
|
253
|
-
}
|
|
254
|
-
function formatIncidentList(data, total, status) {
|
|
255
|
-
if (data.length === 0) {
|
|
256
|
-
return dim(`No ${status ?? "unresolved"} incidents found.`);
|
|
257
|
-
}
|
|
258
|
-
const rows = data.map((g) => [
|
|
259
|
-
cyan(truncate(g.id, 12)),
|
|
260
|
-
truncate(g.title, 38),
|
|
261
|
-
String(g.count),
|
|
262
|
-
g.environments.join(","),
|
|
263
|
-
relativeTime(g.lastSeen)
|
|
264
|
-
]);
|
|
265
|
-
const table = formatTable(rows, ["ID", "TITLE", "COUNT", "ENV", "LAST SEEN"]);
|
|
266
|
-
const summary = `
|
|
267
|
-
${bold(String(total))} ${status ?? "unresolved"} incident${total !== 1 ? "s" : ""}`;
|
|
268
|
-
return table + summary;
|
|
269
|
-
}
|
|
270
|
-
function formatLLMContext(group, samples) {
|
|
271
|
-
const lines = [];
|
|
272
|
-
lines.push(`# Incident: ${group.title}`);
|
|
273
|
-
lines.push(`Group ID: ${group.id}`);
|
|
274
|
-
lines.push(`Fingerprint: ${group.fingerprint}`);
|
|
275
|
-
lines.push(`Occurrences: ${group.count} | First: ${new Date(group.firstSeen).toISOString()} | Last: ${new Date(group.lastSeen).toISOString()}`);
|
|
276
|
-
lines.push(`Environments: ${group.environments.join(", ")}`);
|
|
277
|
-
lines.push(`Status: ${group.status}`);
|
|
278
|
-
lines.push("");
|
|
279
|
-
lines.push("## Error");
|
|
280
|
-
lines.push(`Type: ${group.errorType}`);
|
|
281
|
-
lines.push(`Message: ${samples[0]?.errorMessage ?? group.title}`);
|
|
282
|
-
lines.push(`Normalized: ${samples[0]?.normalizedMessage ?? group.title}`);
|
|
283
|
-
if (samples[0] && samples[0].stackFrames.length > 0) {
|
|
284
|
-
lines.push("");
|
|
285
|
-
lines.push("## Stack");
|
|
286
|
-
for (const f of samples[0].stackFrames) {
|
|
287
|
-
lines.push(` ${f.fn ?? "(anonymous)"} ${f.file}:${f.line}${f.col != null ? `:${f.col}` : ""}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
for (let i = 0;i < samples.length; i++) {
|
|
291
|
-
const s = samples[i];
|
|
292
|
-
lines.push("");
|
|
293
|
-
lines.push(`## Sample ${i + 1} / ${group.sampleIds.length}`);
|
|
294
|
-
lines.push(`Device: ${s.device.userAgent}`);
|
|
295
|
-
if (s.device.viewport) {
|
|
296
|
-
lines.push(`Viewport: ${s.device.viewport.width}x${s.device.viewport.height} Language: ${s.device.language ?? "unknown"}`);
|
|
297
|
-
}
|
|
298
|
-
lines.push(`URL: ${s.url}`);
|
|
299
|
-
if (s.route)
|
|
300
|
-
lines.push(`Route: ${s.route}`);
|
|
301
|
-
lines.push(`Environment: ${s.environmentMode}`);
|
|
302
|
-
if (s.consoleEntries.length > 0) {
|
|
303
|
-
lines.push("");
|
|
304
|
-
lines.push("### Console");
|
|
305
|
-
for (const e of s.consoleEntries) {
|
|
306
|
-
const args = e.args.map((a) => String(a)).join(" ");
|
|
307
|
-
lines.push(` [${e.level}] ${args}`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (s.networkEntries.length > 0) {
|
|
311
|
-
lines.push("");
|
|
312
|
-
lines.push("### Network");
|
|
313
|
-
for (const n of s.networkEntries) {
|
|
314
|
-
const status = n.status ? ` \u2192 ${n.status}` : n.error ? ` \u2192 ERROR` : "";
|
|
315
|
-
lines.push(` ${n.method} ${n.url}${status} (${n.durationMs}ms)`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
if (s.timeline.length > 0) {
|
|
319
|
-
lines.push("");
|
|
320
|
-
lines.push("### Timeline");
|
|
321
|
-
for (const e of s.timeline) {
|
|
322
|
-
lines.push(` +${e.t}ms ${e.kind}: ${e.summary}`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
if (i < samples.length - 1)
|
|
326
|
-
lines.push(`
|
|
327
|
-
---`);
|
|
328
|
-
}
|
|
329
|
-
return lines.join(`
|
|
330
|
-
`);
|
|
331
|
-
}
|
|
332
|
-
function formatIncidentDetail(group, sample) {
|
|
333
|
-
const lines = [];
|
|
334
|
-
lines.push(bold(`${group.errorType}: ${truncate(group.title, 80)}`));
|
|
335
|
-
lines.push(`${dim("Status:")} ${group.status} ${dim("\xB7")} ${group.count} occurrences ${dim("\xB7")} ${group.environments.join(", ")}`);
|
|
336
|
-
lines.push(`${dim("First seen:")} ${new Date(group.firstSeen).toISOString().slice(0, 10)} ${dim("\xB7")} ${dim("Last seen:")} ${relativeTime(group.lastSeen)}`);
|
|
337
|
-
if (sample.stackFrames.length > 0) {
|
|
338
|
-
lines.push("");
|
|
339
|
-
lines.push(bold("Stack"));
|
|
340
|
-
for (const f of sample.stackFrames.slice(0, 6)) {
|
|
341
|
-
const fn = f.fn ? `${cyan(f.fn)} ` : "";
|
|
342
|
-
lines.push(` ${fn}${dim(f.file + ":" + f.line)}`);
|
|
343
|
-
if (f.codeSnippet) {
|
|
344
|
-
for (const snippetLine of f.codeSnippet.split(`
|
|
345
|
-
`)) {
|
|
346
|
-
const isError = snippetLine.startsWith("\u25BA");
|
|
347
|
-
lines.push(isError ? ` ${bold(snippetLine)}` : ` ${dim(snippetLine)}`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (sample.consoleEntries.length > 0) {
|
|
353
|
-
lines.push("");
|
|
354
|
-
lines.push(bold("Console"));
|
|
355
|
-
for (const e of sample.consoleEntries.slice(-5)) {
|
|
356
|
-
const level = e.level === "error" ? red(`[${e.level}]`) : yellow(`[${e.level}]`);
|
|
357
|
-
const args = e.args.map((a) => truncate(String(a), 60)).join(" ");
|
|
358
|
-
lines.push(` ${level} ${args}`);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (sample.networkEntries.length > 0) {
|
|
362
|
-
lines.push("");
|
|
363
|
-
lines.push(bold("Network"));
|
|
364
|
-
for (const n of sample.networkEntries.slice(-5)) {
|
|
365
|
-
const status = n.status ? n.status >= 400 ? red(String(n.status)) : green(String(n.status)) : dim("???");
|
|
366
|
-
lines.push(` ${dim(n.method)} ${truncate(n.url, 50)} ${dim("\u2192")} ${status} ${dim(`(${n.durationMs}ms)`)}`);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (sample.timeline.length > 0) {
|
|
370
|
-
lines.push("");
|
|
371
|
-
lines.push(bold("Timeline"));
|
|
372
|
-
for (const e of sample.timeline.slice(0, 10)) {
|
|
373
|
-
const t = String(e.t).padStart(5);
|
|
374
|
-
lines.push(` ${dim(t + "ms")} ${e.summary}`);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
return lines.join(`
|
|
378
|
-
`);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// src/commands/incidents.ts
|
|
382
|
-
async function runIncidents(opts) {
|
|
383
|
-
requireSession();
|
|
384
|
-
const config = loadConfig(process.cwd(), opts.pk);
|
|
385
|
-
const result = await fetchIncidents(config, {
|
|
386
|
-
status: opts.status ?? "unresolved",
|
|
387
|
-
limit: opts.limit,
|
|
388
|
-
offset: opts.offset
|
|
389
|
-
});
|
|
390
|
-
if (opts.json) {
|
|
391
|
-
process.stdout.write(JSON.stringify(result, null, 2) + `
|
|
392
|
-
`);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
console.log(formatIncidentList(result.data, result.total, opts.status));
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// src/commands/incident.ts
|
|
399
|
-
async function runIncident(opts) {
|
|
400
|
-
requireSession();
|
|
401
|
-
const config = loadConfig(process.cwd(), opts.pk);
|
|
402
|
-
if (opts.delete) {
|
|
403
|
-
const ok = await deleteGroup(config, opts.id);
|
|
404
|
-
if (!ok) {
|
|
405
|
-
console.error(`Incident group ${opts.id} not found.`);
|
|
406
|
-
process.exit(1);
|
|
407
|
-
}
|
|
408
|
-
if (opts.json) {
|
|
409
|
-
process.stdout.write(JSON.stringify({ ok: true, deleted: opts.id }) + `
|
|
410
|
-
`);
|
|
411
|
-
} else {
|
|
412
|
-
console.log(`Deleted incident group ${opts.id} and all related data.`);
|
|
413
|
-
}
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
if (opts.sample != null) {
|
|
417
|
-
const s = await fetchSample(config, opts.id, opts.sample);
|
|
418
|
-
if (!s) {
|
|
419
|
-
console.error(`Sample ${opts.sample} not found for incident ${opts.id}`);
|
|
420
|
-
process.exit(1);
|
|
421
|
-
}
|
|
422
|
-
if (opts.json) {
|
|
423
|
-
process.stdout.write(JSON.stringify(s, null, 2) + `
|
|
424
|
-
`);
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
const detail = await fetchIncident(config, opts.id);
|
|
428
|
-
if (!detail) {
|
|
429
|
-
console.error(`Incident ${opts.id} not found.`);
|
|
430
|
-
process.exit(1);
|
|
431
|
-
}
|
|
432
|
-
console.log(formatIncidentDetail(detail.group, s));
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const result = await fetchIncident(config, opts.id);
|
|
436
|
-
if (!result) {
|
|
437
|
-
console.error(`Incident ${opts.id} not found.`);
|
|
438
|
-
process.exit(1);
|
|
439
|
-
}
|
|
440
|
-
if (opts.json) {
|
|
441
|
-
process.stdout.write(JSON.stringify(result, null, 2) + `
|
|
442
|
-
`);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
console.log(formatIncidentDetail(result.group, result.sample));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// src/commands/init.ts
|
|
449
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
450
|
-
import { resolve as resolve2, join as join2 } from "path";
|
|
451
|
-
import { execSync as execSync2 } from "child_process";
|
|
452
96
|
|
|
453
97
|
// ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
|
|
454
98
|
import { styleText } from "util";
|
|
@@ -586,7 +230,7 @@ var fastStringWidth = (input, options = {}) => {
|
|
|
586
230
|
var dist_default2 = fastStringWidth;
|
|
587
231
|
|
|
588
232
|
// ../../node_modules/.bun/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js
|
|
589
|
-
var
|
|
233
|
+
var ESC = "\x1B";
|
|
590
234
|
var CSI = "\x9B";
|
|
591
235
|
var END_CODE = 39;
|
|
592
236
|
var ANSI_ESCAPE_BELL = "\x07";
|
|
@@ -620,8 +264,8 @@ var getClosingCode = (openingCode) => {
|
|
|
620
264
|
return 0;
|
|
621
265
|
return;
|
|
622
266
|
};
|
|
623
|
-
var wrapAnsiCode = (code) => `${
|
|
624
|
-
var wrapAnsiHyperlink = (url) => `${
|
|
267
|
+
var wrapAnsiCode = (code) => `${ESC}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
|
|
268
|
+
var wrapAnsiHyperlink = (url) => `${ESC}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
|
|
625
269
|
var wrapWord = (rows, word, columns) => {
|
|
626
270
|
const characters = word[Symbol.iterator]();
|
|
627
271
|
let isInsideEscape = false;
|
|
@@ -640,7 +284,7 @@ var wrapWord = (rows, word, columns) => {
|
|
|
640
284
|
rows.push(character);
|
|
641
285
|
visible = 0;
|
|
642
286
|
}
|
|
643
|
-
if (character ===
|
|
287
|
+
if (character === ESC || character === CSI) {
|
|
644
288
|
isInsideEscape = true;
|
|
645
289
|
isInsideLinkEscape = word.startsWith(ANSI_ESCAPE_LINK, rawCharacterIndex + 1);
|
|
646
290
|
}
|
|
@@ -759,7 +403,7 @@ var exec = (string, columns, options = {}) => {
|
|
|
759
403
|
} else {
|
|
760
404
|
inSurrogate = false;
|
|
761
405
|
}
|
|
762
|
-
if (character ===
|
|
406
|
+
if (character === ESC || character === CSI) {
|
|
763
407
|
GROUP_REGEX.lastIndex = i + 1;
|
|
764
408
|
const groupsResult = GROUP_REGEX.exec(preString);
|
|
765
409
|
const groups = groupsResult?.groups;
|
|
@@ -1895,47 +1539,511 @@ ${l2}
|
|
|
1895
1539
|
}
|
|
1896
1540
|
}
|
|
1897
1541
|
}
|
|
1898
|
-
}).prompt();
|
|
1899
|
-
};
|
|
1900
|
-
var i = `${styleText2("gray", S_BAR)} `;
|
|
1901
|
-
var text = (t2) => new n({
|
|
1902
|
-
validate: t2.validate,
|
|
1903
|
-
placeholder: t2.placeholder,
|
|
1904
|
-
defaultValue: t2.defaultValue,
|
|
1905
|
-
initialValue: t2.initialValue,
|
|
1906
|
-
output: t2.output,
|
|
1907
|
-
signal: t2.signal,
|
|
1908
|
-
input: t2.input,
|
|
1909
|
-
render() {
|
|
1910
|
-
const i2 = t2?.withGuide ?? settings.withGuide, s = `${`${i2 ? `${styleText2("gray", S_BAR)}
|
|
1911
|
-
` : ""}${symbol(this.state)} `}${t2.message}
|
|
1912
|
-
`, c3 = t2.placeholder ? styleText2("inverse", t2.placeholder[0]) + styleText2("dim", t2.placeholder.slice(1)) : styleText2(["inverse", "hidden"], "_"), o2 = this.userInput ? this.userInputWithCursor : c3, a2 = this.value ?? "";
|
|
1913
|
-
switch (this.state) {
|
|
1914
|
-
case "error": {
|
|
1915
|
-
const n2 = this.error ? ` ${styleText2("yellow", this.error)}` : "", r2 = i2 ? `${styleText2("yellow", S_BAR)} ` : "", d = i2 ? styleText2("yellow", S_BAR_END) : "";
|
|
1916
|
-
return `${s.trim()}
|
|
1917
|
-
${r2}${o2}
|
|
1918
|
-
${d}${n2}
|
|
1919
|
-
`;
|
|
1920
|
-
}
|
|
1921
|
-
case "submit": {
|
|
1922
|
-
const n2 = a2 ? ` ${styleText2("dim", a2)}` : "", r2 = i2 ? styleText2("gray", S_BAR) : "";
|
|
1923
|
-
return `${s}${r2}${n2}`;
|
|
1542
|
+
}).prompt();
|
|
1543
|
+
};
|
|
1544
|
+
var i = `${styleText2("gray", S_BAR)} `;
|
|
1545
|
+
var text = (t2) => new n({
|
|
1546
|
+
validate: t2.validate,
|
|
1547
|
+
placeholder: t2.placeholder,
|
|
1548
|
+
defaultValue: t2.defaultValue,
|
|
1549
|
+
initialValue: t2.initialValue,
|
|
1550
|
+
output: t2.output,
|
|
1551
|
+
signal: t2.signal,
|
|
1552
|
+
input: t2.input,
|
|
1553
|
+
render() {
|
|
1554
|
+
const i2 = t2?.withGuide ?? settings.withGuide, s = `${`${i2 ? `${styleText2("gray", S_BAR)}
|
|
1555
|
+
` : ""}${symbol(this.state)} `}${t2.message}
|
|
1556
|
+
`, c3 = t2.placeholder ? styleText2("inverse", t2.placeholder[0]) + styleText2("dim", t2.placeholder.slice(1)) : styleText2(["inverse", "hidden"], "_"), o2 = this.userInput ? this.userInputWithCursor : c3, a2 = this.value ?? "";
|
|
1557
|
+
switch (this.state) {
|
|
1558
|
+
case "error": {
|
|
1559
|
+
const n2 = this.error ? ` ${styleText2("yellow", this.error)}` : "", r2 = i2 ? `${styleText2("yellow", S_BAR)} ` : "", d = i2 ? styleText2("yellow", S_BAR_END) : "";
|
|
1560
|
+
return `${s.trim()}
|
|
1561
|
+
${r2}${o2}
|
|
1562
|
+
${d}${n2}
|
|
1563
|
+
`;
|
|
1564
|
+
}
|
|
1565
|
+
case "submit": {
|
|
1566
|
+
const n2 = a2 ? ` ${styleText2("dim", a2)}` : "", r2 = i2 ? styleText2("gray", S_BAR) : "";
|
|
1567
|
+
return `${s}${r2}${n2}`;
|
|
1568
|
+
}
|
|
1569
|
+
case "cancel": {
|
|
1570
|
+
const n2 = a2 ? ` ${styleText2(["strikethrough", "dim"], a2)}` : "", r2 = i2 ? styleText2("gray", S_BAR) : "";
|
|
1571
|
+
return `${s}${r2}${n2}${a2.trim() ? `
|
|
1572
|
+
${r2}` : ""}`;
|
|
1573
|
+
}
|
|
1574
|
+
default: {
|
|
1575
|
+
const n2 = i2 ? `${styleText2("cyan", S_BAR)} ` : "", r2 = i2 ? styleText2("cyan", S_BAR_END) : "";
|
|
1576
|
+
return `${s}${n2}${o2}
|
|
1577
|
+
${r2}
|
|
1578
|
+
`;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}).prompt();
|
|
1583
|
+
|
|
1584
|
+
// src/config.ts
|
|
1585
|
+
var DEFAULT_ENDPOINT = "https://app.snapfail.com";
|
|
1586
|
+
var ENV_FILE = ".env";
|
|
1587
|
+
function tryLoadProjectKey(cwd = process.cwd(), pk) {
|
|
1588
|
+
if (pk)
|
|
1589
|
+
return { projectKey: pk, endpoint: DEFAULT_ENDPOINT };
|
|
1590
|
+
const fromEnv = process.env["SNAPFAIL_PROJECT_KEY"];
|
|
1591
|
+
if (fromEnv)
|
|
1592
|
+
return { projectKey: fromEnv, endpoint: DEFAULT_ENDPOINT };
|
|
1593
|
+
const envPath = resolve(cwd, ENV_FILE);
|
|
1594
|
+
if (existsSync(envPath)) {
|
|
1595
|
+
const match = readFileSync(envPath, "utf-8").match(/^SNAPFAIL_PROJECT_KEY=(.+)$/m);
|
|
1596
|
+
if (match)
|
|
1597
|
+
return { projectKey: match[1].trim(), endpoint: DEFAULT_ENDPOINT };
|
|
1598
|
+
}
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
function loadConfig(cwd = process.cwd(), pk) {
|
|
1602
|
+
const key = tryLoadProjectKey(cwd, pk);
|
|
1603
|
+
if (key)
|
|
1604
|
+
return key;
|
|
1605
|
+
throw new Error('No project key found. Run "snapfail init" or set SNAPFAIL_PROJECT_KEY.');
|
|
1606
|
+
}
|
|
1607
|
+
async function resolveProjectKey(pk, interactive, sessionToken) {
|
|
1608
|
+
const config = tryLoadProjectKey(process.cwd(), pk);
|
|
1609
|
+
if (config)
|
|
1610
|
+
return config.projectKey;
|
|
1611
|
+
if (!interactive || !sessionToken) {
|
|
1612
|
+
throw new Error('No project key found. Run "snapfail init" or pass --pk.');
|
|
1613
|
+
}
|
|
1614
|
+
const res = await fetch(`${DEFAULT_ENDPOINT}/api/cli/projects`, {
|
|
1615
|
+
headers: { Authorization: `Bearer ${sessionToken}` }
|
|
1616
|
+
});
|
|
1617
|
+
if (!res.ok)
|
|
1618
|
+
throw new Error(`Failed to fetch projects (${res.status})`);
|
|
1619
|
+
const { projects } = await res.json();
|
|
1620
|
+
if (projects.length === 0) {
|
|
1621
|
+
throw new Error('No projects found. Run "snapfail init" to create one.');
|
|
1622
|
+
}
|
|
1623
|
+
if (projects.length === 1) {
|
|
1624
|
+
console.log(`Using project: ${projects[0].name}`);
|
|
1625
|
+
return projects[0].key;
|
|
1626
|
+
}
|
|
1627
|
+
const pick = await select({
|
|
1628
|
+
message: "Select a project",
|
|
1629
|
+
options: projects.map((proj) => ({ value: proj.key, label: proj.name, hint: proj.key }))
|
|
1630
|
+
});
|
|
1631
|
+
if (isCancel(pick)) {
|
|
1632
|
+
cancel("Cancelled.");
|
|
1633
|
+
process.exit(0);
|
|
1634
|
+
}
|
|
1635
|
+
return pick;
|
|
1636
|
+
}
|
|
1637
|
+
function writeProjectKey(projectKey, cwd = process.cwd(), keyName = "SNAPFAIL_PROJECT_KEY") {
|
|
1638
|
+
const envPath = resolve(cwd, ENV_FILE);
|
|
1639
|
+
const line = `${keyName}=${projectKey}`;
|
|
1640
|
+
const pattern = new RegExp(`^${keyName}=.*`, "m");
|
|
1641
|
+
if (existsSync(envPath)) {
|
|
1642
|
+
let content = readFileSync(envPath, "utf-8");
|
|
1643
|
+
if (pattern.test(content)) {
|
|
1644
|
+
content = content.replace(pattern, line);
|
|
1645
|
+
} else {
|
|
1646
|
+
content = content.trimEnd() + `
|
|
1647
|
+
${line}
|
|
1648
|
+
`;
|
|
1649
|
+
}
|
|
1650
|
+
writeFileSync(envPath, content, "utf-8");
|
|
1651
|
+
} else {
|
|
1652
|
+
writeFileSync(envPath, `${line}
|
|
1653
|
+
`, "utf-8");
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/session.ts
|
|
1658
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2, rmSync } from "fs";
|
|
1659
|
+
import { join } from "path";
|
|
1660
|
+
import { homedir } from "os";
|
|
1661
|
+
var SESSION_DIR = join(homedir(), ".snapfail");
|
|
1662
|
+
var SESSION_FILE = join(SESSION_DIR, "auth.json");
|
|
1663
|
+
function readSession() {
|
|
1664
|
+
try {
|
|
1665
|
+
if (!existsSync2(SESSION_FILE))
|
|
1666
|
+
return null;
|
|
1667
|
+
const raw = JSON.parse(readFileSync2(SESSION_FILE, "utf-8"));
|
|
1668
|
+
if (!raw.token || !raw.endpoint)
|
|
1669
|
+
return null;
|
|
1670
|
+
return raw;
|
|
1671
|
+
} catch {
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
function writeSession(session) {
|
|
1676
|
+
mkdirSync(SESSION_DIR, { recursive: true });
|
|
1677
|
+
writeFileSync2(SESSION_FILE, JSON.stringify(session, null, 2) + `
|
|
1678
|
+
`, "utf-8");
|
|
1679
|
+
}
|
|
1680
|
+
function clearSession() {
|
|
1681
|
+
try {
|
|
1682
|
+
if (existsSync2(SESSION_FILE))
|
|
1683
|
+
rmSync(SESSION_FILE);
|
|
1684
|
+
} catch {}
|
|
1685
|
+
}
|
|
1686
|
+
function requireSession() {
|
|
1687
|
+
const session = readSession();
|
|
1688
|
+
if (!session) {
|
|
1689
|
+
throw new Error('No active session. Run "snapfail init" to authenticate.');
|
|
1690
|
+
}
|
|
1691
|
+
return session;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/api.ts
|
|
1695
|
+
async function apiFetch(url, options) {
|
|
1696
|
+
let res;
|
|
1697
|
+
try {
|
|
1698
|
+
res = await fetch(url, options);
|
|
1699
|
+
} catch (err) {
|
|
1700
|
+
throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1701
|
+
}
|
|
1702
|
+
if (res.status === 401)
|
|
1703
|
+
throw new Error("Unauthorized: check your project key.");
|
|
1704
|
+
if (res.status === 404)
|
|
1705
|
+
return null;
|
|
1706
|
+
if (!res.ok) {
|
|
1707
|
+
const body = await res.text().catch(() => "");
|
|
1708
|
+
throw new Error(`API error ${res.status}: ${body}`);
|
|
1709
|
+
}
|
|
1710
|
+
return res.json();
|
|
1711
|
+
}
|
|
1712
|
+
async function fetchIncidents(config, opts = {}) {
|
|
1713
|
+
const url = new URL(`${config.endpoint}/api/incidents`);
|
|
1714
|
+
url.searchParams.set("projectKey", config.projectKey);
|
|
1715
|
+
if (opts.status)
|
|
1716
|
+
url.searchParams.set("status", opts.status);
|
|
1717
|
+
if (opts.limit != null)
|
|
1718
|
+
url.searchParams.set("limit", String(opts.limit));
|
|
1719
|
+
if (opts.offset != null)
|
|
1720
|
+
url.searchParams.set("offset", String(opts.offset));
|
|
1721
|
+
const data = await apiFetch(url.toString());
|
|
1722
|
+
return data;
|
|
1723
|
+
}
|
|
1724
|
+
async function fetchIncident(config, id) {
|
|
1725
|
+
const url = `${config.endpoint}/api/incidents/${id}`;
|
|
1726
|
+
const data = await apiFetch(url, {
|
|
1727
|
+
headers: { "X-Project-Key": config.projectKey }
|
|
1728
|
+
});
|
|
1729
|
+
return data;
|
|
1730
|
+
}
|
|
1731
|
+
async function deleteGroup(config, id) {
|
|
1732
|
+
const url = `${config.endpoint}/api/incidents/${id}`;
|
|
1733
|
+
const data = await apiFetch(url, {
|
|
1734
|
+
method: "DELETE",
|
|
1735
|
+
headers: { "X-Project-Key": config.projectKey }
|
|
1736
|
+
});
|
|
1737
|
+
return data !== null;
|
|
1738
|
+
}
|
|
1739
|
+
async function updateIncidentStatus(config, id, status) {
|
|
1740
|
+
const url = `${config.endpoint}/api/incidents/${id}`;
|
|
1741
|
+
const data = await apiFetch(url, {
|
|
1742
|
+
method: "PATCH",
|
|
1743
|
+
headers: { "X-Project-Key": config.projectKey, "Content-Type": "application/json" },
|
|
1744
|
+
body: JSON.stringify({ status })
|
|
1745
|
+
});
|
|
1746
|
+
return data !== null;
|
|
1747
|
+
}
|
|
1748
|
+
async function fetchSample(config, groupId, sampleIndex) {
|
|
1749
|
+
const url = `${config.endpoint}/api/incidents/${groupId}/samples/${sampleIndex}`;
|
|
1750
|
+
const data = await apiFetch(url, {
|
|
1751
|
+
headers: { "X-Project-Key": config.projectKey }
|
|
1752
|
+
});
|
|
1753
|
+
return data;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// src/format.ts
|
|
1757
|
+
var ESC2 = "\x1B[";
|
|
1758
|
+
var bold = (s) => `${ESC2}1m${s}${ESC2}0m`;
|
|
1759
|
+
var dim = (s) => `${ESC2}2m${s}${ESC2}0m`;
|
|
1760
|
+
var red = (s) => `${ESC2}31m${s}${ESC2}0m`;
|
|
1761
|
+
var green = (s) => `${ESC2}32m${s}${ESC2}0m`;
|
|
1762
|
+
var yellow = (s) => `${ESC2}33m${s}${ESC2}0m`;
|
|
1763
|
+
var cyan = (s) => `${ESC2}36m${s}${ESC2}0m`;
|
|
1764
|
+
function truncate(s, n2) {
|
|
1765
|
+
if (s.length <= n2)
|
|
1766
|
+
return s;
|
|
1767
|
+
return s.slice(0, n2 - 1) + "\u2026";
|
|
1768
|
+
}
|
|
1769
|
+
function relativeTime(ts) {
|
|
1770
|
+
const diff = Date.now() - ts;
|
|
1771
|
+
const s = Math.floor(diff / 1000);
|
|
1772
|
+
if (s < 60)
|
|
1773
|
+
return `${s}s ago`;
|
|
1774
|
+
const m2 = Math.floor(s / 60);
|
|
1775
|
+
if (m2 < 60)
|
|
1776
|
+
return `${m2}m ago`;
|
|
1777
|
+
const h2 = Math.floor(m2 / 60);
|
|
1778
|
+
if (h2 < 24)
|
|
1779
|
+
return `${h2}h ago`;
|
|
1780
|
+
const d = Math.floor(h2 / 24);
|
|
1781
|
+
return `${d}d ago`;
|
|
1782
|
+
}
|
|
1783
|
+
function formatTable(rows, headers) {
|
|
1784
|
+
const allRows = [headers, ...rows];
|
|
1785
|
+
const widths = headers.map((_2, i2) => Math.max(...allRows.map((r2) => (r2[i2] ?? "").length)));
|
|
1786
|
+
const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
|
|
1787
|
+
const header = " " + headers.map((h2, i2) => bold(pad(h2, widths[i2]))).join(" ");
|
|
1788
|
+
const divider = " " + widths.map((w) => dim("\u2500".repeat(w))).join(" ");
|
|
1789
|
+
const body = rows.map((r2) => " " + r2.map((cell, i2) => pad(cell, widths[i2])).join(" ")).join(`
|
|
1790
|
+
`);
|
|
1791
|
+
return [header, divider, body].join(`
|
|
1792
|
+
`);
|
|
1793
|
+
}
|
|
1794
|
+
function formatIncidentList(data, total, status) {
|
|
1795
|
+
if (data.length === 0) {
|
|
1796
|
+
return dim(`No ${status ?? "unresolved"} incidents found.`);
|
|
1797
|
+
}
|
|
1798
|
+
const rows = data.map((g2) => [
|
|
1799
|
+
cyan(truncate(g2.id, 12)),
|
|
1800
|
+
truncate(g2.title, 38),
|
|
1801
|
+
String(g2.count),
|
|
1802
|
+
g2.environments.join(","),
|
|
1803
|
+
relativeTime(g2.lastSeen)
|
|
1804
|
+
]);
|
|
1805
|
+
const table = formatTable(rows, ["ID", "TITLE", "COUNT", "ENV", "LAST SEEN"]);
|
|
1806
|
+
const summary = `
|
|
1807
|
+
${bold(String(total))} ${status ?? "unresolved"} incident${total !== 1 ? "s" : ""}`;
|
|
1808
|
+
return table + summary;
|
|
1809
|
+
}
|
|
1810
|
+
function formatLLMContext(group, samples) {
|
|
1811
|
+
const lines = [];
|
|
1812
|
+
lines.push(`# Incident: ${group.title}`);
|
|
1813
|
+
lines.push(`Group ID: ${group.id}`);
|
|
1814
|
+
lines.push(`Fingerprint: ${group.fingerprint}`);
|
|
1815
|
+
lines.push(`Occurrences: ${group.count} | First: ${new Date(group.firstSeen).toISOString()} | Last: ${new Date(group.lastSeen).toISOString()}`);
|
|
1816
|
+
lines.push(`Environments: ${group.environments.join(", ")}`);
|
|
1817
|
+
lines.push(`Status: ${group.status}`);
|
|
1818
|
+
lines.push("");
|
|
1819
|
+
lines.push("## Error");
|
|
1820
|
+
lines.push(`Type: ${group.errorType}`);
|
|
1821
|
+
lines.push(`Message: ${samples[0]?.errorMessage ?? group.title}`);
|
|
1822
|
+
lines.push(`Normalized: ${samples[0]?.normalizedMessage ?? group.title}`);
|
|
1823
|
+
if (samples[0] && samples[0].stackFrames.length > 0) {
|
|
1824
|
+
lines.push("");
|
|
1825
|
+
lines.push("## Stack");
|
|
1826
|
+
for (const f2 of samples[0].stackFrames) {
|
|
1827
|
+
lines.push(` ${f2.fn ?? "(anonymous)"} ${f2.file}:${f2.line}${f2.col != null ? `:${f2.col}` : ""}`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
for (let i2 = 0;i2 < samples.length; i2++) {
|
|
1831
|
+
const s = samples[i2];
|
|
1832
|
+
lines.push("");
|
|
1833
|
+
lines.push(`## Sample ${i2 + 1} / ${group.sampleIds.length}`);
|
|
1834
|
+
lines.push(`Device: ${s.device.userAgent}`);
|
|
1835
|
+
if (s.device.viewport) {
|
|
1836
|
+
lines.push(`Viewport: ${s.device.viewport.width}x${s.device.viewport.height} Language: ${s.device.language ?? "unknown"}`);
|
|
1837
|
+
}
|
|
1838
|
+
lines.push(`URL: ${s.url}`);
|
|
1839
|
+
if (s.route)
|
|
1840
|
+
lines.push(`Route: ${s.route}`);
|
|
1841
|
+
lines.push(`Environment: ${s.environmentMode}`);
|
|
1842
|
+
if (s.consoleEntries.length > 0) {
|
|
1843
|
+
lines.push("");
|
|
1844
|
+
lines.push("### Console");
|
|
1845
|
+
for (const e of s.consoleEntries) {
|
|
1846
|
+
const args = e.args.map((a2) => String(a2)).join(" ");
|
|
1847
|
+
lines.push(` [${e.level}] ${args}`);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
if (s.networkEntries.length > 0) {
|
|
1851
|
+
lines.push("");
|
|
1852
|
+
lines.push("### Network");
|
|
1853
|
+
for (const n2 of s.networkEntries) {
|
|
1854
|
+
const status = n2.status ? ` \u2192 ${n2.status}` : n2.error ? ` \u2192 ERROR` : "";
|
|
1855
|
+
lines.push(` ${n2.method} ${n2.url}${status} (${n2.durationMs}ms)`);
|
|
1924
1856
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1857
|
+
}
|
|
1858
|
+
if (s.timeline.length > 0) {
|
|
1859
|
+
lines.push("");
|
|
1860
|
+
lines.push("### Timeline");
|
|
1861
|
+
for (const e of s.timeline) {
|
|
1862
|
+
lines.push(` +${e.t}ms ${e.kind}: ${e.summary}`);
|
|
1929
1863
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1864
|
+
}
|
|
1865
|
+
if (i2 < samples.length - 1)
|
|
1866
|
+
lines.push(`
|
|
1867
|
+
---`);
|
|
1868
|
+
}
|
|
1869
|
+
return lines.join(`
|
|
1870
|
+
`);
|
|
1871
|
+
}
|
|
1872
|
+
function formatIncidentDetail(group, sample) {
|
|
1873
|
+
const lines = [];
|
|
1874
|
+
lines.push(bold(`${group.errorType}: ${truncate(group.title, 80)}`));
|
|
1875
|
+
lines.push(`${dim("Status:")} ${group.status} ${dim("\xB7")} ${group.count} occurrences ${dim("\xB7")} ${group.environments.join(", ")}`);
|
|
1876
|
+
lines.push(`${dim("First seen:")} ${new Date(group.firstSeen).toISOString().slice(0, 10)} ${dim("\xB7")} ${dim("Last seen:")} ${relativeTime(group.lastSeen)}`);
|
|
1877
|
+
if (sample.stackFrames.length > 0) {
|
|
1878
|
+
lines.push("");
|
|
1879
|
+
lines.push(bold("Stack"));
|
|
1880
|
+
for (const f2 of sample.stackFrames.slice(0, 6)) {
|
|
1881
|
+
const fn = f2.fn ? `${cyan(f2.fn)} ` : "";
|
|
1882
|
+
lines.push(` ${fn}${dim(f2.file + ":" + f2.line)}`);
|
|
1883
|
+
if (f2.codeSnippet) {
|
|
1884
|
+
for (const snippetLine of f2.codeSnippet.split(`
|
|
1885
|
+
`)) {
|
|
1886
|
+
const isError = snippetLine.startsWith("\u25BA");
|
|
1887
|
+
lines.push(isError ? ` ${bold(snippetLine)}` : ` ${dim(snippetLine)}`);
|
|
1888
|
+
}
|
|
1935
1889
|
}
|
|
1936
1890
|
}
|
|
1937
1891
|
}
|
|
1938
|
-
|
|
1892
|
+
if (sample.consoleEntries.length > 0) {
|
|
1893
|
+
lines.push("");
|
|
1894
|
+
lines.push(bold("Console"));
|
|
1895
|
+
for (const e of sample.consoleEntries.slice(-5)) {
|
|
1896
|
+
const level = e.level === "error" ? red(`[${e.level}]`) : yellow(`[${e.level}]`);
|
|
1897
|
+
const args = e.args.map((a2) => truncate(String(a2), 60)).join(" ");
|
|
1898
|
+
lines.push(` ${level} ${args}`);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
if (sample.networkEntries.length > 0) {
|
|
1902
|
+
lines.push("");
|
|
1903
|
+
lines.push(bold("Network"));
|
|
1904
|
+
for (const n2 of sample.networkEntries.slice(-5)) {
|
|
1905
|
+
const status = n2.status ? n2.status >= 400 ? red(String(n2.status)) : green(String(n2.status)) : dim("???");
|
|
1906
|
+
lines.push(` ${dim(n2.method)} ${truncate(n2.url, 50)} ${dim("\u2192")} ${status} ${dim(`(${n2.durationMs}ms)`)}`);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
if (sample.timeline.length > 0) {
|
|
1910
|
+
lines.push("");
|
|
1911
|
+
lines.push(bold("Timeline"));
|
|
1912
|
+
for (const e of sample.timeline.slice(0, 10)) {
|
|
1913
|
+
const t2 = String(e.t).padStart(5);
|
|
1914
|
+
lines.push(` ${dim(t2 + "ms")} ${e.summary}`);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return lines.join(`
|
|
1918
|
+
`);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// src/commands/incidents.ts
|
|
1922
|
+
var DEFAULT_LIMIT = 5;
|
|
1923
|
+
async function runIncidents(opts) {
|
|
1924
|
+
const session = requireSession();
|
|
1925
|
+
let projectKey;
|
|
1926
|
+
try {
|
|
1927
|
+
projectKey = await resolveProjectKey(opts.pk, !opts.json, session.token);
|
|
1928
|
+
} catch (err) {
|
|
1929
|
+
if (!opts.json) {
|
|
1930
|
+
console.error(err.message);
|
|
1931
|
+
console.error('Run "snapfail init" to set up a project.');
|
|
1932
|
+
} else {
|
|
1933
|
+
process.stdout.write(JSON.stringify({ error: err.message }) + `
|
|
1934
|
+
`);
|
|
1935
|
+
}
|
|
1936
|
+
process.exit(1);
|
|
1937
|
+
}
|
|
1938
|
+
const config = loadConfig(process.cwd(), projectKey);
|
|
1939
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
1940
|
+
const result = await fetchIncidents(config, {
|
|
1941
|
+
status: opts.status ?? "unresolved",
|
|
1942
|
+
limit,
|
|
1943
|
+
offset: opts.offset
|
|
1944
|
+
});
|
|
1945
|
+
if (opts.json) {
|
|
1946
|
+
process.stdout.write(JSON.stringify(result, null, 2) + `
|
|
1947
|
+
`);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
const statusLabel = opts.status ?? "unresolved";
|
|
1951
|
+
const offset = opts.offset ?? 0;
|
|
1952
|
+
const showing = result.data.length;
|
|
1953
|
+
const total = result.total;
|
|
1954
|
+
console.log(`
|
|
1955
|
+
Incidents (${statusLabel}) \u2014 showing ${offset + 1}\u2013${offset + showing} of ${total} groups`);
|
|
1956
|
+
if (total > offset + showing) {
|
|
1957
|
+
console.log(` Use --limit ${limit * 2} or --offset ${offset + showing} to see more.
|
|
1958
|
+
`);
|
|
1959
|
+
} else {
|
|
1960
|
+
console.log("");
|
|
1961
|
+
}
|
|
1962
|
+
console.log(formatIncidentList(result.data, result.total, opts.status));
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// src/commands/incident.ts
|
|
1966
|
+
async function runIncident(opts) {
|
|
1967
|
+
const session = requireSession();
|
|
1968
|
+
let projectKey;
|
|
1969
|
+
try {
|
|
1970
|
+
projectKey = await resolveProjectKey(opts.pk, !opts.json, session.token);
|
|
1971
|
+
} catch (err) {
|
|
1972
|
+
if (!opts.json) {
|
|
1973
|
+
console.error(err.message);
|
|
1974
|
+
} else {
|
|
1975
|
+
process.stdout.write(JSON.stringify({ error: err.message }) + `
|
|
1976
|
+
`);
|
|
1977
|
+
}
|
|
1978
|
+
process.exit(1);
|
|
1979
|
+
}
|
|
1980
|
+
const config = loadConfig(process.cwd(), projectKey);
|
|
1981
|
+
if (opts.delete) {
|
|
1982
|
+
const ok = await deleteGroup(config, opts.id);
|
|
1983
|
+
if (!ok) {
|
|
1984
|
+
console.error(`Incident group ${opts.id} not found.`);
|
|
1985
|
+
process.exit(1);
|
|
1986
|
+
}
|
|
1987
|
+
if (opts.json) {
|
|
1988
|
+
process.stdout.write(JSON.stringify({ ok: true, deleted: opts.id }) + `
|
|
1989
|
+
`);
|
|
1990
|
+
} else {
|
|
1991
|
+
console.log(`Deleted incident group ${opts.id} and all related data.`);
|
|
1992
|
+
}
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
const statusAction = opts.resolve ? "resolved" : opts.ignore ? "ignored" : opts.reopen ? "unresolved" : null;
|
|
1996
|
+
if (statusAction) {
|
|
1997
|
+
const ok = await updateIncidentStatus(config, opts.id, statusAction);
|
|
1998
|
+
if (!ok) {
|
|
1999
|
+
console.error(`Incident ${opts.id} not found.`);
|
|
2000
|
+
process.exit(1);
|
|
2001
|
+
}
|
|
2002
|
+
if (opts.json) {
|
|
2003
|
+
process.stdout.write(JSON.stringify({ ok: true, id: opts.id, status: statusAction }) + `
|
|
2004
|
+
`);
|
|
2005
|
+
} else {
|
|
2006
|
+
const label = statusAction === "resolved" ? "resolved" : statusAction === "ignored" ? "ignored" : "reopened";
|
|
2007
|
+
console.log(`Incident ${opts.id} marked as ${label}.`);
|
|
2008
|
+
}
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
if (opts.sample != null) {
|
|
2012
|
+
const s = await fetchSample(config, opts.id, opts.sample);
|
|
2013
|
+
if (!s) {
|
|
2014
|
+
console.error(`Sample ${opts.sample} not found for incident ${opts.id}`);
|
|
2015
|
+
process.exit(1);
|
|
2016
|
+
}
|
|
2017
|
+
if (opts.json) {
|
|
2018
|
+
process.stdout.write(JSON.stringify(s, null, 2) + `
|
|
2019
|
+
`);
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
const detail = await fetchIncident(config, opts.id);
|
|
2023
|
+
if (!detail) {
|
|
2024
|
+
console.error(`Incident ${opts.id} not found.`);
|
|
2025
|
+
process.exit(1);
|
|
2026
|
+
}
|
|
2027
|
+
console.log(formatIncidentDetail(detail.group, s));
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
const result = await fetchIncident(config, opts.id);
|
|
2031
|
+
if (!result) {
|
|
2032
|
+
console.error(`Incident ${opts.id} not found.`);
|
|
2033
|
+
process.exit(1);
|
|
2034
|
+
}
|
|
2035
|
+
if (opts.json) {
|
|
2036
|
+
process.stdout.write(JSON.stringify(result, null, 2) + `
|
|
2037
|
+
`);
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
console.log(formatIncidentDetail(result.group, result.sample));
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/commands/init.ts
|
|
2044
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
2045
|
+
import { resolve as resolve2, join as join2 } from "path";
|
|
2046
|
+
import { execSync as execSync2 } from "child_process";
|
|
1939
2047
|
|
|
1940
2048
|
// src/skill.ts
|
|
1941
2049
|
var SKILL_BODY = `
|
|
@@ -2053,8 +2161,8 @@ async function waitForCliToken(endpoint) {
|
|
|
2053
2161
|
resolved = true;
|
|
2054
2162
|
const session = { token, email, name: name ?? email, endpoint };
|
|
2055
2163
|
writeSession(session);
|
|
2056
|
-
setTimeout(() => server.stop(), 200);
|
|
2057
2164
|
resolve2(session);
|
|
2165
|
+
setTimeout(() => server.stop(true), 50);
|
|
2058
2166
|
return new Response(`<!doctype html><html><head><meta charset="utf-8"><title>snapfail</title>
|
|
2059
2167
|
<style>
|
|
2060
2168
|
body{background:#0a0a0a;color:#f5f5f5;font-family:system-ui,sans-serif;
|
|
@@ -2384,8 +2492,20 @@ async function runInit(cwd = process.cwd()) {
|
|
|
2384
2492
|
|
|
2385
2493
|
// src/commands/explain.ts
|
|
2386
2494
|
async function runExplain(opts) {
|
|
2387
|
-
requireSession();
|
|
2388
|
-
|
|
2495
|
+
const session = requireSession();
|
|
2496
|
+
let projectKey;
|
|
2497
|
+
try {
|
|
2498
|
+
projectKey = await resolveProjectKey(opts.pk, !opts.json, session.token);
|
|
2499
|
+
} catch (err) {
|
|
2500
|
+
if (!opts.json) {
|
|
2501
|
+
console.error(err.message);
|
|
2502
|
+
} else {
|
|
2503
|
+
process.stdout.write(JSON.stringify({ error: err.message }) + `
|
|
2504
|
+
`);
|
|
2505
|
+
}
|
|
2506
|
+
process.exit(1);
|
|
2507
|
+
}
|
|
2508
|
+
const config = loadConfig(process.cwd(), projectKey);
|
|
2389
2509
|
const result = await fetchIncident(config, opts.id);
|
|
2390
2510
|
if (!result) {
|
|
2391
2511
|
console.error(`Incident ${opts.id} not found.`);
|
|
@@ -2420,6 +2540,220 @@ async function runSkill() {
|
|
|
2420
2540
|
console.log(existed ? `Updated ${SKILL_FILE}` : `Created ${SKILL_FILE}`);
|
|
2421
2541
|
}
|
|
2422
2542
|
|
|
2543
|
+
// src/commands/login.ts
|
|
2544
|
+
async function runLogin() {
|
|
2545
|
+
const existing = readSession();
|
|
2546
|
+
if (existing && existing.endpoint === DEFAULT_ENDPOINT) {
|
|
2547
|
+
clearSession();
|
|
2548
|
+
}
|
|
2549
|
+
const session = await ensureSession(DEFAULT_ENDPOINT);
|
|
2550
|
+
console.log(`
|
|
2551
|
+
Logged in as ${session.email}`);
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// src/help.ts
|
|
2555
|
+
var GLOBAL_HELP = `
|
|
2556
|
+
snapfail \u2014 browser error capture for AI-assisted debugging
|
|
2557
|
+
|
|
2558
|
+
USAGE
|
|
2559
|
+
snapfail <command> [options]
|
|
2560
|
+
|
|
2561
|
+
COMMANDS
|
|
2562
|
+
init Set up snapfail in a project (detects framework, authenticates, writes .env)
|
|
2563
|
+
login Authenticate with your snapfail account
|
|
2564
|
+
skill Install the snapfail skill globally for Claude Code (~/.claude/skills/)
|
|
2565
|
+
incidents List incident groups for your project
|
|
2566
|
+
incident View or update a single incident group
|
|
2567
|
+
explain Output raw evidence context for LLM analysis
|
|
2568
|
+
help Show this help, or help for a specific command
|
|
2569
|
+
|
|
2570
|
+
OPTIONS
|
|
2571
|
+
--version Print the CLI version
|
|
2572
|
+
--help Show help for the current command
|
|
2573
|
+
|
|
2574
|
+
Run \`snapfail help <command>\` for detailed usage of any command.
|
|
2575
|
+
`.trimStart();
|
|
2576
|
+
var COMMAND_HELP = {
|
|
2577
|
+
init: `
|
|
2578
|
+
snapfail init \u2014 Set up snapfail in the current project
|
|
2579
|
+
|
|
2580
|
+
USAGE
|
|
2581
|
+
snapfail init
|
|
2582
|
+
|
|
2583
|
+
DESCRIPTION
|
|
2584
|
+
Authenticates your account, selects or creates a project, writes the
|
|
2585
|
+
project key to .env, injects the browser SDK into your framework config,
|
|
2586
|
+
and generates .claude/skills/snapfail/SKILL.md for AI agents.
|
|
2587
|
+
|
|
2588
|
+
Supported frameworks: astro, vite, next
|
|
2589
|
+
Run once per project. Idempotent \u2014 safe to run again.
|
|
2590
|
+
|
|
2591
|
+
EXIT CODES
|
|
2592
|
+
0 Success
|
|
2593
|
+
1 Cancelled or error
|
|
2594
|
+
`.trimStart(),
|
|
2595
|
+
login: `
|
|
2596
|
+
snapfail login \u2014 Authenticate with your snapfail account
|
|
2597
|
+
|
|
2598
|
+
USAGE
|
|
2599
|
+
snapfail login
|
|
2600
|
+
|
|
2601
|
+
DESCRIPTION
|
|
2602
|
+
Opens a browser window for OAuth authentication. Saves the session to
|
|
2603
|
+
~/.snapfail/auth.json. Clears any existing session for the default
|
|
2604
|
+
endpoint first.
|
|
2605
|
+
|
|
2606
|
+
EXIT CODES
|
|
2607
|
+
0 Authenticated successfully
|
|
2608
|
+
1 Authentication timed out or failed
|
|
2609
|
+
`.trimStart(),
|
|
2610
|
+
skill: `
|
|
2611
|
+
snapfail skill \u2014 Install the snapfail skill globally for Claude Code
|
|
2612
|
+
|
|
2613
|
+
USAGE
|
|
2614
|
+
snapfail skill
|
|
2615
|
+
|
|
2616
|
+
DESCRIPTION
|
|
2617
|
+
Writes the snapfail skill definition to ~/.claude/skills/snapfail/SKILL.md.
|
|
2618
|
+
Claude Code picks this up automatically \u2014 no project configuration needed.
|
|
2619
|
+
|
|
2620
|
+
Use this when you want snapfail available in all your Claude Code sessions,
|
|
2621
|
+
not just projects where you ran \`snapfail init\`.
|
|
2622
|
+
|
|
2623
|
+
EXIT CODES
|
|
2624
|
+
0 Skill installed or updated
|
|
2625
|
+
`.trimStart(),
|
|
2626
|
+
incidents: `
|
|
2627
|
+
snapfail incidents \u2014 List incident groups for your project
|
|
2628
|
+
|
|
2629
|
+
USAGE
|
|
2630
|
+
snapfail incidents [options]
|
|
2631
|
+
|
|
2632
|
+
OPTIONS
|
|
2633
|
+
--pk <key> Project key (overrides .env / SNAPFAIL_PROJECT_KEY)
|
|
2634
|
+
--status <s> Filter by status: unresolved (default), resolved, ignored, all
|
|
2635
|
+
--limit <n> Number of results to show (default: 5)
|
|
2636
|
+
--offset <n> Pagination offset (default: 0)
|
|
2637
|
+
--json Output raw JSON for LLM consumption
|
|
2638
|
+
|
|
2639
|
+
DESCRIPTION
|
|
2640
|
+
Lists grouped incident summaries. Each group aggregates all occurrences of
|
|
2641
|
+
the same error fingerprint. Use \`snapfail incident <id>\` to drill in.
|
|
2642
|
+
|
|
2643
|
+
When running interactively (no --json), prompts for project selection if
|
|
2644
|
+
no project key is configured.
|
|
2645
|
+
|
|
2646
|
+
EXAMPLES
|
|
2647
|
+
snapfail incidents
|
|
2648
|
+
snapfail incidents --limit 20 --status resolved
|
|
2649
|
+
snapfail incidents --json --pk proj_abc123
|
|
2650
|
+
|
|
2651
|
+
EXIT CODES
|
|
2652
|
+
0 Success
|
|
2653
|
+
1 Auth or API error
|
|
2654
|
+
`.trimStart(),
|
|
2655
|
+
incident: `
|
|
2656
|
+
snapfail incident \u2014 View or update a single incident group
|
|
2657
|
+
|
|
2658
|
+
USAGE
|
|
2659
|
+
snapfail incident <id> [options]
|
|
2660
|
+
|
|
2661
|
+
OPTIONS
|
|
2662
|
+
--pk <key> Project key (overrides .env / SNAPFAIL_PROJECT_KEY)
|
|
2663
|
+
--sample <n> Show the Nth raw sample (0-based index)
|
|
2664
|
+
--resolve Mark the incident as resolved
|
|
2665
|
+
--ignore Mark the incident as ignored
|
|
2666
|
+
--reopen Reopen a resolved or ignored incident (sets to unresolved)
|
|
2667
|
+
--delete Permanently delete the group and all its samples
|
|
2668
|
+
--json Output raw JSON for LLM consumption
|
|
2669
|
+
|
|
2670
|
+
DESCRIPTION
|
|
2671
|
+
Without status flags: displays the incident group summary and a
|
|
2672
|
+
representative timeline sample.
|
|
2673
|
+
|
|
2674
|
+
With --sample <n>: shows the full raw event data for that specific sample,
|
|
2675
|
+
useful for comparing across different occurrences.
|
|
2676
|
+
|
|
2677
|
+
Status flags (--resolve, --ignore, --reopen) update the group on the server
|
|
2678
|
+
and print confirmation. Use --json to get machine-readable output.
|
|
2679
|
+
|
|
2680
|
+
EXAMPLES
|
|
2681
|
+
snapfail incident abc123
|
|
2682
|
+
snapfail incident abc123 --sample 2
|
|
2683
|
+
snapfail incident abc123 --resolve
|
|
2684
|
+
snapfail incident abc123 --json --pk proj_abc123
|
|
2685
|
+
|
|
2686
|
+
EXIT CODES
|
|
2687
|
+
0 Success
|
|
2688
|
+
1 Not found or API error
|
|
2689
|
+
`.trimStart(),
|
|
2690
|
+
explain: `
|
|
2691
|
+
snapfail explain \u2014 Output raw evidence context for LLM analysis
|
|
2692
|
+
|
|
2693
|
+
USAGE
|
|
2694
|
+
snapfail explain <id> [options]
|
|
2695
|
+
|
|
2696
|
+
OPTIONS
|
|
2697
|
+
--pk <key> Project key (overrides .env / SNAPFAIL_PROJECT_KEY)
|
|
2698
|
+
--json Output raw JSON ({ group, samples }) instead of formatted markdown
|
|
2699
|
+
|
|
2700
|
+
DESCRIPTION
|
|
2701
|
+
Fetches the incident group and up to 3 representative samples, then outputs
|
|
2702
|
+
structured evidence (error, stack, console, network, timeline) formatted for
|
|
2703
|
+
an LLM to reason about root cause.
|
|
2704
|
+
|
|
2705
|
+
This command does NOT pre-diagnose. Pipe the output to a frontier model
|
|
2706
|
+
(Claude, GPT-4, etc.) and ask it to identify the root cause.
|
|
2707
|
+
|
|
2708
|
+
The --json flag outputs the raw data objects, useful when you want to
|
|
2709
|
+
process the evidence programmatically.
|
|
2710
|
+
|
|
2711
|
+
EXAMPLES
|
|
2712
|
+
snapfail explain abc123
|
|
2713
|
+
snapfail explain abc123 --json | claude --print "What caused this error?"
|
|
2714
|
+
|
|
2715
|
+
EXIT CODES
|
|
2716
|
+
0 Success
|
|
2717
|
+
1 Not found or API error
|
|
2718
|
+
`.trimStart()
|
|
2719
|
+
};
|
|
2720
|
+
function printHelp(command) {
|
|
2721
|
+
if (!command) {
|
|
2722
|
+
process.stdout.write(GLOBAL_HELP);
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
const text2 = COMMAND_HELP[command];
|
|
2726
|
+
if (text2) {
|
|
2727
|
+
process.stdout.write(text2);
|
|
2728
|
+
} else {
|
|
2729
|
+
process.stdout.write(`No help found for command: ${command}
|
|
2730
|
+
|
|
2731
|
+
`);
|
|
2732
|
+
process.stdout.write(GLOBAL_HELP);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
// package.json
|
|
2736
|
+
var package_default = {
|
|
2737
|
+
name: "snapfail",
|
|
2738
|
+
version: "0.0.26",
|
|
2739
|
+
type: "module",
|
|
2740
|
+
description: "CLI for snapfail \u2014 project setup, incident inspection and AI diagnostics",
|
|
2741
|
+
license: "MIT",
|
|
2742
|
+
bin: {
|
|
2743
|
+
snapfail: "./dist/index.js"
|
|
2744
|
+
},
|
|
2745
|
+
main: "./dist/index.js",
|
|
2746
|
+
files: ["dist"],
|
|
2747
|
+
scripts: {
|
|
2748
|
+
build: "bun build src/index.ts --outdir dist --target bun --format esm --sourcemap=none",
|
|
2749
|
+
prepublishOnly: "bun run build"
|
|
2750
|
+
},
|
|
2751
|
+
dependencies: {
|
|
2752
|
+
"@clack/prompts": "^1.5.1",
|
|
2753
|
+
"@snapfail/protocol": "^0.0.1"
|
|
2754
|
+
}
|
|
2755
|
+
};
|
|
2756
|
+
|
|
2423
2757
|
// src/index.ts
|
|
2424
2758
|
function parseArgs(argv) {
|
|
2425
2759
|
const [, , rawCommand = "incidents", ...rest] = argv;
|
|
@@ -2440,20 +2774,33 @@ function parseArgs(argv) {
|
|
|
2440
2774
|
}
|
|
2441
2775
|
return { command, args, flags };
|
|
2442
2776
|
}
|
|
2443
|
-
var VERSION =
|
|
2777
|
+
var VERSION = package_default.version;
|
|
2444
2778
|
async function main() {
|
|
2445
2779
|
const { command, args, flags } = parseArgs(process.argv);
|
|
2446
2780
|
const json = flags["json"] === true;
|
|
2447
2781
|
const pk = typeof flags["pk"] === "string" ? flags["pk"] : undefined;
|
|
2782
|
+
const helpFlag = flags["help"] === true || flags["h"] === true;
|
|
2448
2783
|
if (command === "--version" || command === "-v" || flags["version"] === true) {
|
|
2449
2784
|
console.log(VERSION);
|
|
2450
2785
|
return;
|
|
2451
2786
|
}
|
|
2787
|
+
if (command === "help") {
|
|
2788
|
+
printHelp(args[0]);
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
if (helpFlag) {
|
|
2792
|
+
printHelp(command);
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2452
2795
|
try {
|
|
2453
2796
|
if (command === "init") {
|
|
2454
2797
|
await runInit();
|
|
2455
2798
|
return;
|
|
2456
2799
|
}
|
|
2800
|
+
if (command === "login") {
|
|
2801
|
+
await runLogin();
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2457
2804
|
if (command === "skill") {
|
|
2458
2805
|
await runSkill();
|
|
2459
2806
|
return;
|
|
@@ -2471,25 +2818,35 @@ async function main() {
|
|
|
2471
2818
|
if (command === "incident") {
|
|
2472
2819
|
const id = args[0];
|
|
2473
2820
|
if (!id) {
|
|
2474
|
-
|
|
2821
|
+
printHelp("incident");
|
|
2475
2822
|
process.exit(1);
|
|
2476
2823
|
}
|
|
2477
2824
|
const sampleFlag = flags["sample"];
|
|
2478
2825
|
const sample = typeof sampleFlag === "string" ? parseInt(sampleFlag) : undefined;
|
|
2479
|
-
await runIncident({
|
|
2826
|
+
await runIncident({
|
|
2827
|
+
id,
|
|
2828
|
+
sample,
|
|
2829
|
+
json,
|
|
2830
|
+
pk,
|
|
2831
|
+
delete: flags["delete"] === true,
|
|
2832
|
+
resolve: flags["resolve"] === true,
|
|
2833
|
+
ignore: flags["ignore"] === true,
|
|
2834
|
+
reopen: flags["reopen"] === true
|
|
2835
|
+
});
|
|
2480
2836
|
return;
|
|
2481
2837
|
}
|
|
2482
2838
|
if (command === "explain") {
|
|
2483
2839
|
const id = args[0];
|
|
2484
2840
|
if (!id) {
|
|
2485
|
-
|
|
2841
|
+
printHelp("explain");
|
|
2486
2842
|
process.exit(1);
|
|
2487
2843
|
}
|
|
2488
2844
|
await runExplain({ id, json, pk });
|
|
2489
2845
|
return;
|
|
2490
2846
|
}
|
|
2491
|
-
console.error(`Unknown command: ${command}
|
|
2492
|
-
|
|
2847
|
+
console.error(`Unknown command: ${command}
|
|
2848
|
+
`);
|
|
2849
|
+
printHelp();
|
|
2493
2850
|
process.exit(1);
|
|
2494
2851
|
} catch (err) {
|
|
2495
2852
|
const message = err instanceof Error ? err.message : String(err);
|