snapfail 0.0.2 → 0.0.7

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.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // SnapFail CLI entry point
3
+ // Built for Node.js (also works with Bun).
4
+ // The compiled dist/index.js is generated by `bun run build`.
5
+ import "../dist/index.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * SnapFail CLI
3
+ *
4
+ * Authenticates against the SnapFail dashboard via a browser-based OAuth-style
5
+ * flow, obtains an API key for a project, writes it to the project's .env file,
6
+ * and injects the SDK plugin/integration into the project's Vite or Astro config
7
+ * automatically.
8
+ *
9
+ * Usage:
10
+ * snapfail init # initialise SnapFail in the current project
11
+ * snapfail init ./path # use a different project root
12
+ * snapfail --version
13
+ * snapfail --help
14
+ */
15
+ export {};
package/dist/index.js CHANGED
@@ -1,24 +1,435 @@
1
- #!/usr/bin/env node
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
2
3
 
3
4
  // src/index.ts
4
- import { outro } from "@clack/prompts";
5
- var [framework, command] = process.argv.slice(2);
6
- async function main() {
7
- if (framework === "astro" && command === "init") {
8
- const { init } = await import("./init-PT3WVALP.js");
9
- await init();
10
- return;
11
- }
12
- console.log([
13
- "",
14
- " snapfail \u2014 error monitoring for modern web apps",
15
- "",
16
- " Usage:",
17
- " npx snapfail astro init Add Snapfail to an Astro project",
18
- ""
19
- ].join("\n"));
20
- }
21
- main().catch((err) => {
22
- outro(`Error: ${err.message}`);
5
+ import { join, resolve, basename } from "path";
6
+ import { existsSync, readFileSync, writeFileSync } from "fs";
7
+ import { createServer } from "http";
8
+ import { spawn } from "child_process";
9
+ var VERSION = "0.0.6";
10
+ var APP_URL = process.env["SNAPFAIL_APP_URL"] ?? "https://app.snapfail.com";
11
+ var AUTH_TIMEOUT_MS = 5 * 60 * 1000;
12
+ var c = {
13
+ reset: "\x1B[0m",
14
+ bold: "\x1B[1m",
15
+ dim: "\x1B[2m",
16
+ green: "\x1B[32m",
17
+ yellow: "\x1B[33m",
18
+ red: "\x1B[31m",
19
+ cyan: "\x1B[36m"
20
+ };
21
+ function fmt(style, msg) {
22
+ return `${c[style]}${msg}${c.reset}`;
23
+ }
24
+ var log = (msg) => console.log(` ${fmt("green", "▶")} ${msg}`);
25
+ var warn = (msg) => console.warn(` ${fmt("yellow", "⚠")} ${msg}`);
26
+ var fail = (msg) => console.error(` ${fmt("red", "✗")} ${msg}`);
27
+ var success = (msg) => console.log(` ${fmt("green", "✓")} ${msg}`);
28
+ var dim = (msg) => fmt("dim", msg);
29
+ var bold = (msg) => fmt("bold", msg);
30
+ var green = (msg) => fmt("green", msg);
31
+ async function askYesNo(question, defaultYes = true) {
32
+ const readline = await import("readline");
33
+ const rl = readline.createInterface({
34
+ input: process.stdin,
35
+ output: process.stdout
36
+ });
37
+ return new Promise((resolve2) => {
38
+ const promptChar = defaultYes ? "[Y/n]" : "[y/N]";
39
+ rl.question(` ${fmt("cyan", "?")} ${question} ${dim(promptChar)} `, (answer) => {
40
+ rl.close();
41
+ const clean = answer.trim().toLowerCase();
42
+ if (clean === "") {
43
+ resolve2(defaultYes);
44
+ } else if (clean === "y" || clean === "yes") {
45
+ resolve2(true);
46
+ } else if (clean === "n" || clean === "no") {
47
+ resolve2(false);
48
+ } else {
49
+ resolve2(defaultYes);
50
+ }
51
+ });
52
+ });
53
+ }
54
+ function randomHex(bytes = 16) {
55
+ const buf = new Uint8Array(bytes);
56
+ crypto.getRandomValues(buf);
57
+ return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
58
+ }
59
+ function openBrowser(url) {
60
+ try {
61
+ const p = process.platform;
62
+ if (p === "darwin")
63
+ spawn("open", [url], { stdio: "ignore", detached: true }).unref();
64
+ else if (p === "win32")
65
+ spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
66
+ else
67
+ spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
68
+ } catch {}
69
+ }
70
+ function detectProject(cwd) {
71
+ for (const f of ["astro.config.mjs", "astro.config.ts", "astro.config.js", "astro.config.cjs"]) {
72
+ if (existsSync(join(cwd, f)))
73
+ return { kind: "astro", configFile: join(cwd, f), htmlFile: null };
74
+ }
75
+ for (const f of ["vite.config.ts", "vite.config.js", "vite.config.mts", "vite.config.mjs"]) {
76
+ if (existsSync(join(cwd, f)))
77
+ return { kind: "vite", configFile: join(cwd, f), htmlFile: null };
78
+ }
79
+ for (const f of ["index.html", "public/index.html", "src/index.html"]) {
80
+ if (existsSync(join(cwd, f)))
81
+ return { kind: "html", configFile: null, htmlFile: join(cwd, f) };
82
+ }
83
+ return { kind: "unknown", configFile: null, htmlFile: null };
84
+ }
85
+ function lastImportEnd(content) {
86
+ const importRe = /^import\s[^;]+?['"][^\n]*['"];?\n/gm;
87
+ let pos = 0;
88
+ let m;
89
+ while ((m = importRe.exec(content)) !== null) {
90
+ pos = m.index + m[0].length;
91
+ }
92
+ return pos;
93
+ }
94
+ function insertImport(content, importLine) {
95
+ const pos = lastImportEnd(content);
96
+ return pos > 0 ? content.slice(0, pos) + importLine + `
97
+ ` + content.slice(pos) : importLine + `
98
+ ` + content;
99
+ }
100
+ function injectAstroConfig(configPath, appName, enableReplay) {
101
+ try {
102
+ let src = readFileSync(configPath, "utf8");
103
+ if (src.includes("@snapfail/sdk") || src.includes("snapfailAstro") || src.includes("snapfail(")) {
104
+ warn(`SnapFail is already present in ${basename(configPath)}.`);
105
+ return true;
106
+ }
107
+ src = insertImport(src, `import { snapfail } from '@snapfail/sdk';`);
108
+ const replayParam = enableReplay ? ", recordSession: true" : "";
109
+ const pluginCall = `snapfail({ apiKey: process.env.SNAPFAIL_API_KEY ?? '', appName: '${appName}'${replayParam} })`;
110
+ if (/integrations\s*:\s*\[/.test(src)) {
111
+ src = src.replace(/integrations\s*:\s*\[/, `integrations: [
112
+ ${pluginCall},`);
113
+ } else if (/defineConfig\s*\(/.test(src)) {
114
+ src = src.replace(/defineConfig\s*\(\s*\{/, `defineConfig({
115
+ integrations: [${pluginCall}],`);
116
+ } else {
117
+ warn("Could not find defineConfig in your Astro config — please add the integration manually.");
118
+ return false;
119
+ }
120
+ writeFileSync(configPath, src, "utf8");
121
+ return true;
122
+ } catch (err) {
123
+ fail(`Could not modify ${basename(configPath)}: ${err.message}`);
124
+ return false;
125
+ }
126
+ }
127
+ function injectViteConfig(configPath, appName, enableReplay) {
128
+ try {
129
+ let src = readFileSync(configPath, "utf8");
130
+ if (src.includes("@snapfail/sdk") || src.includes("snapfailVite") || src.includes("snapfail(")) {
131
+ warn(`SnapFail is already present in ${basename(configPath)}.`);
132
+ return true;
133
+ }
134
+ src = insertImport(src, `import { snapfail } from '@snapfail/sdk';`);
135
+ const replayParam = enableReplay ? ", recordSession: true" : "";
136
+ const pluginCall = `snapfail({ apiKey: process.env.SNAPFAIL_API_KEY ?? '', appName: '${appName}'${replayParam} })`;
137
+ if (/plugins\s*:\s*\[/.test(src)) {
138
+ src = src.replace(/plugins\s*:\s*\[/, `plugins: [
139
+ ${pluginCall},`);
140
+ } else if (/defineConfig\s*\(/.test(src)) {
141
+ src = src.replace(/defineConfig\s*\(\s*\{/, `defineConfig({
142
+ plugins: [${pluginCall}],`);
143
+ } else {
144
+ warn("Could not find defineConfig in your Vite config — please add the plugin manually.");
145
+ return false;
146
+ }
147
+ writeFileSync(configPath, src, "utf8");
148
+ return true;
149
+ } catch (err) {
150
+ fail(`Could not modify ${basename(configPath)}: ${err.message}`);
151
+ return false;
152
+ }
153
+ }
154
+ function injectHtmlFile(htmlPath, apiKey, enableReplay) {
155
+ try {
156
+ let src = readFileSync(htmlPath, "utf8");
157
+ if (src.includes("__snapfailConfig") || src.includes("@snapfail/sdk") || src.includes("widget.js")) {
158
+ warn(`SnapFail is already present in ${basename(htmlPath)}.`);
159
+ return true;
160
+ }
161
+ const config = { apiKey };
162
+ if (enableReplay) {
163
+ config.recordSession = true;
164
+ }
165
+ const snippet = ` <!-- SnapFail SDK -->
166
+ <script>window.__snapfailConfig=${JSON.stringify(config)};</script>
167
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@snapfail/sdk/dist/widget.js"></script>
168
+ `;
169
+ src = src.replace("</head>", `${snippet}</head>`);
170
+ writeFileSync(htmlPath, src, "utf8");
171
+ return true;
172
+ } catch (err) {
173
+ fail(`Could not modify ${basename(htmlPath)}: ${err.message}`);
174
+ return false;
175
+ }
176
+ }
177
+ function writeEnvFile(cwd, apiKey) {
178
+ const envPath = join(cwd, ".env");
179
+ const KEY = "SNAPFAIL_API_KEY";
180
+ const entry = `${KEY}=${apiKey}`;
181
+ try {
182
+ if (!existsSync(envPath)) {
183
+ writeFileSync(envPath, `# SnapFail
184
+ ${entry}
185
+ `, "utf8");
186
+ success(`Created .env → ${bold(KEY)}`);
187
+ } else {
188
+ let content = readFileSync(envPath, "utf8");
189
+ if (new RegExp(`^${KEY}=`, "m").test(content)) {
190
+ content = content.replace(new RegExp(`^${KEY}=.*$`, "m"), entry);
191
+ success(`Updated ${bold(KEY)} in .env`);
192
+ } else {
193
+ const nl = content.endsWith(`
194
+ `) ? "" : `
195
+ `;
196
+ content = content + `${nl}
197
+ # SnapFail
198
+ ${entry}
199
+ `;
200
+ success(`Added ${bold(KEY)} to .env`);
201
+ }
202
+ writeFileSync(envPath, content, "utf8");
203
+ }
204
+ } catch (err) {
205
+ warn(`Could not write .env: ${err.message}`);
206
+ warn(`Add manually: ${bold(entry)}`);
207
+ }
208
+ }
209
+ function printManualInstructions(kind, apiKey, appName, enableReplay) {
210
+ console.log("");
211
+ warn("Automatic injection was skipped. Add SnapFail manually:");
212
+ console.log("");
213
+ const replayParam = enableReplay ? ", recordSession: true" : "";
214
+ if (kind === "astro") {
215
+ console.log(` ${dim("# .env")}`);
216
+ console.log(` SNAPFAIL_API_KEY=${apiKey}`);
217
+ console.log("");
218
+ console.log(` ${dim("// astro.config.mjs")}`);
219
+ console.log(` import { snapfail } from '@snapfail/sdk';`);
220
+ console.log(` export default defineConfig({`);
221
+ console.log(` integrations: [snapfail({ apiKey: process.env.SNAPFAIL_API_KEY ?? '', appName: '${appName}'${replayParam} })],`);
222
+ console.log(` });`);
223
+ } else if (kind === "vite") {
224
+ console.log(` ${dim("# .env")}`);
225
+ console.log(` SNAPFAIL_API_KEY=${apiKey}`);
226
+ console.log("");
227
+ console.log(` ${dim("// vite.config.ts")}`);
228
+ console.log(` import { snapfail } from '@snapfail/sdk';`);
229
+ console.log(` export default defineConfig({`);
230
+ console.log(` plugins: [snapfail({ apiKey: process.env.SNAPFAIL_API_KEY ?? '', appName: '${appName}'${replayParam} })],`);
231
+ console.log(` });`);
232
+ } else {
233
+ const config = { apiKey };
234
+ if (enableReplay) {
235
+ config.recordSession = true;
236
+ }
237
+ console.log(` ${dim("<!-- index.html <head> — CDN widget (no build tool) -->")}`);
238
+ console.log(` <script>window.__snapfailConfig=${JSON.stringify(config)};</script>`);
239
+ console.log(` <script type="module" src="https://cdn.jsdelivr.net/npm/@snapfail/sdk/dist/widget.js"></script>`);
240
+ }
241
+ console.log("");
242
+ }
243
+ async function runBrowserAuth() {
244
+ const csrfToken = randomHex(16);
245
+ return new Promise((resolve2) => {
246
+ let done = false;
247
+ let server = null;
248
+ const timeout = setTimeout(() => {
249
+ if (!done) {
250
+ done = true;
251
+ server?.close();
252
+ fail("Authentication timed out (5 min). Run `snapfail init` again.");
253
+ resolve2(null);
254
+ }
255
+ }, AUTH_TIMEOUT_MS);
256
+ server = createServer((req, res) => {
257
+ const corsHeaders = {
258
+ "Access-Control-Allow-Origin": "*",
259
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
260
+ "Access-Control-Allow-Headers": "Content-Type"
261
+ };
262
+ if (req.method === "OPTIONS") {
263
+ res.writeHead(204, corsHeaders);
264
+ res.end();
265
+ return;
266
+ }
267
+ const url = new URL(req.url ?? "/", `http://localhost`);
268
+ if (url.pathname !== "/callback") {
269
+ res.writeHead(404, corsHeaders);
270
+ res.end("Not found");
271
+ return;
272
+ }
273
+ const apiKey = url.searchParams.get("apiKey") ?? "";
274
+ const projectName = url.searchParams.get("projectName") ?? "My App";
275
+ const returnToken = url.searchParams.get("token") ?? "";
276
+ if (returnToken !== csrfToken || !apiKey.startsWith("sf_")) {
277
+ const body2 = htmlString("✗ Auth failed", `<p style="color:#ff4444">Invalid token or API key. Please run <code>snapfail init</code> again.</p>`, false);
278
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8", ...corsHeaders });
279
+ res.end(body2);
280
+ return;
281
+ }
282
+ const body = htmlString("✓ Connected!", `<p>You can close this tab and return to your terminal.</p>
283
+ <p style="color:#666;font-size:.85em;margin-top:1.5em">Project: <strong>${esc(projectName)}</strong></p>`, true);
284
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", ...corsHeaders });
285
+ res.end(body);
286
+ setTimeout(() => {
287
+ clearTimeout(timeout);
288
+ server?.close();
289
+ }, 150);
290
+ if (!done) {
291
+ done = true;
292
+ resolve2({ apiKey, projectName });
293
+ }
294
+ });
295
+ server.listen(0, "127.0.0.1", () => {
296
+ const addr = server.address();
297
+ const callbackBase = `http://localhost:${addr.port}/callback`;
298
+ const authUrl = `${APP_URL}/cli-auth?token=${csrfToken}&callback=${encodeURIComponent(callbackBase)}`;
299
+ console.log("");
300
+ log("Opening browser for authentication…");
301
+ console.log(` ${dim(authUrl)}`);
302
+ console.log("");
303
+ log("Waiting for you to complete sign-in in the browser…");
304
+ console.log(` ${dim("(press Ctrl+C to abort)")}`);
305
+ openBrowser(authUrl);
306
+ });
307
+ });
308
+ }
309
+ function htmlString(title, body, ok) {
310
+ const accentColor = ok ? "#c4ffb8" : "#ff4444";
311
+ return `<!DOCTYPE html>
312
+ <html lang="en">
313
+ <head>
314
+ <meta charset="utf-8">
315
+ <title>${esc(title)} — SnapFail</title>
316
+ <style>
317
+ *{box-sizing:border-box}
318
+ body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;
319
+ background:#0a0a0a;color:#e0e0e0;font-family:ui-monospace,monospace;text-align:center;padding:2rem}
320
+ h1{font-size:2.5rem;margin-bottom:1rem;color:${accentColor}}
321
+ p{color:#888;margin:.5rem 0}
322
+ strong{color:${accentColor}}
323
+ code{background:#1a1a1a;padding:.1em .4em;border-radius:4px}
324
+ </style>
325
+ </head>
326
+ <body>
327
+ <div>
328
+ <h1>${esc(title)}</h1>
329
+ ${body}
330
+ </div>
331
+ </body>
332
+ </html>`;
333
+ }
334
+ function esc(s) {
335
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
336
+ }
337
+ function getInstallCommand(cwd) {
338
+ if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb"))) {
339
+ return "bun add @snapfail/sdk";
340
+ }
341
+ if (existsSync(join(cwd, "pnpm-lock.yaml")) || existsSync(join(cwd, "pnpm-workspace.yaml"))) {
342
+ return "pnpm add @snapfail/sdk";
343
+ }
344
+ if (existsSync(join(cwd, "yarn.lock"))) {
345
+ return "yarn add @snapfail/sdk";
346
+ }
347
+ return "npm install @snapfail/sdk";
348
+ }
349
+ async function cmdInit(cwd) {
350
+ console.log("");
351
+ console.log(` ${bold("SnapFail CLI")} ${dim(`v${VERSION}`)}`);
352
+ console.log(` ${dim("─".repeat(38))}`);
353
+ console.log("");
354
+ const project = detectProject(cwd);
355
+ if (project.kind === "unknown") {
356
+ fail("No astro.config, vite.config, or index.html found.");
357
+ fail("Run `snapfail init` from your project's root directory.");
358
+ process.exit(1);
359
+ }
360
+ log(`Detected: ${bold(project.kind)} project`);
361
+ if (project.configFile)
362
+ log(`Config: ${dim(basename(project.configFile))}`);
363
+ const auth = await runBrowserAuth();
364
+ if (!auth)
365
+ process.exit(1);
366
+ const { apiKey, projectName } = auth;
367
+ console.log("");
368
+ success(`Authenticated! Project: ${bold(green(projectName))}`);
369
+ success(`API Key: ${bold(green(apiKey))}`);
370
+ console.log("");
371
+ if (project.kind !== "html") {
372
+ log("Writing API key to .env…");
373
+ writeEnvFile(cwd, apiKey);
374
+ console.log("");
375
+ }
376
+ const enableReplay = await askYesNo("Do you want to enable session recording/replays? (captures full rrweb replay videos)", false);
377
+ console.log("");
378
+ log("Injecting SnapFail SDK into your project…");
379
+ let ok = false;
380
+ if (project.kind === "astro" && project.configFile) {
381
+ ok = injectAstroConfig(project.configFile, projectName, enableReplay);
382
+ } else if (project.kind === "vite" && project.configFile) {
383
+ ok = injectViteConfig(project.configFile, projectName, enableReplay);
384
+ } else if (project.htmlFile) {
385
+ ok = injectHtmlFile(project.htmlFile, apiKey, enableReplay);
386
+ }
387
+ if (!ok) {
388
+ printManualInstructions(project.kind, apiKey, projectName, enableReplay);
389
+ } else {
390
+ console.log("");
391
+ success("SnapFail is now integrated into your project!");
392
+ console.log("");
393
+ if (project.kind !== "html") {
394
+ console.log(` ${dim("Make sure the SDK package is installed:")}`);
395
+ console.log(` ${bold(" " + getInstallCommand(cwd))}`);
396
+ console.log("");
397
+ console.log(` ${dim("Add .env to .gitignore to keep your API key private:")}`);
398
+ console.log(` ${bold(' echo ".env" >> .gitignore')}`);
399
+ }
400
+ console.log("");
401
+ }
402
+ }
403
+ function printHelp() {
404
+ console.log(`
405
+ ${bold("snapfail")} ${dim(`v${VERSION}`)} — Error-tracking CLI for SnapFail Protocol
406
+
407
+ ${bold("Usage:")}
408
+ snapfail init [path] Authenticate & inject the SDK into your project
409
+ snapfail --version Print version
410
+ snapfail --help Show this help
411
+
412
+ ${bold("Examples:")}
413
+ snapfail init ${dim("# run from your project root")}
414
+ snapfail init ./my-app ${dim("# run against a specific directory")}
415
+
416
+ ${bold("Environment:")}
417
+ SNAPFAIL_APP_URL ${dim("Override dashboard URL (default: https://app.snapfail.com)")}
418
+ `);
419
+ }
420
+ var [, , cmd, ...rest] = process.argv;
421
+ if (!cmd || cmd === "--help" || cmd === "-h") {
422
+ printHelp();
423
+ } else if (cmd === "--version" || cmd === "-v") {
424
+ console.log(VERSION);
425
+ } else if (cmd === "init") {
426
+ const target = rest[0] ? resolve(rest[0]) : process.cwd();
427
+ cmdInit(target).catch((err) => {
428
+ fail(err instanceof Error ? err.message : String(err));
429
+ process.exit(1);
430
+ });
431
+ } else {
432
+ fail(`Unknown command: ${cmd}`);
433
+ printHelp();
23
434
  process.exit(1);
24
- });
435
+ }
package/package.json CHANGED
@@ -1,28 +1,22 @@
1
1
  {
2
2
  "name": "snapfail",
3
- "version": "0.0.2",
4
- "description": "CLI for Snapfail error monitoring for modern web apps",
5
- "license": "MIT",
6
- "keywords": ["snapfail", "cli", "error-monitoring", "astro"],
7
- "publishConfig": {
8
- "access": "public"
9
- },
3
+ "version": "0.0.7",
4
+ "description": "CLI to initialise SnapFail error-tracking in Vite & Astro projects",
10
5
  "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
11
8
  "bin": {
12
- "snapfail": "./dist/index.js"
9
+ "snapfail": "bin/snapfail.js"
13
10
  },
14
11
  "files": [
15
- "dist"
12
+ "dist",
13
+ "bin"
16
14
  ],
17
15
  "scripts": {
18
- "build": "tsup",
19
- "dev": "tsup --watch"
20
- },
21
- "dependencies": {
22
- "@clack/prompts": "^0.9.0"
16
+ "build": "bun build ./src/index.ts --outfile ./dist/index.js --target=node --sourcemap=none && bunx tsc --project tsconfig.json --declaration --emitDeclarationOnly --outDir dist --noEmit false",
17
+ "dev": "bun --watch ./src/index.ts"
23
18
  },
24
- "devDependencies": {
25
- "tsup": "^8.4.0",
26
- "typescript": "^6.0.3"
19
+ "engines": {
20
+ "node": ">=18"
27
21
  }
28
22
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Snapfail
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/README.md DELETED
@@ -1,52 +0,0 @@
1
- # snapfail
2
-
3
- CLI for [Snapfail](https://snapfail.dev) — error monitoring for modern web apps.
4
-
5
- Snapfail renders your pages in a real browser, diffs screenshots pixel-by-pixel, and asserts that critical DOM nodes are alive. Status pages lie. Pixels don't.
6
-
7
- ## Usage
8
-
9
- No installation required. Run directly with `npx`:
10
-
11
- ```bash
12
- npx snapfail astro init
13
- ```
14
-
15
- ### `snapfail astro init`
16
-
17
- Adds Snapfail to an existing Astro project:
18
-
19
- 1. Detects your `astro.config.*`
20
- 2. Detects your package manager (pnpm / yarn / bun / npm)
21
- 3. Installs `@snapfail/astro` and `@snapfail/sdk`
22
- 4. Injects the integration into your config automatically
23
-
24
- ```
25
- ◆ snapfail · astro init
26
-
27
- ● Found astro.config.mjs
28
-
29
- ◆ Beacon endpoint (DSN)
30
- │ /api/beacon
31
-
32
- ✔ Packages installed
33
- ✔ Integration added to astro.config.mjs
34
-
35
- ◆ Done! Run your dev server to start capturing errors.
36
- ```
37
-
38
- After running, your `astro.config.mjs` will look like this:
39
-
40
- ```js
41
- import { snapfail } from '@snapfail/astro'
42
-
43
- export default defineConfig({
44
- integrations: [
45
- snapfail({ dsn: '/api/beacon' }),
46
- ],
47
- })
48
- ```
49
-
50
- ## License
51
-
52
- MIT
@@ -1,120 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/commands/astro/init.ts
4
- import { intro, outro, text, spinner, log, isCancel, cancel, note } from "@clack/prompts";
5
-
6
- // src/utils/pm.ts
7
- import { existsSync } from "fs";
8
- import { join } from "path";
9
- import { exec } from "child_process";
10
- import { promisify } from "util";
11
- var execAsync = promisify(exec);
12
- function detectPM(cwd = process.cwd()) {
13
- if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
14
- if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
15
- if (existsSync(join(cwd, "bun.lockb"))) return "bun";
16
- return "npm";
17
- }
18
- async function install(pm, packages, cwd = process.cwd()) {
19
- const sub = pm === "npm" ? "install" : "add";
20
- await execAsync(`${pm} ${sub} ${packages.join(" ")}`, { cwd });
21
- }
22
-
23
- // src/utils/config.ts
24
- import { readFileSync, writeFileSync } from "fs";
25
- import { join as join2 } from "path";
26
- var IMPORT_LINE = `import { snapfail } from '@snapfail/astro';
27
- `;
28
- function injectAstroConfig(configFile, dsn, cwd = process.cwd()) {
29
- const filePath = join2(cwd, configFile);
30
- let src = readFileSync(filePath, "utf-8");
31
- if (src.includes("@snapfail/astro")) return true;
32
- const lastImportIdx = [...src.matchAll(/^import .+$/gm)].at(-1);
33
- if (lastImportIdx?.index != null) {
34
- const insertAt = lastImportIdx.index + lastImportIdx[0].length;
35
- src = src.slice(0, insertAt) + "\n" + IMPORT_LINE + src.slice(insertAt);
36
- } else {
37
- src = IMPORT_LINE + src;
38
- }
39
- if (/integrations\s*:\s*\[/.test(src)) {
40
- src = src.replace(
41
- /integrations\s*:\s*\[/,
42
- `integrations: [
43
- snapfail({ dsn: '${dsn}' }),`
44
- );
45
- writeFileSync(filePath, src);
46
- return true;
47
- }
48
- if (/defineConfig\s*\(\s*\{/.test(src)) {
49
- src = src.replace(
50
- /defineConfig\s*\(\s*\{/,
51
- `defineConfig({
52
- integrations: [snapfail({ dsn: '${dsn}' })],`
53
- );
54
- writeFileSync(filePath, src);
55
- return true;
56
- }
57
- return false;
58
- }
59
- function findAstroConfig(cwd = process.cwd()) {
60
- for (const name of ["astro.config.mjs", "astro.config.ts", "astro.config.js"]) {
61
- try {
62
- readFileSync(join2(cwd, name));
63
- return name;
64
- } catch {
65
- }
66
- }
67
- return null;
68
- }
69
-
70
- // src/commands/astro/init.ts
71
- async function init() {
72
- intro("snapfail \xB7 astro init");
73
- const configFile = findAstroConfig();
74
- if (!configFile) {
75
- log.error("No astro.config.* found. Run this inside an Astro project.");
76
- process.exit(1);
77
- }
78
- log.info(`Found ${configFile}`);
79
- const dsn = await text({
80
- message: "Beacon endpoint (DSN)",
81
- placeholder: "/api/beacon",
82
- initialValue: "/api/beacon",
83
- validate: (v) => !v.trim() ? "DSN cannot be empty" : void 0
84
- });
85
- if (isCancel(dsn)) {
86
- cancel("Cancelled.");
87
- process.exit(0);
88
- }
89
- const pm = detectPM();
90
- const s = spinner();
91
- s.start(`Installing @snapfail/astro and @snapfail/sdk via ${pm}`);
92
- try {
93
- await install(pm, ["@snapfail/astro", "@snapfail/sdk"]);
94
- s.stop("Packages installed");
95
- } catch (err) {
96
- s.stop("Installation failed");
97
- log.error(err.message);
98
- process.exit(1);
99
- }
100
- const injected = injectAstroConfig(configFile, dsn);
101
- if (injected) {
102
- log.success(`Integration added to ${configFile}`);
103
- } else {
104
- log.warn(`Could not auto-inject into ${configFile}.`);
105
- note(
106
- [
107
- `import { snapfail } from '@snapfail/astro'`,
108
- ``,
109
- `export default defineConfig({`,
110
- ` integrations: [snapfail({ dsn: '${dsn}' })],`,
111
- `})`
112
- ].join("\n"),
113
- "Add this manually"
114
- );
115
- }
116
- outro("Done! Run your dev server to start capturing errors.");
117
- }
118
- export {
119
- init
120
- };