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.
Files changed (2) hide show
  1. package/dist/index.js +767 -410
  2. 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 ESC3 = "\x1B";
38
- var CSI2 = `${ESC3}[`;
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: `${ESC3}7`,
68
- restore: `${ESC3}8`
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 ESC2 = "\x1B";
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) => `${ESC2}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
624
- var wrapAnsiHyperlink = (url) => `${ESC2}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
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 === ESC2 || character === CSI) {
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 === ESC2 || character === CSI) {
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
- case "cancel": {
1926
- const n2 = a2 ? ` ${styleText2(["strikethrough", "dim"], a2)}` : "", r2 = i2 ? styleText2("gray", S_BAR) : "";
1927
- return `${s}${r2}${n2}${a2.trim() ? `
1928
- ${r2}` : ""}`;
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
- default: {
1931
- const n2 = i2 ? `${styleText2("cyan", S_BAR)} ` : "", r2 = i2 ? styleText2("cyan", S_BAR_END) : "";
1932
- return `${s}${n2}${o2}
1933
- ${r2}
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
- }).prompt();
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
- const config = loadConfig(process.cwd(), opts.pk);
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 = "0.0.18";
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
- console.error("Usage: snapfail incident <id> [--sample <n>] [--json]");
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({ id, sample, json, pk, delete: flags["delete"] === true });
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
- console.error("Usage: snapfail explain <id> [--force] [--json]");
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
- console.error(`snapfail v${VERSION} \u2014 Usage: snapfail [incidents|incident|init|explain|skill] [--version]`);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snapfail",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "type": "module",
5
5
  "description": "CLI for snapfail — project setup, incident inspection and AI diagnostics",
6
6
  "license": "MIT",