@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.
- package/dist/cli.js +180 -57
- 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
|
|
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
|
|
127
|
-
const
|
|
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(
|
|
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
|
-
|
|
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",
|
|
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:
|
|
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
|
-
|
|
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
|
|
272
|
-
|
|
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
|
|
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(`
|
|
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 : ${
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
console.log(
|
|
317
|
-
if (
|
|
318
|
-
console.log(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
--
|
|
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
|
|
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