snapfail 0.0.1 → 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,33 +1,22 @@
1
1
  {
2
2
  "name": "snapfail",
3
- "version": "0.0.1",
4
- "description": "CLI for Snapfail error monitoring for modern web apps",
5
- "license": "MIT",
6
- "keywords": [
7
- "snapfail",
8
- "cli",
9
- "error-monitoring",
10
- "astro"
11
- ],
12
- "publishConfig": {
13
- "access": "public"
14
- },
3
+ "version": "0.0.7",
4
+ "description": "CLI to initialise SnapFail error-tracking in Vite & Astro projects",
15
5
  "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
16
8
  "bin": {
17
- "snapfail": "./dist/index.js"
9
+ "snapfail": "bin/snapfail.js"
18
10
  },
19
11
  "files": [
20
- "dist"
12
+ "dist",
13
+ "bin"
21
14
  ],
22
- "dependencies": {
23
- "@clack/prompts": "^0.9.0"
24
- },
25
- "devDependencies": {
26
- "tsup": "^8.4.0",
27
- "typescript": "^6.0.3"
28
- },
29
15
  "scripts": {
30
- "build": "tsup",
31
- "dev": "tsup --watch"
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"
18
+ },
19
+ "engines": {
20
+ "node": ">=18"
32
21
  }
33
- }
22
+ }
@@ -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
- };