@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.
- package/dist/cli.js +208 -14
- 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
|
-
|
|
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
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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 (
|
|
344
|
-
console.log(
|
|
345
|
-
console.log(`
|
|
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.
|
|
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