snapfail 0.0.25 → 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 +730 -415
- 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,368 +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 clearSession() {
|
|
156
|
-
try {
|
|
157
|
-
if (existsSync2(SESSION_FILE))
|
|
158
|
-
rmSync(SESSION_FILE);
|
|
159
|
-
} catch {}
|
|
160
|
-
}
|
|
161
|
-
function requireSession() {
|
|
162
|
-
const session = readSession();
|
|
163
|
-
if (!session) {
|
|
164
|
-
throw new Error('No active session. Run "snapfail init" to authenticate.');
|
|
165
|
-
}
|
|
166
|
-
return session;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// src/api.ts
|
|
170
|
-
async function apiFetch(url, options) {
|
|
171
|
-
let res;
|
|
172
|
-
try {
|
|
173
|
-
res = await fetch(url, options);
|
|
174
|
-
} catch (err) {
|
|
175
|
-
throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
176
|
-
}
|
|
177
|
-
if (res.status === 401)
|
|
178
|
-
throw new Error("Unauthorized: check your project key.");
|
|
179
|
-
if (res.status === 404)
|
|
180
|
-
return null;
|
|
181
|
-
if (!res.ok) {
|
|
182
|
-
const body = await res.text().catch(() => "");
|
|
183
|
-
throw new Error(`API error ${res.status}: ${body}`);
|
|
184
|
-
}
|
|
185
|
-
return res.json();
|
|
186
|
-
}
|
|
187
|
-
async function fetchIncidents(config, opts = {}) {
|
|
188
|
-
const url = new URL(`${config.endpoint}/api/incidents`);
|
|
189
|
-
url.searchParams.set("projectKey", config.projectKey);
|
|
190
|
-
if (opts.status)
|
|
191
|
-
url.searchParams.set("status", opts.status);
|
|
192
|
-
if (opts.limit != null)
|
|
193
|
-
url.searchParams.set("limit", String(opts.limit));
|
|
194
|
-
if (opts.offset != null)
|
|
195
|
-
url.searchParams.set("offset", String(opts.offset));
|
|
196
|
-
const data = await apiFetch(url.toString());
|
|
197
|
-
return data;
|
|
198
|
-
}
|
|
199
|
-
async function fetchIncident(config, id) {
|
|
200
|
-
const url = `${config.endpoint}/api/incidents/${id}`;
|
|
201
|
-
const data = await apiFetch(url, {
|
|
202
|
-
headers: { "X-Project-Key": config.projectKey }
|
|
203
|
-
});
|
|
204
|
-
return data;
|
|
205
|
-
}
|
|
206
|
-
async function deleteGroup(config, id) {
|
|
207
|
-
const url = `${config.endpoint}/api/incidents/${id}`;
|
|
208
|
-
const data = await apiFetch(url, {
|
|
209
|
-
method: "DELETE",
|
|
210
|
-
headers: { "X-Project-Key": config.projectKey }
|
|
211
|
-
});
|
|
212
|
-
return data !== null;
|
|
213
|
-
}
|
|
214
|
-
async function fetchSample(config, groupId, sampleIndex) {
|
|
215
|
-
const url = `${config.endpoint}/api/incidents/${groupId}/samples/${sampleIndex}`;
|
|
216
|
-
const data = await apiFetch(url, {
|
|
217
|
-
headers: { "X-Project-Key": config.projectKey }
|
|
218
|
-
});
|
|
219
|
-
return data;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// src/format.ts
|
|
223
|
-
var ESC = "\x1B[";
|
|
224
|
-
var bold = (s) => `${ESC}1m${s}${ESC}0m`;
|
|
225
|
-
var dim = (s) => `${ESC}2m${s}${ESC}0m`;
|
|
226
|
-
var red = (s) => `${ESC}31m${s}${ESC}0m`;
|
|
227
|
-
var green = (s) => `${ESC}32m${s}${ESC}0m`;
|
|
228
|
-
var yellow = (s) => `${ESC}33m${s}${ESC}0m`;
|
|
229
|
-
var cyan = (s) => `${ESC}36m${s}${ESC}0m`;
|
|
230
|
-
function truncate(s, n) {
|
|
231
|
-
if (s.length <= n)
|
|
232
|
-
return s;
|
|
233
|
-
return s.slice(0, n - 1) + "\u2026";
|
|
234
|
-
}
|
|
235
|
-
function relativeTime(ts) {
|
|
236
|
-
const diff = Date.now() - ts;
|
|
237
|
-
const s = Math.floor(diff / 1000);
|
|
238
|
-
if (s < 60)
|
|
239
|
-
return `${s}s ago`;
|
|
240
|
-
const m = Math.floor(s / 60);
|
|
241
|
-
if (m < 60)
|
|
242
|
-
return `${m}m ago`;
|
|
243
|
-
const h = Math.floor(m / 60);
|
|
244
|
-
if (h < 24)
|
|
245
|
-
return `${h}h ago`;
|
|
246
|
-
const d = Math.floor(h / 24);
|
|
247
|
-
return `${d}d ago`;
|
|
248
|
-
}
|
|
249
|
-
function formatTable(rows, headers) {
|
|
250
|
-
const allRows = [headers, ...rows];
|
|
251
|
-
const widths = headers.map((_, i) => Math.max(...allRows.map((r) => (r[i] ?? "").length)));
|
|
252
|
-
const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
|
|
253
|
-
const header = " " + headers.map((h, i) => bold(pad(h, widths[i]))).join(" ");
|
|
254
|
-
const divider = " " + widths.map((w) => dim("\u2500".repeat(w))).join(" ");
|
|
255
|
-
const body = rows.map((r) => " " + r.map((cell, i) => pad(cell, widths[i])).join(" ")).join(`
|
|
256
|
-
`);
|
|
257
|
-
return [header, divider, body].join(`
|
|
258
|
-
`);
|
|
259
|
-
}
|
|
260
|
-
function formatIncidentList(data, total, status) {
|
|
261
|
-
if (data.length === 0) {
|
|
262
|
-
return dim(`No ${status ?? "unresolved"} incidents found.`);
|
|
263
|
-
}
|
|
264
|
-
const rows = data.map((g) => [
|
|
265
|
-
cyan(truncate(g.id, 12)),
|
|
266
|
-
truncate(g.title, 38),
|
|
267
|
-
String(g.count),
|
|
268
|
-
g.environments.join(","),
|
|
269
|
-
relativeTime(g.lastSeen)
|
|
270
|
-
]);
|
|
271
|
-
const table = formatTable(rows, ["ID", "TITLE", "COUNT", "ENV", "LAST SEEN"]);
|
|
272
|
-
const summary = `
|
|
273
|
-
${bold(String(total))} ${status ?? "unresolved"} incident${total !== 1 ? "s" : ""}`;
|
|
274
|
-
return table + summary;
|
|
275
|
-
}
|
|
276
|
-
function formatLLMContext(group, samples) {
|
|
277
|
-
const lines = [];
|
|
278
|
-
lines.push(`# Incident: ${group.title}`);
|
|
279
|
-
lines.push(`Group ID: ${group.id}`);
|
|
280
|
-
lines.push(`Fingerprint: ${group.fingerprint}`);
|
|
281
|
-
lines.push(`Occurrences: ${group.count} | First: ${new Date(group.firstSeen).toISOString()} | Last: ${new Date(group.lastSeen).toISOString()}`);
|
|
282
|
-
lines.push(`Environments: ${group.environments.join(", ")}`);
|
|
283
|
-
lines.push(`Status: ${group.status}`);
|
|
284
|
-
lines.push("");
|
|
285
|
-
lines.push("## Error");
|
|
286
|
-
lines.push(`Type: ${group.errorType}`);
|
|
287
|
-
lines.push(`Message: ${samples[0]?.errorMessage ?? group.title}`);
|
|
288
|
-
lines.push(`Normalized: ${samples[0]?.normalizedMessage ?? group.title}`);
|
|
289
|
-
if (samples[0] && samples[0].stackFrames.length > 0) {
|
|
290
|
-
lines.push("");
|
|
291
|
-
lines.push("## Stack");
|
|
292
|
-
for (const f of samples[0].stackFrames) {
|
|
293
|
-
lines.push(` ${f.fn ?? "(anonymous)"} ${f.file}:${f.line}${f.col != null ? `:${f.col}` : ""}`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
for (let i = 0;i < samples.length; i++) {
|
|
297
|
-
const s = samples[i];
|
|
298
|
-
lines.push("");
|
|
299
|
-
lines.push(`## Sample ${i + 1} / ${group.sampleIds.length}`);
|
|
300
|
-
lines.push(`Device: ${s.device.userAgent}`);
|
|
301
|
-
if (s.device.viewport) {
|
|
302
|
-
lines.push(`Viewport: ${s.device.viewport.width}x${s.device.viewport.height} Language: ${s.device.language ?? "unknown"}`);
|
|
303
|
-
}
|
|
304
|
-
lines.push(`URL: ${s.url}`);
|
|
305
|
-
if (s.route)
|
|
306
|
-
lines.push(`Route: ${s.route}`);
|
|
307
|
-
lines.push(`Environment: ${s.environmentMode}`);
|
|
308
|
-
if (s.consoleEntries.length > 0) {
|
|
309
|
-
lines.push("");
|
|
310
|
-
lines.push("### Console");
|
|
311
|
-
for (const e of s.consoleEntries) {
|
|
312
|
-
const args = e.args.map((a) => String(a)).join(" ");
|
|
313
|
-
lines.push(` [${e.level}] ${args}`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (s.networkEntries.length > 0) {
|
|
317
|
-
lines.push("");
|
|
318
|
-
lines.push("### Network");
|
|
319
|
-
for (const n of s.networkEntries) {
|
|
320
|
-
const status = n.status ? ` \u2192 ${n.status}` : n.error ? ` \u2192 ERROR` : "";
|
|
321
|
-
lines.push(` ${n.method} ${n.url}${status} (${n.durationMs}ms)`);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (s.timeline.length > 0) {
|
|
325
|
-
lines.push("");
|
|
326
|
-
lines.push("### Timeline");
|
|
327
|
-
for (const e of s.timeline) {
|
|
328
|
-
lines.push(` +${e.t}ms ${e.kind}: ${e.summary}`);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if (i < samples.length - 1)
|
|
332
|
-
lines.push(`
|
|
333
|
-
---`);
|
|
334
|
-
}
|
|
335
|
-
return lines.join(`
|
|
336
|
-
`);
|
|
337
|
-
}
|
|
338
|
-
function formatIncidentDetail(group, sample) {
|
|
339
|
-
const lines = [];
|
|
340
|
-
lines.push(bold(`${group.errorType}: ${truncate(group.title, 80)}`));
|
|
341
|
-
lines.push(`${dim("Status:")} ${group.status} ${dim("\xB7")} ${group.count} occurrences ${dim("\xB7")} ${group.environments.join(", ")}`);
|
|
342
|
-
lines.push(`${dim("First seen:")} ${new Date(group.firstSeen).toISOString().slice(0, 10)} ${dim("\xB7")} ${dim("Last seen:")} ${relativeTime(group.lastSeen)}`);
|
|
343
|
-
if (sample.stackFrames.length > 0) {
|
|
344
|
-
lines.push("");
|
|
345
|
-
lines.push(bold("Stack"));
|
|
346
|
-
for (const f of sample.stackFrames.slice(0, 6)) {
|
|
347
|
-
const fn = f.fn ? `${cyan(f.fn)} ` : "";
|
|
348
|
-
lines.push(` ${fn}${dim(f.file + ":" + f.line)}`);
|
|
349
|
-
if (f.codeSnippet) {
|
|
350
|
-
for (const snippetLine of f.codeSnippet.split(`
|
|
351
|
-
`)) {
|
|
352
|
-
const isError = snippetLine.startsWith("\u25BA");
|
|
353
|
-
lines.push(isError ? ` ${bold(snippetLine)}` : ` ${dim(snippetLine)}`);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
if (sample.consoleEntries.length > 0) {
|
|
359
|
-
lines.push("");
|
|
360
|
-
lines.push(bold("Console"));
|
|
361
|
-
for (const e of sample.consoleEntries.slice(-5)) {
|
|
362
|
-
const level = e.level === "error" ? red(`[${e.level}]`) : yellow(`[${e.level}]`);
|
|
363
|
-
const args = e.args.map((a) => truncate(String(a), 60)).join(" ");
|
|
364
|
-
lines.push(` ${level} ${args}`);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
if (sample.networkEntries.length > 0) {
|
|
368
|
-
lines.push("");
|
|
369
|
-
lines.push(bold("Network"));
|
|
370
|
-
for (const n of sample.networkEntries.slice(-5)) {
|
|
371
|
-
const status = n.status ? n.status >= 400 ? red(String(n.status)) : green(String(n.status)) : dim("???");
|
|
372
|
-
lines.push(` ${dim(n.method)} ${truncate(n.url, 50)} ${dim("\u2192")} ${status} ${dim(`(${n.durationMs}ms)`)}`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
if (sample.timeline.length > 0) {
|
|
376
|
-
lines.push("");
|
|
377
|
-
lines.push(bold("Timeline"));
|
|
378
|
-
for (const e of sample.timeline.slice(0, 10)) {
|
|
379
|
-
const t = String(e.t).padStart(5);
|
|
380
|
-
lines.push(` ${dim(t + "ms")} ${e.summary}`);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
return lines.join(`
|
|
384
|
-
`);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// src/commands/incidents.ts
|
|
388
|
-
async function runIncidents(opts) {
|
|
389
|
-
requireSession();
|
|
390
|
-
const config = loadConfig(process.cwd(), opts.pk);
|
|
391
|
-
const result = await fetchIncidents(config, {
|
|
392
|
-
status: opts.status ?? "unresolved",
|
|
393
|
-
limit: opts.limit,
|
|
394
|
-
offset: opts.offset
|
|
395
|
-
});
|
|
396
|
-
if (opts.json) {
|
|
397
|
-
process.stdout.write(JSON.stringify(result, null, 2) + `
|
|
398
|
-
`);
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
console.log(formatIncidentList(result.data, result.total, opts.status));
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// src/commands/incident.ts
|
|
405
|
-
async function runIncident(opts) {
|
|
406
|
-
requireSession();
|
|
407
|
-
const config = loadConfig(process.cwd(), opts.pk);
|
|
408
|
-
if (opts.delete) {
|
|
409
|
-
const ok = await deleteGroup(config, opts.id);
|
|
410
|
-
if (!ok) {
|
|
411
|
-
console.error(`Incident group ${opts.id} not found.`);
|
|
412
|
-
process.exit(1);
|
|
413
|
-
}
|
|
414
|
-
if (opts.json) {
|
|
415
|
-
process.stdout.write(JSON.stringify({ ok: true, deleted: opts.id }) + `
|
|
416
|
-
`);
|
|
417
|
-
} else {
|
|
418
|
-
console.log(`Deleted incident group ${opts.id} and all related data.`);
|
|
419
|
-
}
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
if (opts.sample != null) {
|
|
423
|
-
const s = await fetchSample(config, opts.id, opts.sample);
|
|
424
|
-
if (!s) {
|
|
425
|
-
console.error(`Sample ${opts.sample} not found for incident ${opts.id}`);
|
|
426
|
-
process.exit(1);
|
|
427
|
-
}
|
|
428
|
-
if (opts.json) {
|
|
429
|
-
process.stdout.write(JSON.stringify(s, null, 2) + `
|
|
430
|
-
`);
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
const detail = await fetchIncident(config, opts.id);
|
|
434
|
-
if (!detail) {
|
|
435
|
-
console.error(`Incident ${opts.id} not found.`);
|
|
436
|
-
process.exit(1);
|
|
437
|
-
}
|
|
438
|
-
console.log(formatIncidentDetail(detail.group, s));
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
const result = await fetchIncident(config, opts.id);
|
|
442
|
-
if (!result) {
|
|
443
|
-
console.error(`Incident ${opts.id} not found.`);
|
|
444
|
-
process.exit(1);
|
|
445
|
-
}
|
|
446
|
-
if (opts.json) {
|
|
447
|
-
process.stdout.write(JSON.stringify(result, null, 2) + `
|
|
448
|
-
`);
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
console.log(formatIncidentDetail(result.group, result.sample));
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// src/commands/init.ts
|
|
455
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
456
|
-
import { resolve as resolve2, join as join2 } from "path";
|
|
457
|
-
import { execSync as execSync2 } from "child_process";
|
|
458
96
|
|
|
459
97
|
// ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
|
|
460
98
|
import { styleText } from "util";
|
|
@@ -592,7 +230,7 @@ var fastStringWidth = (input, options = {}) => {
|
|
|
592
230
|
var dist_default2 = fastStringWidth;
|
|
593
231
|
|
|
594
232
|
// ../../node_modules/.bun/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js
|
|
595
|
-
var
|
|
233
|
+
var ESC = "\x1B";
|
|
596
234
|
var CSI = "\x9B";
|
|
597
235
|
var END_CODE = 39;
|
|
598
236
|
var ANSI_ESCAPE_BELL = "\x07";
|
|
@@ -626,8 +264,8 @@ var getClosingCode = (openingCode) => {
|
|
|
626
264
|
return 0;
|
|
627
265
|
return;
|
|
628
266
|
};
|
|
629
|
-
var wrapAnsiCode = (code) => `${
|
|
630
|
-
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}`;
|
|
631
269
|
var wrapWord = (rows, word, columns) => {
|
|
632
270
|
const characters = word[Symbol.iterator]();
|
|
633
271
|
let isInsideEscape = false;
|
|
@@ -646,7 +284,7 @@ var wrapWord = (rows, word, columns) => {
|
|
|
646
284
|
rows.push(character);
|
|
647
285
|
visible = 0;
|
|
648
286
|
}
|
|
649
|
-
if (character ===
|
|
287
|
+
if (character === ESC || character === CSI) {
|
|
650
288
|
isInsideEscape = true;
|
|
651
289
|
isInsideLinkEscape = word.startsWith(ANSI_ESCAPE_LINK, rawCharacterIndex + 1);
|
|
652
290
|
}
|
|
@@ -765,7 +403,7 @@ var exec = (string, columns, options = {}) => {
|
|
|
765
403
|
} else {
|
|
766
404
|
inSurrogate = false;
|
|
767
405
|
}
|
|
768
|
-
if (character ===
|
|
406
|
+
if (character === ESC || character === CSI) {
|
|
769
407
|
GROUP_REGEX.lastIndex = i + 1;
|
|
770
408
|
const groupsResult = GROUP_REGEX.exec(preString);
|
|
771
409
|
const groups = groupsResult?.groups;
|
|
@@ -1901,47 +1539,511 @@ ${l2}
|
|
|
1901
1539
|
}
|
|
1902
1540
|
}
|
|
1903
1541
|
}
|
|
1904
|
-
}).prompt();
|
|
1905
|
-
};
|
|
1906
|
-
var i = `${styleText2("gray", S_BAR)} `;
|
|
1907
|
-
var text = (t2) => new n({
|
|
1908
|
-
validate: t2.validate,
|
|
1909
|
-
placeholder: t2.placeholder,
|
|
1910
|
-
defaultValue: t2.defaultValue,
|
|
1911
|
-
initialValue: t2.initialValue,
|
|
1912
|
-
output: t2.output,
|
|
1913
|
-
signal: t2.signal,
|
|
1914
|
-
input: t2.input,
|
|
1915
|
-
render() {
|
|
1916
|
-
const i2 = t2?.withGuide ?? settings.withGuide, s = `${`${i2 ? `${styleText2("gray", S_BAR)}
|
|
1917
|
-
` : ""}${symbol(this.state)} `}${t2.message}
|
|
1918
|
-
`, 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 ?? "";
|
|
1919
|
-
switch (this.state) {
|
|
1920
|
-
case "error": {
|
|
1921
|
-
const n2 = this.error ? ` ${styleText2("yellow", this.error)}` : "", r2 = i2 ? `${styleText2("yellow", S_BAR)} ` : "", d = i2 ? styleText2("yellow", S_BAR_END) : "";
|
|
1922
|
-
return `${s.trim()}
|
|
1923
|
-
${r2}${o2}
|
|
1924
|
-
${d}${n2}
|
|
1925
|
-
`;
|
|
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}`);
|
|
1926
1848
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
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)`);
|
|
1930
1856
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
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}`);
|
|
1935
1863
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
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
|
+
}
|
|
1941
1889
|
}
|
|
1942
1890
|
}
|
|
1943
1891
|
}
|
|
1944
|
-
|
|
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";
|
|
1945
2047
|
|
|
1946
2048
|
// src/skill.ts
|
|
1947
2049
|
var SKILL_BODY = `
|
|
@@ -2059,8 +2161,8 @@ async function waitForCliToken(endpoint) {
|
|
|
2059
2161
|
resolved = true;
|
|
2060
2162
|
const session = { token, email, name: name ?? email, endpoint };
|
|
2061
2163
|
writeSession(session);
|
|
2062
|
-
setTimeout(() => server.stop(), 200);
|
|
2063
2164
|
resolve2(session);
|
|
2165
|
+
setTimeout(() => server.stop(true), 50);
|
|
2064
2166
|
return new Response(`<!doctype html><html><head><meta charset="utf-8"><title>snapfail</title>
|
|
2065
2167
|
<style>
|
|
2066
2168
|
body{background:#0a0a0a;color:#f5f5f5;font-family:system-ui,sans-serif;
|
|
@@ -2390,8 +2492,20 @@ async function runInit(cwd = process.cwd()) {
|
|
|
2390
2492
|
|
|
2391
2493
|
// src/commands/explain.ts
|
|
2392
2494
|
async function runExplain(opts) {
|
|
2393
|
-
requireSession();
|
|
2394
|
-
|
|
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);
|
|
2395
2509
|
const result = await fetchIncident(config, opts.id);
|
|
2396
2510
|
if (!result) {
|
|
2397
2511
|
console.error(`Incident ${opts.id} not found.`);
|
|
@@ -2436,10 +2550,192 @@ async function runLogin() {
|
|
|
2436
2550
|
console.log(`
|
|
2437
2551
|
Logged in as ${session.email}`);
|
|
2438
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
|
+
}
|
|
2439
2735
|
// package.json
|
|
2440
2736
|
var package_default = {
|
|
2441
2737
|
name: "snapfail",
|
|
2442
|
-
version: "0.0.
|
|
2738
|
+
version: "0.0.26",
|
|
2443
2739
|
type: "module",
|
|
2444
2740
|
description: "CLI for snapfail \u2014 project setup, incident inspection and AI diagnostics",
|
|
2445
2741
|
license: "MIT",
|
|
@@ -2483,10 +2779,19 @@ async function main() {
|
|
|
2483
2779
|
const { command, args, flags } = parseArgs(process.argv);
|
|
2484
2780
|
const json = flags["json"] === true;
|
|
2485
2781
|
const pk = typeof flags["pk"] === "string" ? flags["pk"] : undefined;
|
|
2782
|
+
const helpFlag = flags["help"] === true || flags["h"] === true;
|
|
2486
2783
|
if (command === "--version" || command === "-v" || flags["version"] === true) {
|
|
2487
2784
|
console.log(VERSION);
|
|
2488
2785
|
return;
|
|
2489
2786
|
}
|
|
2787
|
+
if (command === "help") {
|
|
2788
|
+
printHelp(args[0]);
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
if (helpFlag) {
|
|
2792
|
+
printHelp(command);
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2490
2795
|
try {
|
|
2491
2796
|
if (command === "init") {
|
|
2492
2797
|
await runInit();
|
|
@@ -2513,25 +2818,35 @@ async function main() {
|
|
|
2513
2818
|
if (command === "incident") {
|
|
2514
2819
|
const id = args[0];
|
|
2515
2820
|
if (!id) {
|
|
2516
|
-
|
|
2821
|
+
printHelp("incident");
|
|
2517
2822
|
process.exit(1);
|
|
2518
2823
|
}
|
|
2519
2824
|
const sampleFlag = flags["sample"];
|
|
2520
2825
|
const sample = typeof sampleFlag === "string" ? parseInt(sampleFlag) : undefined;
|
|
2521
|
-
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
|
+
});
|
|
2522
2836
|
return;
|
|
2523
2837
|
}
|
|
2524
2838
|
if (command === "explain") {
|
|
2525
2839
|
const id = args[0];
|
|
2526
2840
|
if (!id) {
|
|
2527
|
-
|
|
2841
|
+
printHelp("explain");
|
|
2528
2842
|
process.exit(1);
|
|
2529
2843
|
}
|
|
2530
2844
|
await runExplain({ id, json, pk });
|
|
2531
2845
|
return;
|
|
2532
2846
|
}
|
|
2533
|
-
console.error(`Unknown command: ${command}
|
|
2534
|
-
|
|
2847
|
+
console.error(`Unknown command: ${command}
|
|
2848
|
+
`);
|
|
2849
|
+
printHelp();
|
|
2535
2850
|
process.exit(1);
|
|
2536
2851
|
} catch (err) {
|
|
2537
2852
|
const message = err instanceof Error ? err.message : String(err);
|