@fixprompt/cli 0.2.1 → 0.4.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 +180 -57
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -87,11 +87,18 @@ 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";
94
- var TOKEN_ENV_VAR_NAME = "FIXPROMPT_DEPLOY_TOKEN";
94
+ var ENV_VAR_BY_SCOPE = {
95
+ deploy: "FIXPROMPT_DEPLOY_TOKEN",
96
+ read: "FIXPROMPT_READ_TOKEN"
97
+ };
98
+ var ENDPOINT_BY_SCOPE = {
99
+ deploy: "deploy-tokens",
100
+ read: "read-tokens"
101
+ };
95
102
  function fileExists(p) {
96
103
  try {
97
104
  return statSync(p).isFile();
@@ -123,8 +130,9 @@ function detectProvider(cwd) {
123
130
  }
124
131
  return null;
125
132
  }
126
- async function mintDeployToken(opts) {
127
- const url = `${opts.endpoint.replace(/\/$/, "")}/admin/projects/${opts.projectId}/deploy-tokens`;
133
+ async function mintToken(opts) {
134
+ const path = ENDPOINT_BY_SCOPE[opts.scope];
135
+ const url = `${opts.endpoint.replace(/\/$/, "")}/admin/projects/${opts.projectId}/${path}`;
128
136
  const res = await fetch(url, {
129
137
  method: "POST",
130
138
  headers: {
@@ -135,20 +143,21 @@ async function mintDeployToken(opts) {
135
143
  });
136
144
  const text = await res.text();
137
145
  if (!res.ok) {
138
- throw new Error(`broker /admin/projects/${opts.projectId}/deploy-tokens responded ${res.status}: ${text.slice(0, 200)}`);
146
+ throw new Error(
147
+ `broker /admin/projects/${opts.projectId}/${path} responded ${res.status}: ${text.slice(0, 200)}`
148
+ );
139
149
  }
140
150
  return JSON.parse(text);
141
151
  }
142
- function writeEas(token, env, force) {
143
- console.log(`
144
- \u2022 Writing ${TOKEN_ENV_VAR_NAME} to EAS env '${env}'\u2026`);
152
+ function writeEas(envVar, token, env, force) {
153
+ console.log(` \xB7 writing ${envVar} to EAS env '${env}'\u2026`);
145
154
  const useShell = process.platform === "win32";
146
155
  const args = [
147
156
  "eas-cli",
148
157
  "env:create",
149
158
  env,
150
159
  "--name",
151
- TOKEN_ENV_VAR_NAME,
160
+ envVar,
152
161
  "--value",
153
162
  token,
154
163
  "--type",
@@ -171,11 +180,10 @@ function writeEas(token, env, force) {
171
180
  );
172
181
  }
173
182
  }
174
- function writeGitHubActions(token, cwd) {
175
- console.log(`
176
- \u2022 Writing ${TOKEN_ENV_VAR_NAME} to GitHub Actions repo secrets\u2026`);
183
+ function writeGitHubActions(envVar, token, cwd) {
184
+ console.log(` \xB7 writing ${envVar} to GitHub Actions repo secrets\u2026`);
177
185
  const useShell = process.platform === "win32";
178
- const args = ["secret", "set", TOKEN_ENV_VAR_NAME, "--body", "-"];
186
+ const args = ["secret", "set", envVar, "--body", "-"];
179
187
  const r = spawnSync("gh", useShell ? args.map((a) => `"${a.replace(/"/g, '\\"')}"`) : args, {
180
188
  input: token,
181
189
  cwd,
@@ -190,7 +198,7 @@ function writeGitHubActions(token, cwd) {
190
198
  );
191
199
  }
192
200
  }
193
- async function writeVercel(token, target) {
201
+ async function writeVercel(envVar, token, target) {
194
202
  const vt = process.env.VERCEL_TOKEN;
195
203
  if (!vt) {
196
204
  throw new Error(
@@ -216,8 +224,7 @@ directory or pass --vercel-project-id <prj_\u2026>.`
216
224
  }
217
225
  }
218
226
  const targets = target.vercelTargets ?? ["production", "preview"];
219
- console.log(`
220
- \u2022 Writing ${TOKEN_ENV_VAR_NAME} to Vercel project ${projectId} (${targets.join(", ")})\u2026`);
227
+ console.log(` \xB7 writing ${envVar} to Vercel project ${projectId} (${targets.join(", ")})\u2026`);
221
228
  const res = await fetch(`https://api.vercel.com/v10/projects/${projectId}/env`, {
222
229
  method: "POST",
223
230
  headers: {
@@ -225,7 +232,7 @@ directory or pass --vercel-project-id <prj_\u2026>.`
225
232
  "Content-Type": "application/json"
226
233
  },
227
234
  body: JSON.stringify({
228
- key: TOKEN_ENV_VAR_NAME,
235
+ key: envVar,
229
236
  value: token,
230
237
  type: "plain",
231
238
  target: targets
@@ -236,8 +243,100 @@ directory or pass --vercel-project-id <prj_\u2026>.`
236
243
  throw new Error(`Vercel /env responded ${res.status}: ${text.slice(0, 200)}`);
237
244
  }
238
245
  }
246
+ function writeToProvider(envVar, token, provider, target, force, cwd) {
247
+ switch (provider) {
248
+ case "eas":
249
+ writeEas(envVar, token, target.easEnv, force);
250
+ return;
251
+ case "github-actions":
252
+ writeGitHubActions(envVar, token, cwd);
253
+ return;
254
+ case "vercel":
255
+ return writeVercel(envVar, token, target);
256
+ }
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 appendClaudeMdGuidance(cwd, projectId, projectSlug) {
292
+ const claudePath = join(cwd, "CLAUDE.md");
293
+ const section = [
294
+ "",
295
+ "## FixLoop \u2014 production error logs (auto-fetch on bug reports)",
296
+ "",
297
+ 'This project ships errors to FixLoop. When the user reports a bug, crash, error, "X is broken", or pastes an error stack:',
298
+ "",
299
+ "1. Read the FixLoop env vars from `.env.local` (gitignored \u2014 never commit).",
300
+ "2. `curl` the broker for recent error lines.",
301
+ "3. Parse the response `entries[]` \u2014 each has `ts`, `level`, `message`, optional `attrs` + `stack`. Match against the user's symptom.",
302
+ "4. Propose a fix grounded in what the logs show. **Do not ask the user to paste log lines \u2014 fetch them yourself.**",
303
+ "",
304
+ "### Fetch logs",
305
+ "",
306
+ "```sh",
307
+ 'curl -sH "x-fixprompt-read-token: $FIXPROMPT_READ_TOKEN" \\',
308
+ ' "$FIXPROMPT_BROKER_URL/projects/$FIXPROMPT_PROJECT_ID/logs?since=24h&level=error,warn,fatal&limit=200"',
309
+ "```",
310
+ "",
311
+ "Query params: `since` (15m..7d), `level` (csv: debug,info,warn,error,fatal), `q` (substring), `limit` (1..500).",
312
+ "",
313
+ projectSlug ? `**Project**: \`${projectSlug}\`.` : "",
314
+ `Manage / revoke: https://fixloop-dashboard.vercel.app/integrations/${projectId}`,
315
+ ""
316
+ ].filter(Boolean).join("\n");
317
+ if (existsSync(claudePath)) {
318
+ const existing = readFileSync(claudePath, "utf8");
319
+ if (existing.includes("## FixLoop \u2014")) return;
320
+ appendFileSync(claudePath, section);
321
+ } else {
322
+ writeFileSync(claudePath, "# Project\n" + section);
323
+ }
324
+ }
325
+ function writeLocalDiscovery(opts) {
326
+ console.log(` \xB7 writing .env.local + CLAUDE.md guidance to ${opts.cwd}\u2026`);
327
+ const giChanged = ensureGitignoreHas(opts.cwd, ".env.local");
328
+ if (giChanged) console.log(" + appended .env.local to .gitignore");
329
+ appendEnvLocal(opts.cwd, opts.token, opts.projectId, opts.projectSlug, opts.endpoint);
330
+ appendClaudeMdGuidance(opts.cwd, opts.projectId, opts.projectSlug);
331
+ }
239
332
  async function connect(args) {
240
333
  const cwd = process.cwd();
334
+ const scopeRaw = (args.flags.scope ?? "deploy").toLowerCase();
335
+ if (scopeRaw !== "deploy" && scopeRaw !== "read" && scopeRaw !== "both") {
336
+ console.error(`Invalid --scope '${scopeRaw}'. Allowed: deploy | read | both.`);
337
+ process.exit(1);
338
+ }
339
+ const scope = scopeRaw;
241
340
  const adminToken = args.flags["admin-token"] ?? process.env.FIXPROMPT_ADMIN_TOKEN ?? process.env.INTERNAL_ADMIN_TOKEN;
242
341
  if (!adminToken) {
243
342
  console.error(
@@ -247,7 +346,7 @@ async function connect(args) {
247
346
  }
248
347
  const endpoint = (args.flags.endpoint ?? process.env.FIXPROMPT_ENDPOINT ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
249
348
  let projectId = args.flags["project-id"] ?? process.env.FIXPROMPT_PROJECT_ID ?? null;
250
- const projectSlug = args.flags["project-slug"] ?? null;
349
+ let projectSlug = args.flags["project-slug"] ?? null;
251
350
  if (!projectId && projectSlug) {
252
351
  const res = await fetch(`${endpoint}/admin/projects/by-slug/${encodeURIComponent(projectSlug)}`, {
253
352
  headers: { "x-internal-admin-token": adminToken }
@@ -259,6 +358,7 @@ async function connect(args) {
259
358
  }
260
359
  const parsed = JSON.parse(text);
261
360
  projectId = parsed.id;
361
+ projectSlug = parsed.slug;
262
362
  console.log(` resolved --project-slug=${projectSlug} \u2192 ${parsed.id} (${parsed.name})`);
263
363
  }
264
364
  if (!projectId) {
@@ -268,8 +368,9 @@ async function connect(args) {
268
368
  process.exit(1);
269
369
  }
270
370
  const explicit = args.flags.provider;
271
- const provider = explicit ?? detectProvider(cwd);
272
- if (!provider) {
371
+ const detectedProvider = explicit ?? detectProvider(cwd);
372
+ const scopeNeedsProvider = scope !== "read";
373
+ if (scopeNeedsProvider && !detectedProvider) {
273
374
  console.error(
274
375
  `Couldn't detect a provider from ${cwd}. Pass --provider <eas|vercel|github-actions>.
275
376
  Detection looks for: app.json/eas.json (EAS), .vercel/project.json (Vercel),
@@ -277,57 +378,73 @@ async function connect(args) {
277
378
  );
278
379
  process.exit(1);
279
380
  }
381
+ const provider = detectedProvider;
280
382
  const target = {
281
- provider,
383
+ provider: provider ?? "eas",
384
+ // unused when provider is null; placeholder
282
385
  easEnv: args.flags["eas-env"] ?? "production",
283
386
  vercelProjectId: args.flags["vercel-project-id"] ?? void 0,
284
387
  vercelTargets: typeof args.flags["vercel-targets"] === "string" ? args.flags["vercel-targets"].split(",").map((t) => t.trim()) : void 0
285
388
  };
286
- const label = args.flags.label ?? `${provider}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
389
+ const labelBase = args.flags.label ?? `${provider}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
287
390
  console.log(`fixprompt connect`);
288
- console.log(` provider : ${provider}`);
391
+ console.log(` scope : ${scope}`);
392
+ console.log(` provider : ${provider ?? "(none \u2014 read-only, writing local files)"}`);
289
393
  console.log(` project_id : ${projectId}`);
290
394
  console.log(` endpoint : ${endpoint}`);
291
- console.log(` label : ${label}`);
395
+ console.log(` label : ${labelBase}`);
292
396
  if (provider === "eas") console.log(` eas-env : ${target.easEnv}`);
293
- console.log(`
294
- \u2022 Minting deploy token via broker\u2026`);
295
- const minted = await mintDeployToken({
296
- endpoint,
297
- adminToken,
298
- projectId,
299
- label
300
- });
301
- console.log(` \u2713 token_id ${minted.token_id} (value hidden \u2014 written to CI store only)`);
302
397
  const force = args.flags.force === true;
303
- switch (provider) {
304
- case "eas":
305
- writeEas(minted.token, target.easEnv, force);
306
- break;
307
- case "github-actions":
308
- writeGitHubActions(minted.token, cwd);
309
- break;
310
- case "vercel":
311
- await writeVercel(minted.token, target);
312
- break;
398
+ const scopes = scope === "both" ? ["deploy", "read"] : [scope];
399
+ const mintedTokens = [];
400
+ for (const s of scopes) {
401
+ const envVar = ENV_VAR_BY_SCOPE[s];
402
+ const label = scope === "both" ? `${labelBase}-${s}` : labelBase;
403
+ console.log(`
404
+ \u2022 Minting ${s} token via broker\u2026`);
405
+ const minted = await mintToken({ endpoint, adminToken, projectId, label, scope: s });
406
+ console.log(` \u2713 token_id ${minted.token_id}`);
407
+ if (s === "read") {
408
+ writeLocalDiscovery({
409
+ cwd,
410
+ token: minted.token,
411
+ projectId,
412
+ projectSlug,
413
+ endpoint
414
+ });
415
+ } else if (provider) {
416
+ await writeToProvider(envVar, minted.token, provider, target, force, cwd);
417
+ }
418
+ mintedTokens.push({ scope: s, token_id: minted.token_id, envVar });
313
419
  }
314
- console.log(`
315
- \u2713 ${TOKEN_ENV_VAR_NAME} is now set on ${provider}.`);
316
- console.log(` Next deploy can run with no shell paste:`);
317
- if (provider === "eas") {
318
- console.log(` npm run release:ota --branch=production --message="..."`);
319
- } else if (provider === "github-actions") {
320
- console.log(` Reference \${{ secrets.${TOKEN_ENV_VAR_NAME} }} in your workflow.`);
321
- } else {
322
- console.log(` The token is in your Vercel project env for the next deploy.`);
420
+ const readMinted = mintedTokens.some((m) => m.scope === "read");
421
+ const deployMinted = mintedTokens.some((m) => m.scope === "deploy");
422
+ console.log("");
423
+ if (deployMinted && provider) {
424
+ console.log(`\u2713 FIXPROMPT_DEPLOY_TOKEN set on ${provider}.`);
425
+ if (provider === "eas") {
426
+ console.log(` Next OTA: npm run release:ota --branch=production --message="..."`);
427
+ } else if (provider === "github-actions") {
428
+ console.log(` Reference \${{ secrets.FIXPROMPT_DEPLOY_TOKEN }} in your workflow.`);
429
+ } else {
430
+ console.log(` The deploy token is in your Vercel project env.`);
431
+ }
432
+ }
433
+ if (readMinted) {
434
+ console.log(`\u2713 FIXPROMPT_READ_TOKEN written to .env.local; CLAUDE.md updated.`);
435
+ console.log(` Coding agents (Claude Code / Cursor / Copilot) opening this repo will see`);
436
+ console.log(` the FixLoop section in CLAUDE.md and know to fetch logs on bug reports.`);
323
437
  }
324
438
  console.log(`
325
- To revoke later: dashboard \u2192 Integrations \u2192 Deploy tokens \u2192 ${minted.token_id}.`);
439
+ Revoke any of these from the dashboard \u2192 Integrations.`);
440
+ for (const m of mintedTokens) {
441
+ console.log(` ${m.scope}: ${m.token_id}`);
442
+ }
326
443
  }
327
444
 
328
445
  // src/version.ts
329
446
  var CLI_NAME = "@fixprompt/cli";
330
- var CLI_VERSION = "0.2.1";
447
+ var CLI_VERSION = "0.4.0";
331
448
 
332
449
  // src/cli.ts
333
450
  var DEFAULT_ENDPOINT2 = "https://geosloghub-production.up.railway.app";
@@ -366,15 +483,21 @@ Usage:
366
483
  fixprompt help
367
484
 
368
485
  connect options:
369
- --project-id <uuid> FixPrompt project id (or $FIXPROMPT_PROJECT_ID)
486
+ --scope <s> deploy | read | both (default: deploy)
487
+ deploy \u2192 mints fpd_\u2026, writes FIXPROMPT_DEPLOY_TOKEN
488
+ read \u2192 mints fpr_\u2026, writes FIXPROMPT_READ_TOKEN
489
+ (lets a customer's coding agent fetch
490
+ production logs from FixLoop directly)
491
+ both \u2192 does both in one pass
492
+ --project-id <uuid> FixLoop project id (or $FIXPROMPT_PROJECT_ID)
493
+ --project-slug <slug> Look up project_id by slug (no UUID needed)
370
494
  --admin-token <fpa_\u2026> Broker admin token (or $FIXPROMPT_ADMIN_TOKEN)
371
495
  --provider <name> eas | vercel | github-actions (auto-detected from cwd)
372
496
  --eas-env <env> EAS env to write to (default: production)
373
497
  --vercel-project-id <id> Override Vercel project id (default: .vercel/project.json)
374
498
  --vercel-targets <list> Comma-separated (default: production,preview)
375
499
  --label <text> Token label (default: <provider>-<timestamp>)
376
- --force Overwrite an existing FIXPROMPT_DEPLOY_TOKEN value
377
- --project-slug <slug> Look up project_id by slug (no UUID needed)
500
+ --force Overwrite an existing env var value
378
501
 
379
502
  deploy-start auth (pick one):
380
503
  --deploy-token <fpd_...> Deploy-only token from dashboard (recommended for CI)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fixprompt/cli",
3
- "version": "0.2.1",
3
+ "version": "0.4.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": {