@fixprompt/cli 0.4.0 → 0.6.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 +645 -50
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -87,9 +87,362 @@ function detectContext() {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// src/connect.ts
|
|
90
|
-
import { appendFileSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
91
|
-
import { join } from "path";
|
|
90
|
+
import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
91
|
+
import { join as join2 } from "path";
|
|
92
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
93
|
+
|
|
94
|
+
// src/install-sdk.ts
|
|
95
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
96
|
+
import { dirname, join } from "path";
|
|
92
97
|
import { spawnSync } from "child_process";
|
|
98
|
+
function readPackageJson(cwd) {
|
|
99
|
+
const p = join(cwd, "package.json");
|
|
100
|
+
if (!existsSync(p)) return null;
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function firstExisting(cwd, candidates) {
|
|
108
|
+
for (const c of candidates) {
|
|
109
|
+
const full = join(cwd, c);
|
|
110
|
+
if (existsSync(full)) return full;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function detectPackageManager(cwd) {
|
|
115
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
116
|
+
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
117
|
+
if (existsSync(join(cwd, "bun.lockb"))) return "bun";
|
|
118
|
+
return "npm";
|
|
119
|
+
}
|
|
120
|
+
function detectStack(cwd) {
|
|
121
|
+
const pj = readPackageJson(cwd);
|
|
122
|
+
const deps = { ...pj?.dependencies ?? {}, ...pj?.devDependencies ?? {} };
|
|
123
|
+
const has = (n) => Object.prototype.hasOwnProperty.call(deps, n);
|
|
124
|
+
const packageManager = detectPackageManager(cwd);
|
|
125
|
+
if (has("react-native") || has("expo")) {
|
|
126
|
+
return {
|
|
127
|
+
runtime: "react-native",
|
|
128
|
+
packageManager,
|
|
129
|
+
sdkPackage: "@fixprompt/react-native",
|
|
130
|
+
envVarName: "EXPO_PUBLIC_FIXPROMPT_KEY",
|
|
131
|
+
entryFile: firstExisting(cwd, [
|
|
132
|
+
"App.tsx",
|
|
133
|
+
"App.jsx",
|
|
134
|
+
"App.ts",
|
|
135
|
+
"App.js",
|
|
136
|
+
"src/App.tsx",
|
|
137
|
+
"src/App.jsx",
|
|
138
|
+
"src/App.ts",
|
|
139
|
+
"src/App.js",
|
|
140
|
+
"index.tsx",
|
|
141
|
+
"index.jsx",
|
|
142
|
+
"index.ts",
|
|
143
|
+
"index.js"
|
|
144
|
+
])
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (has("next")) {
|
|
148
|
+
const appLayout = firstExisting(cwd, [
|
|
149
|
+
"app/layout.tsx",
|
|
150
|
+
"app/layout.jsx",
|
|
151
|
+
"app/layout.ts",
|
|
152
|
+
"app/layout.js",
|
|
153
|
+
"src/app/layout.tsx",
|
|
154
|
+
"src/app/layout.jsx",
|
|
155
|
+
"src/app/layout.ts",
|
|
156
|
+
"src/app/layout.js"
|
|
157
|
+
]);
|
|
158
|
+
if (appLayout) {
|
|
159
|
+
return {
|
|
160
|
+
runtime: "next-app",
|
|
161
|
+
packageManager,
|
|
162
|
+
sdkPackage: "@fixprompt/browser",
|
|
163
|
+
envVarName: "NEXT_PUBLIC_FIXPROMPT_KEY",
|
|
164
|
+
entryFile: appLayout
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const pagesApp = firstExisting(cwd, [
|
|
168
|
+
"pages/_app.tsx",
|
|
169
|
+
"pages/_app.jsx",
|
|
170
|
+
"pages/_app.ts",
|
|
171
|
+
"pages/_app.js",
|
|
172
|
+
"src/pages/_app.tsx",
|
|
173
|
+
"src/pages/_app.jsx",
|
|
174
|
+
"src/pages/_app.ts",
|
|
175
|
+
"src/pages/_app.js"
|
|
176
|
+
]);
|
|
177
|
+
return {
|
|
178
|
+
runtime: "next-pages",
|
|
179
|
+
packageManager,
|
|
180
|
+
sdkPackage: "@fixprompt/browser",
|
|
181
|
+
envVarName: "NEXT_PUBLIC_FIXPROMPT_KEY",
|
|
182
|
+
entryFile: pagesApp
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (has("vite")) {
|
|
186
|
+
return {
|
|
187
|
+
runtime: "vite",
|
|
188
|
+
packageManager,
|
|
189
|
+
sdkPackage: "@fixprompt/browser",
|
|
190
|
+
envVarName: "VITE_FIXPROMPT_KEY",
|
|
191
|
+
entryFile: firstExisting(cwd, [
|
|
192
|
+
"src/main.tsx",
|
|
193
|
+
"src/main.jsx",
|
|
194
|
+
"src/main.ts",
|
|
195
|
+
"src/main.js"
|
|
196
|
+
])
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (has("react-scripts")) {
|
|
200
|
+
return {
|
|
201
|
+
runtime: "cra",
|
|
202
|
+
packageManager,
|
|
203
|
+
sdkPackage: "@fixprompt/browser",
|
|
204
|
+
envVarName: "REACT_APP_FIXPROMPT_KEY",
|
|
205
|
+
entryFile: firstExisting(cwd, [
|
|
206
|
+
"src/index.tsx",
|
|
207
|
+
"src/index.jsx",
|
|
208
|
+
"src/index.ts",
|
|
209
|
+
"src/index.js"
|
|
210
|
+
])
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (has("react") || has("@vitejs/plugin-react")) {
|
|
214
|
+
return {
|
|
215
|
+
runtime: "browser",
|
|
216
|
+
packageManager,
|
|
217
|
+
sdkPackage: "@fixprompt/browser",
|
|
218
|
+
envVarName: "VITE_FIXPROMPT_KEY",
|
|
219
|
+
entryFile: firstExisting(cwd, [
|
|
220
|
+
"src/main.tsx",
|
|
221
|
+
"src/main.jsx",
|
|
222
|
+
"src/index.tsx",
|
|
223
|
+
"src/index.jsx"
|
|
224
|
+
])
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (has("express") || has("@nestjs/core") || has("fastify") || pj?.main) {
|
|
228
|
+
return {
|
|
229
|
+
runtime: "node",
|
|
230
|
+
packageManager,
|
|
231
|
+
// @fixprompt/node isn't published yet — fall back to the legacy
|
|
232
|
+
// GitHub install which is still what every existing backend uses.
|
|
233
|
+
sdkPackage: "@geos/loghub-client@github:goscha01/geos-loghub-client",
|
|
234
|
+
envVarName: "LOGHUB_KEY",
|
|
235
|
+
entryFile: null
|
|
236
|
+
// legacy client has no init() to inject
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
runtime: null,
|
|
241
|
+
packageManager,
|
|
242
|
+
sdkPackage: "",
|
|
243
|
+
envVarName: "",
|
|
244
|
+
entryFile: null
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function installSdk(cwd, stack) {
|
|
248
|
+
if (!stack.sdkPackage) return { skipped: true, reason: "no SDK for this stack" };
|
|
249
|
+
const pj = readPackageJson(cwd);
|
|
250
|
+
const deps = { ...pj?.dependencies ?? {}, ...pj?.devDependencies ?? {} };
|
|
251
|
+
const bareName = stack.sdkPackage.split("@github:")[0].replace(/@\^?[\d.]+$/, "");
|
|
252
|
+
if (Object.prototype.hasOwnProperty.call(deps, bareName)) {
|
|
253
|
+
return { skipped: true, reason: `${bareName} already in package.json` };
|
|
254
|
+
}
|
|
255
|
+
const useShell = process.platform === "win32";
|
|
256
|
+
const pm = stack.packageManager;
|
|
257
|
+
const args = pm === "yarn" ? ["add", stack.sdkPackage] : pm === "pnpm" ? ["add", stack.sdkPackage] : pm === "bun" ? ["add", stack.sdkPackage] : ["install", stack.sdkPackage];
|
|
258
|
+
console.log(` \xB7 running ${pm} ${args.join(" ")}\u2026`);
|
|
259
|
+
const r = spawnSync(
|
|
260
|
+
pm,
|
|
261
|
+
useShell ? args.map((a) => `"${a.replace(/"/g, '\\"')}"`) : args,
|
|
262
|
+
{ cwd, stdio: "inherit", shell: useShell }
|
|
263
|
+
);
|
|
264
|
+
if (r.status !== 0) {
|
|
265
|
+
throw new Error(`${pm} install exited ${r.status}`);
|
|
266
|
+
}
|
|
267
|
+
return { skipped: false };
|
|
268
|
+
}
|
|
269
|
+
function alreadyHasFixPromptInit(src) {
|
|
270
|
+
return /initFixPrompt\s*\(/.test(src) || /from\s+['"]@fixprompt\//.test(src);
|
|
271
|
+
}
|
|
272
|
+
function buildBrowserInitSnippet(slug, envAccess, envCheck) {
|
|
273
|
+
return `
|
|
274
|
+
import { initFixPrompt } from '@fixprompt/browser';
|
|
275
|
+
const __fpKey = ${envAccess};
|
|
276
|
+
if (__fpKey) {
|
|
277
|
+
initFixPrompt({
|
|
278
|
+
projectKey: __fpKey,
|
|
279
|
+
source: '${slug}-prod',
|
|
280
|
+
service: '${slug}',
|
|
281
|
+
env: ${envCheck} ? 'prod' : 'dev',
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
function buildRnInitSnippet(slug) {
|
|
287
|
+
return `
|
|
288
|
+
import { initFixPrompt } from '@fixprompt/react-native';
|
|
289
|
+
const __fpKey = process.env.EXPO_PUBLIC_FIXPROMPT_KEY;
|
|
290
|
+
if (__fpKey) {
|
|
291
|
+
initFixPrompt({
|
|
292
|
+
projectKey: __fpKey,
|
|
293
|
+
source: '${slug}-prod',
|
|
294
|
+
service: '${slug}',
|
|
295
|
+
env: __DEV__ ? 'dev' : 'prod',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
function injectAfterImports(src, snippet) {
|
|
301
|
+
const lines = src.split("\n");
|
|
302
|
+
let lastImportIdx = -1;
|
|
303
|
+
for (let i = 0; i < lines.length; i++) {
|
|
304
|
+
const l = lines[i].trim();
|
|
305
|
+
if (l.startsWith("import ")) lastImportIdx = i;
|
|
306
|
+
if (lastImportIdx !== -1 && l && !l.startsWith("import ") && !l.startsWith("//") && !l.startsWith("/*")) {
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (lastImportIdx === -1) return snippet.replace(/^\n/, "") + src;
|
|
311
|
+
lines.splice(lastImportIdx + 1, 0, snippet.replace(/^\n/, "").replace(/\n$/, ""));
|
|
312
|
+
return lines.join("\n");
|
|
313
|
+
}
|
|
314
|
+
function injectVite(opts) {
|
|
315
|
+
const entry = opts.stack.entryFile;
|
|
316
|
+
if (!entry) return { applied: false, reason: "no Vite entry file (src/main.*) found" };
|
|
317
|
+
const src = readFileSync(entry, "utf8");
|
|
318
|
+
if (alreadyHasFixPromptInit(src)) return { applied: false, file: entry, reason: "already wired" };
|
|
319
|
+
const snippet = buildBrowserInitSnippet(
|
|
320
|
+
opts.slug,
|
|
321
|
+
"import.meta.env.VITE_FIXPROMPT_KEY",
|
|
322
|
+
"import.meta.env.PROD"
|
|
323
|
+
);
|
|
324
|
+
writeFileSync(entry, injectAfterImports(src, snippet));
|
|
325
|
+
return { applied: true, file: entry };
|
|
326
|
+
}
|
|
327
|
+
function injectCra(opts) {
|
|
328
|
+
const entry = opts.stack.entryFile;
|
|
329
|
+
if (!entry) return { applied: false, reason: "no CRA entry file (src/index.*) found" };
|
|
330
|
+
const src = readFileSync(entry, "utf8");
|
|
331
|
+
if (alreadyHasFixPromptInit(src)) return { applied: false, file: entry, reason: "already wired" };
|
|
332
|
+
const snippet = buildBrowserInitSnippet(
|
|
333
|
+
opts.slug,
|
|
334
|
+
"process.env.REACT_APP_FIXPROMPT_KEY",
|
|
335
|
+
"process.env.NODE_ENV === 'production'"
|
|
336
|
+
);
|
|
337
|
+
writeFileSync(entry, injectAfterImports(src, snippet));
|
|
338
|
+
return { applied: true, file: entry };
|
|
339
|
+
}
|
|
340
|
+
function injectRn(opts) {
|
|
341
|
+
const entry = opts.stack.entryFile;
|
|
342
|
+
if (!entry) return { applied: false, reason: "no React Native entry (App.* / index.*) found" };
|
|
343
|
+
const src = readFileSync(entry, "utf8");
|
|
344
|
+
if (alreadyHasFixPromptInit(src)) return { applied: false, file: entry, reason: "already wired" };
|
|
345
|
+
const snippet = buildRnInitSnippet(opts.slug);
|
|
346
|
+
writeFileSync(entry, injectAfterImports(src, snippet));
|
|
347
|
+
return { applied: true, file: entry };
|
|
348
|
+
}
|
|
349
|
+
function injectNextApp(opts) {
|
|
350
|
+
const layoutPath = opts.stack.entryFile;
|
|
351
|
+
if (!layoutPath) return { applied: false, reason: "no Next.js layout file found" };
|
|
352
|
+
const layoutDir = dirname(layoutPath);
|
|
353
|
+
const initPath = join(layoutDir, "fixprompt-init.tsx");
|
|
354
|
+
if (!existsSync(initPath)) {
|
|
355
|
+
const initContent = `'use client';
|
|
356
|
+
import { useEffect } from 'react';
|
|
357
|
+
import { initFixPrompt } from '@fixprompt/browser';
|
|
358
|
+
|
|
359
|
+
let __fpDone = false;
|
|
360
|
+
|
|
361
|
+
export function FixPromptInit() {
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
if (__fpDone) return;
|
|
364
|
+
const key = process.env.NEXT_PUBLIC_FIXPROMPT_KEY;
|
|
365
|
+
if (!key) return;
|
|
366
|
+
initFixPrompt({
|
|
367
|
+
projectKey: key,
|
|
368
|
+
source: '${opts.slug}-prod',
|
|
369
|
+
service: '${opts.slug}',
|
|
370
|
+
env: process.env.NODE_ENV === 'production' ? 'prod' : 'dev',
|
|
371
|
+
});
|
|
372
|
+
__fpDone = true;
|
|
373
|
+
}, []);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
`;
|
|
377
|
+
writeFileSync(initPath, initContent);
|
|
378
|
+
}
|
|
379
|
+
const layoutSrc = readFileSync(layoutPath, "utf8");
|
|
380
|
+
if (alreadyHasFixPromptInit(layoutSrc)) {
|
|
381
|
+
return { applied: false, file: layoutPath, reason: "layout already imports FixPromptInit" };
|
|
382
|
+
}
|
|
383
|
+
let next = layoutSrc;
|
|
384
|
+
const importLine = `import { FixPromptInit } from './fixprompt-init';
|
|
385
|
+
`;
|
|
386
|
+
const lines = next.split("\n");
|
|
387
|
+
let lastImportIdx = -1;
|
|
388
|
+
for (let i = 0; i < lines.length; i++) {
|
|
389
|
+
if (lines[i].trim().startsWith("import ")) lastImportIdx = i;
|
|
390
|
+
}
|
|
391
|
+
if (lastImportIdx >= 0) {
|
|
392
|
+
lines.splice(lastImportIdx + 1, 0, importLine.trim());
|
|
393
|
+
} else {
|
|
394
|
+
lines.unshift(importLine.trim());
|
|
395
|
+
}
|
|
396
|
+
next = lines.join("\n");
|
|
397
|
+
const bodyOpen = /<body([^>]*)>/;
|
|
398
|
+
if (!bodyOpen.test(next)) {
|
|
399
|
+
return {
|
|
400
|
+
applied: false,
|
|
401
|
+
file: layoutPath,
|
|
402
|
+
reason: "layout has no <body> tag \u2014 paste <FixPromptInit /> manually"
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
next = next.replace(bodyOpen, (m) => `${m}
|
|
406
|
+
<FixPromptInit />`);
|
|
407
|
+
writeFileSync(layoutPath, next);
|
|
408
|
+
return { applied: true, file: layoutPath };
|
|
409
|
+
}
|
|
410
|
+
function injectNextPages(opts) {
|
|
411
|
+
const entry = opts.stack.entryFile;
|
|
412
|
+
if (!entry) return { applied: false, reason: "no pages/_app.* found" };
|
|
413
|
+
const src = readFileSync(entry, "utf8");
|
|
414
|
+
if (alreadyHasFixPromptInit(src)) return { applied: false, file: entry, reason: "already wired" };
|
|
415
|
+
const snippet = buildBrowserInitSnippet(
|
|
416
|
+
opts.slug,
|
|
417
|
+
"process.env.NEXT_PUBLIC_FIXPROMPT_KEY",
|
|
418
|
+
"process.env.NODE_ENV === 'production'"
|
|
419
|
+
);
|
|
420
|
+
writeFileSync(entry, injectAfterImports(src, snippet));
|
|
421
|
+
return { applied: true, file: entry };
|
|
422
|
+
}
|
|
423
|
+
function injectInit(opts) {
|
|
424
|
+
switch (opts.stack.runtime) {
|
|
425
|
+
case "vite":
|
|
426
|
+
return injectVite(opts);
|
|
427
|
+
case "cra":
|
|
428
|
+
return injectCra(opts);
|
|
429
|
+
case "browser":
|
|
430
|
+
return injectVite(opts);
|
|
431
|
+
// close enough — uses VITE_ env, edits the same kind of file
|
|
432
|
+
case "react-native":
|
|
433
|
+
return injectRn(opts);
|
|
434
|
+
case "next-app":
|
|
435
|
+
return injectNextApp(opts);
|
|
436
|
+
case "next-pages":
|
|
437
|
+
return injectNextPages(opts);
|
|
438
|
+
case "node":
|
|
439
|
+
return { applied: false, reason: "Node SDK (@geos/loghub-client) has no init() to inject" };
|
|
440
|
+
default:
|
|
441
|
+
return { applied: false, reason: "unknown stack \u2014 no injection performed" };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/connect.ts
|
|
93
446
|
var DEFAULT_ENDPOINT = "https://geosloghub-production.up.railway.app";
|
|
94
447
|
var ENV_VAR_BY_SCOPE = {
|
|
95
448
|
deploy: "FIXPROMPT_DEPLOY_TOKEN",
|
|
@@ -114,13 +467,13 @@ function dirExists(p) {
|
|
|
114
467
|
}
|
|
115
468
|
}
|
|
116
469
|
function detectProvider(cwd) {
|
|
117
|
-
if (fileExists(
|
|
470
|
+
if (fileExists(join2(cwd, "app.json")) || fileExists(join2(cwd, "app.config.js")) || fileExists(join2(cwd, "app.config.ts")) || fileExists(join2(cwd, "eas.json"))) {
|
|
118
471
|
return "eas";
|
|
119
472
|
}
|
|
120
|
-
if (fileExists(
|
|
473
|
+
if (fileExists(join2(cwd, "vercel.json")) || fileExists(join2(cwd, ".vercel", "project.json"))) {
|
|
121
474
|
return "vercel";
|
|
122
475
|
}
|
|
123
|
-
const wfDir =
|
|
476
|
+
const wfDir = join2(cwd, ".github", "workflows");
|
|
124
477
|
if (dirExists(wfDir)) {
|
|
125
478
|
try {
|
|
126
479
|
const files = readdirSync(wfDir);
|
|
@@ -167,7 +520,7 @@ function writeEas(envVar, token, env, force) {
|
|
|
167
520
|
"--non-interactive"
|
|
168
521
|
];
|
|
169
522
|
if (force) args.push("--force");
|
|
170
|
-
const r =
|
|
523
|
+
const r = spawnSync2("npx", useShell ? args.map((a) => `"${a.replace(/"/g, '\\"')}"`) : args, {
|
|
171
524
|
stdio: "inherit",
|
|
172
525
|
shell: useShell
|
|
173
526
|
});
|
|
@@ -184,7 +537,7 @@ function writeGitHubActions(envVar, token, cwd) {
|
|
|
184
537
|
console.log(` \xB7 writing ${envVar} to GitHub Actions repo secrets\u2026`);
|
|
185
538
|
const useShell = process.platform === "win32";
|
|
186
539
|
const args = ["secret", "set", envVar, "--body", "-"];
|
|
187
|
-
const r =
|
|
540
|
+
const r = spawnSync2("gh", useShell ? args.map((a) => `"${a.replace(/"/g, '\\"')}"`) : args, {
|
|
188
541
|
input: token,
|
|
189
542
|
cwd,
|
|
190
543
|
stdio: ["pipe", "inherit", "inherit"],
|
|
@@ -209,7 +562,7 @@ then re-run with that token exported.`
|
|
|
209
562
|
}
|
|
210
563
|
let projectId = target.vercelProjectId;
|
|
211
564
|
if (!projectId) {
|
|
212
|
-
const pj =
|
|
565
|
+
const pj = join2(process.cwd(), ".vercel", "project.json");
|
|
213
566
|
if (!fileExists(pj)) {
|
|
214
567
|
throw new Error(
|
|
215
568
|
`Could not resolve Vercel project id. Either run from a 'vercel link'-ed
|
|
@@ -217,7 +570,7 @@ directory or pass --vercel-project-id <prj_\u2026>.`
|
|
|
217
570
|
);
|
|
218
571
|
}
|
|
219
572
|
try {
|
|
220
|
-
const parsed = JSON.parse(
|
|
573
|
+
const parsed = JSON.parse(readFileSync2(pj, "utf8"));
|
|
221
574
|
projectId = parsed.projectId;
|
|
222
575
|
} catch (e) {
|
|
223
576
|
throw new Error(`failed to read .vercel/project.json: ${e.message}`);
|
|
@@ -256,20 +609,20 @@ function writeToProvider(envVar, token, provider, target, force, cwd) {
|
|
|
256
609
|
}
|
|
257
610
|
}
|
|
258
611
|
function ensureGitignoreHas(cwd, pattern) {
|
|
259
|
-
const gi =
|
|
612
|
+
const gi = join2(cwd, ".gitignore");
|
|
260
613
|
let s = "";
|
|
261
614
|
try {
|
|
262
|
-
s =
|
|
615
|
+
s = readFileSync2(gi, "utf8");
|
|
263
616
|
} catch {
|
|
264
617
|
}
|
|
265
618
|
const lines = s.split(/\r?\n/);
|
|
266
619
|
if (lines.some((l) => l.trim() === pattern)) return false;
|
|
267
620
|
const sep = s.length === 0 || s.endsWith("\n") ? "" : "\n";
|
|
268
|
-
|
|
621
|
+
writeFileSync2(gi, s + sep + "\n# FixLoop\n" + pattern + "\n");
|
|
269
622
|
return true;
|
|
270
623
|
}
|
|
271
624
|
function appendEnvLocal(cwd, token, projectId, projectSlug, endpoint) {
|
|
272
|
-
const envPath =
|
|
625
|
+
const envPath = join2(cwd, ".env.local");
|
|
273
626
|
const lines = [
|
|
274
627
|
"",
|
|
275
628
|
"# FixLoop \u2014 read token for the local coding agent. Gitignored.",
|
|
@@ -282,44 +635,147 @@ function appendEnvLocal(cwd, token, projectId, projectSlug, endpoint) {
|
|
|
282
635
|
lines.push(`FIXPROMPT_BROKER_URL=${endpoint}`);
|
|
283
636
|
lines.push("");
|
|
284
637
|
const block = lines.join("\n");
|
|
285
|
-
if (
|
|
638
|
+
if (existsSync2(envPath)) {
|
|
286
639
|
appendFileSync(envPath, block);
|
|
287
640
|
} else {
|
|
288
|
-
|
|
641
|
+
writeFileSync2(envPath, block.replace(/^\n/, ""));
|
|
289
642
|
}
|
|
290
643
|
}
|
|
291
|
-
function
|
|
292
|
-
const
|
|
293
|
-
|
|
644
|
+
function detectRuntime(cwd) {
|
|
645
|
+
const pj = join2(cwd, "package.json");
|
|
646
|
+
if (!existsSync2(pj)) return "unknown";
|
|
647
|
+
try {
|
|
648
|
+
const j = JSON.parse(readFileSync2(pj, "utf8"));
|
|
649
|
+
const deps = { ...j.dependencies || {}, ...j.devDependencies || {} };
|
|
650
|
+
if (deps["react-native"] || deps["expo"]) return "react-native";
|
|
651
|
+
if (deps["next"]) return "next";
|
|
652
|
+
if (deps["react"] || deps["vite"] || deps["react-scripts"]) return "browser";
|
|
653
|
+
if (deps["@nestjs/core"] || deps["express"] || deps["fastify"]) return "node";
|
|
654
|
+
} catch {
|
|
655
|
+
}
|
|
656
|
+
return "unknown";
|
|
657
|
+
}
|
|
658
|
+
var FIXLOOP_SECTION_MARKER = "<!-- fixloop:claude-section -->";
|
|
659
|
+
function buildClaudeMdSection(projectId, projectSlug, runtime) {
|
|
660
|
+
const sdkRows = {
|
|
661
|
+
"react-native": {
|
|
662
|
+
pkg: "@fixprompt/react-native",
|
|
663
|
+
entry: "App.tsx / index.js \u2014 call before the first render",
|
|
664
|
+
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});"
|
|
665
|
+
},
|
|
666
|
+
next: {
|
|
667
|
+
pkg: "@fixprompt/browser (client) + @fixprompt/node (server, github:goscha01/geos-loghub-client until @fixprompt/node ships)",
|
|
668
|
+
entry: "src/app/providers.tsx for client (useEffect-init), instrumentation.ts or server-start for Node",
|
|
669
|
+
example: `// client (in a "use client" component, useEffect):
|
|
670
|
+
import { initFixPrompt } from '@fixprompt/browser';
|
|
671
|
+
initFixPrompt({ projectKey: process.env.NEXT_PUBLIC_FIXPROMPT_KEY!, source: '<slug>-prod', service: '<slug>', env: 'prod' });`
|
|
672
|
+
},
|
|
673
|
+
browser: {
|
|
674
|
+
pkg: "@fixprompt/browser",
|
|
675
|
+
entry: "src/main.{ts,tsx} / index.{js,tsx} \u2014 top of file, before render",
|
|
676
|
+
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});"
|
|
677
|
+
},
|
|
678
|
+
node: {
|
|
679
|
+
pkg: "@geos/loghub-client (npm install @geos/loghub-client@github:goscha01/geos-loghub-client \u2014 @fixprompt/node not yet on npm)",
|
|
680
|
+
entry: "src/index.{ts,js} / src/main.{ts,js} \u2014 at server start, before route registration",
|
|
681
|
+
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});"
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
const sdk = runtime === "unknown" ? {
|
|
685
|
+
pkg: "@fixprompt/browser | @fixprompt/react-native | @geos/loghub-client (Node) \u2014 pick by stack",
|
|
686
|
+
entry: "(read package.json to determine runtime)",
|
|
687
|
+
example: "// see https://fixloop-dashboard.vercel.app/onboarding for runtime-specific snippets"
|
|
688
|
+
} : sdkRows[runtime];
|
|
689
|
+
const slugLine = projectSlug ? `\`${projectSlug}\`` : "(see .env.local)";
|
|
690
|
+
const sdkExampleWithSlug = sdk.example.replace(/<slug>/g, projectSlug ?? "<slug>");
|
|
691
|
+
return [
|
|
692
|
+
"",
|
|
693
|
+
FIXLOOP_SECTION_MARKER,
|
|
694
|
+
"## FixLoop \u2014 production error logs (auto-fetch on bug reports + dev loop)",
|
|
294
695
|
"",
|
|
295
|
-
|
|
696
|
+
`This project is wired to FixLoop. Two behaviors expected of the coding agent reading this file:`,
|
|
296
697
|
"",
|
|
297
|
-
|
|
698
|
+
"### 1. Install the SDK on first use (skip if already in `package.json`)",
|
|
298
699
|
"",
|
|
299
|
-
|
|
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.**",
|
|
700
|
+
`Detected runtime: **${runtime}**. Recommended SDK: \`${sdk.pkg}\``,
|
|
303
701
|
"",
|
|
304
|
-
|
|
702
|
+
`- Entry point: ${sdk.entry}`,
|
|
703
|
+
`- Init snippet:`,
|
|
305
704
|
"",
|
|
306
|
-
"```
|
|
307
|
-
|
|
308
|
-
' "$FIXPROMPT_BROKER_URL/projects/$FIXPROMPT_PROJECT_ID/logs?since=24h&level=error,warn,fatal&limit=200"',
|
|
705
|
+
"```ts",
|
|
706
|
+
sdkExampleWithSlug,
|
|
309
707
|
"```",
|
|
310
708
|
"",
|
|
311
|
-
"
|
|
709
|
+
"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.",
|
|
710
|
+
"",
|
|
711
|
+
"### 2. Debug loop on bug reports",
|
|
712
|
+
"",
|
|
713
|
+
`Trigger phrases: "X is broken", "doesn't work", "crashes", "is failing", pasted error stacks, "users are reporting Y", TestFlight/App Store bug reports.`,
|
|
714
|
+
"",
|
|
715
|
+
"Loop until the log query returns no new matches:",
|
|
716
|
+
"",
|
|
717
|
+
"1. **Fetch recent errors** \u2014 env vars come from `.env.local` (gitignored \u2014 never commit):",
|
|
718
|
+
" ```sh",
|
|
719
|
+
' curl -sH "x-fixprompt-read-token: $FIXPROMPT_READ_TOKEN" \\',
|
|
720
|
+
' "$FIXPROMPT_BROKER_URL/projects/$FIXPROMPT_PROJECT_ID/logs?since=24h&level=error,warn,fatal&limit=200"',
|
|
721
|
+
" ```",
|
|
722
|
+
" Query params: `since` (15m..7d), `level` (csv), `q` (substring), `limit` (1..500).",
|
|
723
|
+
"",
|
|
724
|
+
'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"*).',
|
|
725
|
+
"",
|
|
726
|
+
"3. **Apply a fix** grounded in what the logs show. **Do not ask the user to paste log lines \u2014 fetch them yourself.**",
|
|
727
|
+
"",
|
|
728
|
+
"4. **Verify locally** \u2014 run the build / dev server / tests so the fix compiles and the affected path no longer throws.",
|
|
729
|
+
"",
|
|
730
|
+
"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.",
|
|
312
731
|
"",
|
|
313
|
-
|
|
314
|
-
|
|
732
|
+
"### Project",
|
|
733
|
+
"",
|
|
734
|
+
`- Slug: ${slugLine}`,
|
|
735
|
+
`- Dashboard: https://fixloop-dashboard.vercel.app/issues/${projectId}`,
|
|
736
|
+
`- Manage / revoke the read token: https://fixloop-dashboard.vercel.app/integrations/${projectId}`,
|
|
315
737
|
""
|
|
316
|
-
].
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
738
|
+
].join("\n");
|
|
739
|
+
}
|
|
740
|
+
function stripFixloopSection(body) {
|
|
741
|
+
const lines = body.split("\n");
|
|
742
|
+
let start = -1;
|
|
743
|
+
for (let i = 0; i < lines.length; i++) {
|
|
744
|
+
if (lines[i].trim() === FIXLOOP_SECTION_MARKER) {
|
|
745
|
+
start = i;
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
if (lines[i].startsWith("## FixLoop \u2014")) {
|
|
749
|
+
start = i;
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (start === -1) return body;
|
|
754
|
+
const realStart = start > 0 && lines[start - 1] === "" ? start - 1 : start;
|
|
755
|
+
let end = lines.length;
|
|
756
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
757
|
+
if (lines[i].startsWith("## ") && !lines[i].startsWith("## FixLoop \u2014")) {
|
|
758
|
+
end = i;
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const trimmedTail = lines.slice(end);
|
|
763
|
+
while (trimmedTail.length > 0 && realStart > 0 && lines[realStart - 1] === "" && trimmedTail[0] === "") {
|
|
764
|
+
trimmedTail.shift();
|
|
765
|
+
}
|
|
766
|
+
return [...lines.slice(0, realStart), ...trimmedTail].join("\n");
|
|
767
|
+
}
|
|
768
|
+
function appendClaudeMdGuidance(cwd, projectId, projectSlug) {
|
|
769
|
+
const claudePath = join2(cwd, "CLAUDE.md");
|
|
770
|
+
const runtime = detectRuntime(cwd);
|
|
771
|
+
const section = buildClaudeMdSection(projectId, projectSlug, runtime);
|
|
772
|
+
if (existsSync2(claudePath)) {
|
|
773
|
+
const existing = readFileSync2(claudePath, "utf8");
|
|
774
|
+
const stripped = stripFixloopSection(existing);
|
|
775
|
+
const next = stripped.endsWith("\n") ? stripped + section.replace(/^\n/, "") : stripped + section;
|
|
776
|
+
writeFileSync2(claudePath, next);
|
|
321
777
|
} else {
|
|
322
|
-
|
|
778
|
+
writeFileSync2(claudePath, "# Project\n" + section);
|
|
323
779
|
}
|
|
324
780
|
}
|
|
325
781
|
function writeLocalDiscovery(opts) {
|
|
@@ -329,6 +785,61 @@ function writeLocalDiscovery(opts) {
|
|
|
329
785
|
appendEnvLocal(opts.cwd, opts.token, opts.projectId, opts.projectSlug, opts.endpoint);
|
|
330
786
|
appendClaudeMdGuidance(opts.cwd, opts.projectId, opts.projectSlug);
|
|
331
787
|
}
|
|
788
|
+
function deriveSlug(name) {
|
|
789
|
+
const stripped = name.replace(/^@[^/]+\//, "");
|
|
790
|
+
const slug = stripped.toLowerCase().replace(/[_.@]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
791
|
+
if (slug.length < 2 || slug.length > 32) return null;
|
|
792
|
+
if (!/^[a-z0-9]/.test(slug) || !/[a-z0-9]$/.test(slug)) return null;
|
|
793
|
+
return slug;
|
|
794
|
+
}
|
|
795
|
+
function deriveProjectType(runtime) {
|
|
796
|
+
switch (runtime) {
|
|
797
|
+
case "react-native":
|
|
798
|
+
return "mobile";
|
|
799
|
+
case "next-app":
|
|
800
|
+
case "next-pages":
|
|
801
|
+
case "vite":
|
|
802
|
+
case "cra":
|
|
803
|
+
case "browser":
|
|
804
|
+
return "web";
|
|
805
|
+
case "node":
|
|
806
|
+
return "api";
|
|
807
|
+
default:
|
|
808
|
+
return "other";
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
async function createProjectViaAdmin(opts) {
|
|
812
|
+
const res = await fetch(`${opts.endpoint}/admin/projects`, {
|
|
813
|
+
method: "POST",
|
|
814
|
+
headers: {
|
|
815
|
+
"x-internal-admin-token": opts.adminToken,
|
|
816
|
+
"Content-Type": "application/json"
|
|
817
|
+
},
|
|
818
|
+
body: JSON.stringify({
|
|
819
|
+
name: opts.name,
|
|
820
|
+
slug: opts.slug,
|
|
821
|
+
project_type: opts.projectType
|
|
822
|
+
})
|
|
823
|
+
});
|
|
824
|
+
const text = await res.text();
|
|
825
|
+
if (!res.ok) {
|
|
826
|
+
throw new Error(`broker /admin/projects responded ${res.status}: ${text.slice(0, 200)}`);
|
|
827
|
+
}
|
|
828
|
+
return JSON.parse(text);
|
|
829
|
+
}
|
|
830
|
+
async function writeRuntimeKey(key, envVar, provider, target, force, cwd) {
|
|
831
|
+
if (!provider) return { wrote: false, reason: "no provider detected" };
|
|
832
|
+
switch (provider) {
|
|
833
|
+
case "eas":
|
|
834
|
+
writeEas(envVar, key, target.easEnv, force);
|
|
835
|
+
return { wrote: true };
|
|
836
|
+
case "vercel":
|
|
837
|
+
await writeVercel(envVar, key, target);
|
|
838
|
+
return { wrote: true };
|
|
839
|
+
case "github-actions":
|
|
840
|
+
return { wrote: false, reason: "github-actions has no runtime bundle \u2014 runtime key not stored" };
|
|
841
|
+
}
|
|
842
|
+
}
|
|
332
843
|
async function connect(args) {
|
|
333
844
|
const cwd = process.cwd();
|
|
334
845
|
const scopeRaw = (args.flags.scope ?? "deploy").toLowerCase();
|
|
@@ -345,25 +856,67 @@ async function connect(args) {
|
|
|
345
856
|
process.exit(1);
|
|
346
857
|
}
|
|
347
858
|
const endpoint = (args.flags.endpoint ?? process.env.FIXPROMPT_ENDPOINT ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
|
|
859
|
+
const stack = detectStack(cwd);
|
|
860
|
+
const shouldInstall = args.flags["no-install"] !== true;
|
|
348
861
|
let projectId = args.flags["project-id"] ?? process.env.FIXPROMPT_PROJECT_ID ?? null;
|
|
349
862
|
let projectSlug = args.flags["project-slug"] ?? null;
|
|
863
|
+
let freshRuntimeKey = null;
|
|
350
864
|
if (!projectId && projectSlug) {
|
|
351
865
|
const res = await fetch(`${endpoint}/admin/projects/by-slug/${encodeURIComponent(projectSlug)}`, {
|
|
352
866
|
headers: { "x-internal-admin-token": adminToken }
|
|
353
867
|
});
|
|
354
|
-
|
|
355
|
-
|
|
868
|
+
if (res.ok) {
|
|
869
|
+
const parsed = await res.json();
|
|
870
|
+
projectId = parsed.id;
|
|
871
|
+
projectSlug = parsed.slug;
|
|
872
|
+
console.log(` resolved --project-slug=${projectSlug} \u2192 ${parsed.id} (${parsed.name})`);
|
|
873
|
+
} else if (res.status !== 404) {
|
|
874
|
+
const text = await res.text();
|
|
356
875
|
console.error(`Project lookup failed (${res.status}): ${text.slice(0, 200)}`);
|
|
357
876
|
process.exit(1);
|
|
358
877
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
878
|
+
}
|
|
879
|
+
const wantCreate = args.flags.create === true || !projectId && !projectSlug;
|
|
880
|
+
if (!projectId && wantCreate) {
|
|
881
|
+
let slugForCreate = projectSlug;
|
|
882
|
+
let nameForCreate = args.flags["project-name"] ?? null;
|
|
883
|
+
if (!slugForCreate || !nameForCreate) {
|
|
884
|
+
const pj = (() => {
|
|
885
|
+
try {
|
|
886
|
+
return JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
|
|
887
|
+
} catch {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
})();
|
|
891
|
+
const pkgName = pj?.name;
|
|
892
|
+
if (pkgName) {
|
|
893
|
+
slugForCreate = slugForCreate ?? deriveSlug(pkgName);
|
|
894
|
+
nameForCreate = nameForCreate ?? pkgName;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (!slugForCreate) {
|
|
898
|
+
console.error(
|
|
899
|
+
"Could not derive a project slug.\n Pass --project-slug <slug> or set a clean package.json#name.\n Slug rules: 2-32 chars, lowercase a-z 0-9 dashes, starts/ends with alphanum."
|
|
900
|
+
);
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
console.log(`
|
|
904
|
+
\u2022 Creating FixLoop project '${slugForCreate}' (no existing match)\u2026`);
|
|
905
|
+
const created = await createProjectViaAdmin({
|
|
906
|
+
endpoint,
|
|
907
|
+
adminToken,
|
|
908
|
+
name: nameForCreate ?? slugForCreate,
|
|
909
|
+
slug: slugForCreate,
|
|
910
|
+
projectType: deriveProjectType(stack.runtime)
|
|
911
|
+
});
|
|
912
|
+
projectId = created.project_id;
|
|
913
|
+
projectSlug = slugForCreate;
|
|
914
|
+
freshRuntimeKey = created.key;
|
|
915
|
+
console.log(` \u2713 project_id ${created.project_id}, source ${created.source}`);
|
|
363
916
|
}
|
|
364
917
|
if (!projectId) {
|
|
365
918
|
console.error(
|
|
366
|
-
"Need --project-id
|
|
919
|
+
"Need --project-id, --project-slug, or --create (or $FIXPROMPT_PROJECT_ID).\n --create derives slug + name from package.json#name automatically."
|
|
367
920
|
);
|
|
368
921
|
process.exit(1);
|
|
369
922
|
}
|
|
@@ -397,6 +950,18 @@ async function connect(args) {
|
|
|
397
950
|
const force = args.flags.force === true;
|
|
398
951
|
const scopes = scope === "both" ? ["deploy", "read"] : [scope];
|
|
399
952
|
const mintedTokens = [];
|
|
953
|
+
if (freshRuntimeKey && stack.envVarName) {
|
|
954
|
+
console.log(`
|
|
955
|
+
\u2022 Writing runtime key (${stack.envVarName}) to ${provider ?? "no provider"}\u2026`);
|
|
956
|
+
try {
|
|
957
|
+
const r = await writeRuntimeKey(freshRuntimeKey, stack.envVarName, provider, target, force, cwd);
|
|
958
|
+
if (r.wrote) console.log(" \u2713 runtime key stored on CI provider");
|
|
959
|
+
else console.log(` \xB7 skipped (${r.reason}) \u2014 set ${stack.envVarName} manually if needed`);
|
|
960
|
+
} catch (e) {
|
|
961
|
+
console.warn(` ! runtime key write failed: ${e.message}`);
|
|
962
|
+
console.warn(` Manually set ${stack.envVarName}=${freshRuntimeKey.slice(0, 8)}\u2026 in your CI provider.`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
400
965
|
for (const s of scopes) {
|
|
401
966
|
const envVar = ENV_VAR_BY_SCOPE[s];
|
|
402
967
|
const label = scope === "both" ? `${labelBase}-${s}` : labelBase;
|
|
@@ -418,6 +983,27 @@ async function connect(args) {
|
|
|
418
983
|
mintedTokens.push({ scope: s, token_id: minted.token_id, envVar });
|
|
419
984
|
}
|
|
420
985
|
const readMinted = mintedTokens.some((m) => m.scope === "read");
|
|
986
|
+
if (readMinted && shouldInstall && stack.runtime) {
|
|
987
|
+
console.log(`
|
|
988
|
+
\u2022 Installing SDK + injecting init for runtime '${stack.runtime}'\u2026`);
|
|
989
|
+
try {
|
|
990
|
+
const inst = installSdk(cwd, stack);
|
|
991
|
+
if (inst.skipped) console.log(` \xB7 ${inst.reason}`);
|
|
992
|
+
else console.log(` \u2713 ${stack.sdkPackage} installed`);
|
|
993
|
+
} catch (e) {
|
|
994
|
+
console.warn(` ! install failed: ${e.message}`);
|
|
995
|
+
console.warn(` Continuing \u2014 set up the SDK manually with: ${stack.packageManager} add ${stack.sdkPackage}`);
|
|
996
|
+
}
|
|
997
|
+
if (projectSlug) {
|
|
998
|
+
const inj = injectInit({ cwd, stack, slug: projectSlug });
|
|
999
|
+
if (inj.applied) console.log(` \u2713 init code injected into ${inj.file}`);
|
|
1000
|
+
else console.log(` \xB7 injection skipped: ${inj.reason}${inj.file ? ` (${inj.file})` : ""}`);
|
|
1001
|
+
}
|
|
1002
|
+
} else if (readMinted && !stack.runtime) {
|
|
1003
|
+
console.log(`
|
|
1004
|
+
\u2022 Skipped SDK install \u2014 couldn't detect a known runtime in ${cwd}.`);
|
|
1005
|
+
console.log(` Manually install the right @fixprompt/* package and call initFixPrompt({...}).`);
|
|
1006
|
+
}
|
|
421
1007
|
const deployMinted = mintedTokens.some((m) => m.scope === "deploy");
|
|
422
1008
|
console.log("");
|
|
423
1009
|
if (deployMinted && provider) {
|
|
@@ -444,7 +1030,7 @@ async function connect(args) {
|
|
|
444
1030
|
|
|
445
1031
|
// src/version.ts
|
|
446
1032
|
var CLI_NAME = "@fixprompt/cli";
|
|
447
|
-
var CLI_VERSION = "0.
|
|
1033
|
+
var CLI_VERSION = "0.6.0";
|
|
448
1034
|
|
|
449
1035
|
// src/cli.ts
|
|
450
1036
|
var DEFAULT_ENDPOINT2 = "https://geosloghub-production.up.railway.app";
|
|
@@ -484,13 +1070,19 @@ Usage:
|
|
|
484
1070
|
|
|
485
1071
|
connect options:
|
|
486
1072
|
--scope <s> deploy | read | both (default: deploy)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
1073
|
+
\u25B8 For the zero-paste new-project flow, use --scope both.
|
|
1074
|
+
deploy \u2192 mints fpd_\u2026, writes FIXPROMPT_DEPLOY_TOKEN to CI
|
|
1075
|
+
read \u2192 mints fpr_\u2026, writes FIXPROMPT_READ_TOKEN locally,
|
|
1076
|
+
installs the SDK, injects initFixPrompt({\u2026})
|
|
1077
|
+
into the entry file, drops a CLAUDE.md section
|
|
1078
|
+
both \u2192 all of the above in one pass
|
|
1079
|
+
--create Auto-create the FixLoop project if --project-slug doesn't
|
|
1080
|
+
exist (or if no slug given, derived from package.json#name).
|
|
1081
|
+
Writes the new runtime k_ key into the CI provider env.
|
|
1082
|
+
--no-install Skip SDK install + init code injection (read scope only).
|
|
492
1083
|
--project-id <uuid> FixLoop project id (or $FIXPROMPT_PROJECT_ID)
|
|
493
1084
|
--project-slug <slug> Look up project_id by slug (no UUID needed)
|
|
1085
|
+
--project-name <text> Display name for --create (default: package.json#name)
|
|
494
1086
|
--admin-token <fpa_\u2026> Broker admin token (or $FIXPROMPT_ADMIN_TOKEN)
|
|
495
1087
|
--provider <name> eas | vercel | github-actions (auto-detected from cwd)
|
|
496
1088
|
--eas-env <env> EAS env to write to (default: production)
|
|
@@ -499,6 +1091,9 @@ connect options:
|
|
|
499
1091
|
--label <text> Token label (default: <provider>-<timestamp>)
|
|
500
1092
|
--force Overwrite an existing env var value
|
|
501
1093
|
|
|
1094
|
+
Zero-paste flow (in your app repo root):
|
|
1095
|
+
FIXPROMPT_ADMIN_TOKEN=fpa_\u2026 npx @fixprompt/cli connect --scope both --create
|
|
1096
|
+
|
|
502
1097
|
deploy-start auth (pick one):
|
|
503
1098
|
--deploy-token <fpd_...> Deploy-only token from dashboard (recommended for CI)
|
|
504
1099
|
(or $FIXPROMPT_DEPLOY_TOKEN env)
|
package/package.json
CHANGED