@fixprompt/cli 0.5.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 +528 -36
- 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,17 +635,17 @@ 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
644
|
function detectRuntime(cwd) {
|
|
292
|
-
const pj =
|
|
293
|
-
if (!
|
|
645
|
+
const pj = join2(cwd, "package.json");
|
|
646
|
+
if (!existsSync2(pj)) return "unknown";
|
|
294
647
|
try {
|
|
295
|
-
const j = JSON.parse(
|
|
648
|
+
const j = JSON.parse(readFileSync2(pj, "utf8"));
|
|
296
649
|
const deps = { ...j.dependencies || {}, ...j.devDependencies || {} };
|
|
297
650
|
if (deps["react-native"] || deps["expo"]) return "react-native";
|
|
298
651
|
if (deps["next"]) return "next";
|
|
@@ -413,16 +766,16 @@ function stripFixloopSection(body) {
|
|
|
413
766
|
return [...lines.slice(0, realStart), ...trimmedTail].join("\n");
|
|
414
767
|
}
|
|
415
768
|
function appendClaudeMdGuidance(cwd, projectId, projectSlug) {
|
|
416
|
-
const claudePath =
|
|
769
|
+
const claudePath = join2(cwd, "CLAUDE.md");
|
|
417
770
|
const runtime = detectRuntime(cwd);
|
|
418
771
|
const section = buildClaudeMdSection(projectId, projectSlug, runtime);
|
|
419
|
-
if (
|
|
420
|
-
const existing =
|
|
772
|
+
if (existsSync2(claudePath)) {
|
|
773
|
+
const existing = readFileSync2(claudePath, "utf8");
|
|
421
774
|
const stripped = stripFixloopSection(existing);
|
|
422
775
|
const next = stripped.endsWith("\n") ? stripped + section.replace(/^\n/, "") : stripped + section;
|
|
423
|
-
|
|
776
|
+
writeFileSync2(claudePath, next);
|
|
424
777
|
} else {
|
|
425
|
-
|
|
778
|
+
writeFileSync2(claudePath, "# Project\n" + section);
|
|
426
779
|
}
|
|
427
780
|
}
|
|
428
781
|
function writeLocalDiscovery(opts) {
|
|
@@ -432,6 +785,61 @@ function writeLocalDiscovery(opts) {
|
|
|
432
785
|
appendEnvLocal(opts.cwd, opts.token, opts.projectId, opts.projectSlug, opts.endpoint);
|
|
433
786
|
appendClaudeMdGuidance(opts.cwd, opts.projectId, opts.projectSlug);
|
|
434
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
|
+
}
|
|
435
843
|
async function connect(args) {
|
|
436
844
|
const cwd = process.cwd();
|
|
437
845
|
const scopeRaw = (args.flags.scope ?? "deploy").toLowerCase();
|
|
@@ -448,25 +856,67 @@ async function connect(args) {
|
|
|
448
856
|
process.exit(1);
|
|
449
857
|
}
|
|
450
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;
|
|
451
861
|
let projectId = args.flags["project-id"] ?? process.env.FIXPROMPT_PROJECT_ID ?? null;
|
|
452
862
|
let projectSlug = args.flags["project-slug"] ?? null;
|
|
863
|
+
let freshRuntimeKey = null;
|
|
453
864
|
if (!projectId && projectSlug) {
|
|
454
865
|
const res = await fetch(`${endpoint}/admin/projects/by-slug/${encodeURIComponent(projectSlug)}`, {
|
|
455
866
|
headers: { "x-internal-admin-token": adminToken }
|
|
456
867
|
});
|
|
457
|
-
|
|
458
|
-
|
|
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();
|
|
459
875
|
console.error(`Project lookup failed (${res.status}): ${text.slice(0, 200)}`);
|
|
460
876
|
process.exit(1);
|
|
461
877
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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}`);
|
|
466
916
|
}
|
|
467
917
|
if (!projectId) {
|
|
468
918
|
console.error(
|
|
469
|
-
"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."
|
|
470
920
|
);
|
|
471
921
|
process.exit(1);
|
|
472
922
|
}
|
|
@@ -500,6 +950,18 @@ async function connect(args) {
|
|
|
500
950
|
const force = args.flags.force === true;
|
|
501
951
|
const scopes = scope === "both" ? ["deploy", "read"] : [scope];
|
|
502
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
|
+
}
|
|
503
965
|
for (const s of scopes) {
|
|
504
966
|
const envVar = ENV_VAR_BY_SCOPE[s];
|
|
505
967
|
const label = scope === "both" ? `${labelBase}-${s}` : labelBase;
|
|
@@ -521,6 +983,27 @@ async function connect(args) {
|
|
|
521
983
|
mintedTokens.push({ scope: s, token_id: minted.token_id, envVar });
|
|
522
984
|
}
|
|
523
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
|
+
}
|
|
524
1007
|
const deployMinted = mintedTokens.some((m) => m.scope === "deploy");
|
|
525
1008
|
console.log("");
|
|
526
1009
|
if (deployMinted && provider) {
|
|
@@ -547,7 +1030,7 @@ async function connect(args) {
|
|
|
547
1030
|
|
|
548
1031
|
// src/version.ts
|
|
549
1032
|
var CLI_NAME = "@fixprompt/cli";
|
|
550
|
-
var CLI_VERSION = "0.
|
|
1033
|
+
var CLI_VERSION = "0.6.0";
|
|
551
1034
|
|
|
552
1035
|
// src/cli.ts
|
|
553
1036
|
var DEFAULT_ENDPOINT2 = "https://geosloghub-production.up.railway.app";
|
|
@@ -587,13 +1070,19 @@ Usage:
|
|
|
587
1070
|
|
|
588
1071
|
connect options:
|
|
589
1072
|
--scope <s> deploy | read | both (default: deploy)
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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).
|
|
595
1083
|
--project-id <uuid> FixLoop project id (or $FIXPROMPT_PROJECT_ID)
|
|
596
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)
|
|
597
1086
|
--admin-token <fpa_\u2026> Broker admin token (or $FIXPROMPT_ADMIN_TOKEN)
|
|
598
1087
|
--provider <name> eas | vercel | github-actions (auto-detected from cwd)
|
|
599
1088
|
--eas-env <env> EAS env to write to (default: production)
|
|
@@ -602,6 +1091,9 @@ connect options:
|
|
|
602
1091
|
--label <text> Token label (default: <provider>-<timestamp>)
|
|
603
1092
|
--force Overwrite an existing env var value
|
|
604
1093
|
|
|
1094
|
+
Zero-paste flow (in your app repo root):
|
|
1095
|
+
FIXPROMPT_ADMIN_TOKEN=fpa_\u2026 npx @fixprompt/cli connect --scope both --create
|
|
1096
|
+
|
|
605
1097
|
deploy-start auth (pick one):
|
|
606
1098
|
--deploy-token <fpd_...> Deploy-only token from dashboard (recommended for CI)
|
|
607
1099
|
(or $FIXPROMPT_DEPLOY_TOKEN env)
|
package/package.json
CHANGED