@fixprompt/cli 0.3.0 → 0.5.0

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/cli.js +208 -14
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -87,7 +87,7 @@ function detectContext() {
87
87
  }
88
88
 
89
89
  // src/connect.ts
90
- import { readFileSync, readdirSync, statSync } from "fs";
90
+ import { appendFileSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
91
91
  import { join } from "path";
92
92
  import { spawnSync } from "child_process";
93
93
  var DEFAULT_ENDPOINT = "https://geosloghub-production.up.railway.app";
@@ -255,6 +255,183 @@ function writeToProvider(envVar, token, provider, target, force, cwd) {
255
255
  return writeVercel(envVar, token, target);
256
256
  }
257
257
  }
258
+ function ensureGitignoreHas(cwd, pattern) {
259
+ const gi = join(cwd, ".gitignore");
260
+ let s = "";
261
+ try {
262
+ s = readFileSync(gi, "utf8");
263
+ } catch {
264
+ }
265
+ const lines = s.split(/\r?\n/);
266
+ if (lines.some((l) => l.trim() === pattern)) return false;
267
+ const sep = s.length === 0 || s.endsWith("\n") ? "" : "\n";
268
+ writeFileSync(gi, s + sep + "\n# FixLoop\n" + pattern + "\n");
269
+ return true;
270
+ }
271
+ function appendEnvLocal(cwd, token, projectId, projectSlug, endpoint) {
272
+ const envPath = join(cwd, ".env.local");
273
+ const lines = [
274
+ "",
275
+ "# FixLoop \u2014 read token for the local coding agent. Gitignored.",
276
+ `# Generated ${(/* @__PURE__ */ new Date()).toISOString()}`,
277
+ `# Manage / revoke: https://fixloop-dashboard.vercel.app/integrations/${projectId}`,
278
+ `FIXPROMPT_PROJECT_ID=${projectId}`
279
+ ];
280
+ if (projectSlug) lines.push(`FIXPROMPT_PROJECT_SLUG=${projectSlug}`);
281
+ lines.push(`FIXPROMPT_READ_TOKEN=${token}`);
282
+ lines.push(`FIXPROMPT_BROKER_URL=${endpoint}`);
283
+ lines.push("");
284
+ const block = lines.join("\n");
285
+ if (existsSync(envPath)) {
286
+ appendFileSync(envPath, block);
287
+ } else {
288
+ writeFileSync(envPath, block.replace(/^\n/, ""));
289
+ }
290
+ }
291
+ function detectRuntime(cwd) {
292
+ const pj = join(cwd, "package.json");
293
+ if (!existsSync(pj)) return "unknown";
294
+ try {
295
+ const j = JSON.parse(readFileSync(pj, "utf8"));
296
+ const deps = { ...j.dependencies || {}, ...j.devDependencies || {} };
297
+ if (deps["react-native"] || deps["expo"]) return "react-native";
298
+ if (deps["next"]) return "next";
299
+ if (deps["react"] || deps["vite"] || deps["react-scripts"]) return "browser";
300
+ if (deps["@nestjs/core"] || deps["express"] || deps["fastify"]) return "node";
301
+ } catch {
302
+ }
303
+ return "unknown";
304
+ }
305
+ var FIXLOOP_SECTION_MARKER = "<!-- fixloop:claude-section -->";
306
+ function buildClaudeMdSection(projectId, projectSlug, runtime) {
307
+ const sdkRows = {
308
+ "react-native": {
309
+ pkg: "@fixprompt/react-native",
310
+ entry: "App.tsx / index.js \u2014 call before the first render",
311
+ example: "import { initFixPrompt } from '@fixprompt/react-native';\ninitFixPrompt({\n projectKey: process.env.EXPO_PUBLIC_FIXPROMPT_KEY!,\n source: '<slug>-prod',\n service: '<slug>',\n env: 'prod',\n});"
312
+ },
313
+ next: {
314
+ pkg: "@fixprompt/browser (client) + @fixprompt/node (server, github:goscha01/geos-loghub-client until @fixprompt/node ships)",
315
+ entry: "src/app/providers.tsx for client (useEffect-init), instrumentation.ts or server-start for Node",
316
+ example: `// client (in a "use client" component, useEffect):
317
+ import { initFixPrompt } from '@fixprompt/browser';
318
+ initFixPrompt({ projectKey: process.env.NEXT_PUBLIC_FIXPROMPT_KEY!, source: '<slug>-prod', service: '<slug>', env: 'prod' });`
319
+ },
320
+ browser: {
321
+ pkg: "@fixprompt/browser",
322
+ entry: "src/main.{ts,tsx} / index.{js,tsx} \u2014 top of file, before render",
323
+ example: "import { initFixPrompt } from '@fixprompt/browser';\ninitFixPrompt({\n projectKey: import.meta.env.VITE_FIXPROMPT_KEY, // or REACT_APP_FIXPROMPT_KEY for CRA\n source: '<slug>-prod',\n service: '<slug>',\n env: import.meta.env.PROD ? 'prod' : 'dev',\n});"
324
+ },
325
+ node: {
326
+ pkg: "@geos/loghub-client (npm install @geos/loghub-client@github:goscha01/geos-loghub-client \u2014 @fixprompt/node not yet on npm)",
327
+ entry: "src/index.{ts,js} / src/main.{ts,js} \u2014 at server start, before route registration",
328
+ example: "const { loghubLog } = require('@geos/loghub-client');\n// per-request middleware:\napp.use((req, res, next) => {\n res.on('finish', () => loghubLog({ service: '<slug>', app: '<slug>', env: 'prod', level: res.statusCode >= 500 ? 'error' : 'info', message: 'http_request', attrs: { method: req.method, path: req.originalUrl, status: res.statusCode } }));\n next();\n});"
329
+ }
330
+ };
331
+ const sdk = runtime === "unknown" ? {
332
+ pkg: "@fixprompt/browser | @fixprompt/react-native | @geos/loghub-client (Node) \u2014 pick by stack",
333
+ entry: "(read package.json to determine runtime)",
334
+ example: "// see https://fixloop-dashboard.vercel.app/onboarding for runtime-specific snippets"
335
+ } : sdkRows[runtime];
336
+ const slugLine = projectSlug ? `\`${projectSlug}\`` : "(see .env.local)";
337
+ const sdkExampleWithSlug = sdk.example.replace(/<slug>/g, projectSlug ?? "<slug>");
338
+ return [
339
+ "",
340
+ FIXLOOP_SECTION_MARKER,
341
+ "## FixLoop \u2014 production error logs (auto-fetch on bug reports + dev loop)",
342
+ "",
343
+ `This project is wired to FixLoop. Two behaviors expected of the coding agent reading this file:`,
344
+ "",
345
+ "### 1. Install the SDK on first use (skip if already in `package.json`)",
346
+ "",
347
+ `Detected runtime: **${runtime}**. Recommended SDK: \`${sdk.pkg}\``,
348
+ "",
349
+ `- Entry point: ${sdk.entry}`,
350
+ `- Init snippet:`,
351
+ "",
352
+ "```ts",
353
+ sdkExampleWithSlug,
354
+ "```",
355
+ "",
356
+ "After install, the SDK ships every uncaught error to the broker \u2192 Grafana Loki under `service_name=" + (projectSlug ?? "<slug>") + "`. Subsequent debug runs read these errors back via the curl in step 2.",
357
+ "",
358
+ "### 2. Debug loop on bug reports",
359
+ "",
360
+ `Trigger phrases: "X is broken", "doesn't work", "crashes", "is failing", pasted error stacks, "users are reporting Y", TestFlight/App Store bug reports.`,
361
+ "",
362
+ "Loop until the log query returns no new matches:",
363
+ "",
364
+ "1. **Fetch recent errors** \u2014 env vars come from `.env.local` (gitignored \u2014 never commit):",
365
+ " ```sh",
366
+ ' curl -sH "x-fixprompt-read-token: $FIXPROMPT_READ_TOKEN" \\',
367
+ ' "$FIXPROMPT_BROKER_URL/projects/$FIXPROMPT_PROJECT_ID/logs?since=24h&level=error,warn,fatal&limit=200"',
368
+ " ```",
369
+ " Query params: `since` (15m..7d), `level` (csv), `q` (substring), `limit` (1..500).",
370
+ "",
371
+ '2. **Parse `entries[]`** \u2014 each has `ts`, `level`, `message`, optional `attrs` + `stack`. Find the entries matching the user\'s symptom. Cite specific lines in your response (e.g. *"the `Cannot read property foo of null` at -200ms vs `[Storage] read failed` at -240ms suggests AsyncStorage is racing the migration"*).',
372
+ "",
373
+ "3. **Apply a fix** grounded in what the logs show. **Do not ask the user to paste log lines \u2014 fetch them yourself.**",
374
+ "",
375
+ "4. **Verify locally** \u2014 run the build / dev server / tests so the fix compiles and the affected path no longer throws.",
376
+ "",
377
+ "5. **Re-fetch with `since=5m`** after deploy. If the same error still appears OR new errors appear, **loop back to step 2** with the latest entries as context. Stop when the query returns no new matches against the user's symptom.",
378
+ "",
379
+ "### Project",
380
+ "",
381
+ `- Slug: ${slugLine}`,
382
+ `- Dashboard: https://fixloop-dashboard.vercel.app/issues/${projectId}`,
383
+ `- Manage / revoke the read token: https://fixloop-dashboard.vercel.app/integrations/${projectId}`,
384
+ ""
385
+ ].join("\n");
386
+ }
387
+ function stripFixloopSection(body) {
388
+ const lines = body.split("\n");
389
+ let start = -1;
390
+ for (let i = 0; i < lines.length; i++) {
391
+ if (lines[i].trim() === FIXLOOP_SECTION_MARKER) {
392
+ start = i;
393
+ break;
394
+ }
395
+ if (lines[i].startsWith("## FixLoop \u2014")) {
396
+ start = i;
397
+ break;
398
+ }
399
+ }
400
+ if (start === -1) return body;
401
+ const realStart = start > 0 && lines[start - 1] === "" ? start - 1 : start;
402
+ let end = lines.length;
403
+ for (let i = start + 1; i < lines.length; i++) {
404
+ if (lines[i].startsWith("## ") && !lines[i].startsWith("## FixLoop \u2014")) {
405
+ end = i;
406
+ break;
407
+ }
408
+ }
409
+ const trimmedTail = lines.slice(end);
410
+ while (trimmedTail.length > 0 && realStart > 0 && lines[realStart - 1] === "" && trimmedTail[0] === "") {
411
+ trimmedTail.shift();
412
+ }
413
+ return [...lines.slice(0, realStart), ...trimmedTail].join("\n");
414
+ }
415
+ function appendClaudeMdGuidance(cwd, projectId, projectSlug) {
416
+ const claudePath = join(cwd, "CLAUDE.md");
417
+ const runtime = detectRuntime(cwd);
418
+ const section = buildClaudeMdSection(projectId, projectSlug, runtime);
419
+ if (existsSync(claudePath)) {
420
+ const existing = readFileSync(claudePath, "utf8");
421
+ const stripped = stripFixloopSection(existing);
422
+ const next = stripped.endsWith("\n") ? stripped + section.replace(/^\n/, "") : stripped + section;
423
+ writeFileSync(claudePath, next);
424
+ } else {
425
+ writeFileSync(claudePath, "# Project\n" + section);
426
+ }
427
+ }
428
+ function writeLocalDiscovery(opts) {
429
+ console.log(` \xB7 writing .env.local + CLAUDE.md guidance to ${opts.cwd}\u2026`);
430
+ const giChanged = ensureGitignoreHas(opts.cwd, ".env.local");
431
+ if (giChanged) console.log(" + appended .env.local to .gitignore");
432
+ appendEnvLocal(opts.cwd, opts.token, opts.projectId, opts.projectSlug, opts.endpoint);
433
+ appendClaudeMdGuidance(opts.cwd, opts.projectId, opts.projectSlug);
434
+ }
258
435
  async function connect(args) {
259
436
  const cwd = process.cwd();
260
437
  const scopeRaw = (args.flags.scope ?? "deploy").toLowerCase();
@@ -272,7 +449,7 @@ async function connect(args) {
272
449
  }
273
450
  const endpoint = (args.flags.endpoint ?? process.env.FIXPROMPT_ENDPOINT ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
274
451
  let projectId = args.flags["project-id"] ?? process.env.FIXPROMPT_PROJECT_ID ?? null;
275
- const projectSlug = args.flags["project-slug"] ?? null;
452
+ let projectSlug = args.flags["project-slug"] ?? null;
276
453
  if (!projectId && projectSlug) {
277
454
  const res = await fetch(`${endpoint}/admin/projects/by-slug/${encodeURIComponent(projectSlug)}`, {
278
455
  headers: { "x-internal-admin-token": adminToken }
@@ -284,6 +461,7 @@ async function connect(args) {
284
461
  }
285
462
  const parsed = JSON.parse(text);
286
463
  projectId = parsed.id;
464
+ projectSlug = parsed.slug;
287
465
  console.log(` resolved --project-slug=${projectSlug} \u2192 ${parsed.id} (${parsed.name})`);
288
466
  }
289
467
  if (!projectId) {
@@ -293,8 +471,9 @@ async function connect(args) {
293
471
  process.exit(1);
294
472
  }
295
473
  const explicit = args.flags.provider;
296
- const provider = explicit ?? detectProvider(cwd);
297
- if (!provider) {
474
+ const detectedProvider = explicit ?? detectProvider(cwd);
475
+ const scopeNeedsProvider = scope !== "read";
476
+ if (scopeNeedsProvider && !detectedProvider) {
298
477
  console.error(
299
478
  `Couldn't detect a provider from ${cwd}. Pass --provider <eas|vercel|github-actions>.
300
479
  Detection looks for: app.json/eas.json (EAS), .vercel/project.json (Vercel),
@@ -302,8 +481,10 @@ async function connect(args) {
302
481
  );
303
482
  process.exit(1);
304
483
  }
484
+ const provider = detectedProvider;
305
485
  const target = {
306
- provider,
486
+ provider: provider ?? "eas",
487
+ // unused when provider is null; placeholder
307
488
  easEnv: args.flags["eas-env"] ?? "production",
308
489
  vercelProjectId: args.flags["vercel-project-id"] ?? void 0,
309
490
  vercelTargets: typeof args.flags["vercel-targets"] === "string" ? args.flags["vercel-targets"].split(",").map((t) => t.trim()) : void 0
@@ -311,7 +492,7 @@ async function connect(args) {
311
492
  const labelBase = args.flags.label ?? `${provider}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
312
493
  console.log(`fixprompt connect`);
313
494
  console.log(` scope : ${scope}`);
314
- console.log(` provider : ${provider}`);
495
+ console.log(` provider : ${provider ?? "(none \u2014 read-only, writing local files)"}`);
315
496
  console.log(` project_id : ${projectId}`);
316
497
  console.log(` endpoint : ${endpoint}`);
317
498
  console.log(` label : ${labelBase}`);
@@ -326,12 +507,24 @@ async function connect(args) {
326
507
  \u2022 Minting ${s} token via broker\u2026`);
327
508
  const minted = await mintToken({ endpoint, adminToken, projectId, label, scope: s });
328
509
  console.log(` \u2713 token_id ${minted.token_id}`);
329
- await writeToProvider(envVar, minted.token, provider, target, force, cwd);
510
+ if (s === "read") {
511
+ writeLocalDiscovery({
512
+ cwd,
513
+ token: minted.token,
514
+ projectId,
515
+ projectSlug,
516
+ endpoint
517
+ });
518
+ } else if (provider) {
519
+ await writeToProvider(envVar, minted.token, provider, target, force, cwd);
520
+ }
330
521
  mintedTokens.push({ scope: s, token_id: minted.token_id, envVar });
331
522
  }
332
- console.log(`
333
- \u2713 ${mintedTokens.map((m) => m.envVar).join(" + ")} now set on ${provider}.`);
334
- if (mintedTokens.some((m) => m.scope === "deploy")) {
523
+ const readMinted = mintedTokens.some((m) => m.scope === "read");
524
+ const deployMinted = mintedTokens.some((m) => m.scope === "deploy");
525
+ console.log("");
526
+ if (deployMinted && provider) {
527
+ console.log(`\u2713 FIXPROMPT_DEPLOY_TOKEN set on ${provider}.`);
335
528
  if (provider === "eas") {
336
529
  console.log(` Next OTA: npm run release:ota --branch=production --message="..."`);
337
530
  } else if (provider === "github-actions") {
@@ -340,9 +533,10 @@ async function connect(args) {
340
533
  console.log(` The deploy token is in your Vercel project env.`);
341
534
  }
342
535
  }
343
- if (mintedTokens.some((m) => m.scope === "read")) {
344
- console.log(` Customer-side agents can now fetch logs via FIXPROMPT_READ_TOKEN \u2014`);
345
- console.log(` see the fixprompt-debug skill at fixloop-dashboard.vercel.app/claude-skill.md.`);
536
+ if (readMinted) {
537
+ console.log(`\u2713 FIXPROMPT_READ_TOKEN written to .env.local; CLAUDE.md updated.`);
538
+ console.log(` Coding agents (Claude Code / Cursor / Copilot) opening this repo will see`);
539
+ console.log(` the FixLoop section in CLAUDE.md and know to fetch logs on bug reports.`);
346
540
  }
347
541
  console.log(`
348
542
  Revoke any of these from the dashboard \u2192 Integrations.`);
@@ -353,7 +547,7 @@ async function connect(args) {
353
547
 
354
548
  // src/version.ts
355
549
  var CLI_NAME = "@fixprompt/cli";
356
- var CLI_VERSION = "0.3.0";
550
+ var CLI_VERSION = "0.5.0";
357
551
 
358
552
  // src/cli.ts
359
553
  var DEFAULT_ENDPOINT2 = "https://geosloghub-production.up.railway.app";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fixprompt/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "FixPrompt CLI — annotate deployments and ship them to the broker so the dashboard knows what changed.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {