@insforge/cli 0.1.70 → 0.1.72
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/index.js +825 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
5
|
+
import { join as join17, dirname as dirname3 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import * as
|
|
8
|
+
import * as clack17 from "@clack/prompts";
|
|
9
9
|
|
|
10
10
|
// src/lib/prompts.ts
|
|
11
11
|
import * as readline from "readline";
|
|
@@ -1329,36 +1329,36 @@ function registerBranchCreateCommand(branch) {
|
|
|
1329
1329
|
throw new CLIError(`Invalid --mode: ${opts.mode} (must be "full" or "schema-only")`);
|
|
1330
1330
|
}
|
|
1331
1331
|
const mode = opts.mode;
|
|
1332
|
-
const
|
|
1332
|
+
const spinner10 = !json ? clack5.spinner() : null;
|
|
1333
1333
|
let ready;
|
|
1334
1334
|
let provisioned = false;
|
|
1335
1335
|
try {
|
|
1336
|
-
|
|
1336
|
+
spinner10?.start(`Creating branch '${name}'...`);
|
|
1337
1337
|
const created = await createBranchApi(project.project_id, { mode, name }, apiUrl);
|
|
1338
1338
|
captureEvent(project.project_id, "cli_branch_create", {
|
|
1339
1339
|
mode,
|
|
1340
1340
|
parent_project_id: project.project_id
|
|
1341
1341
|
});
|
|
1342
|
-
|
|
1343
|
-
ready = await pollUntilReady(created.id, apiUrl,
|
|
1342
|
+
spinner10?.message(`Branch '${name}' created (appkey: ${created.appkey}). Provisioning...`);
|
|
1343
|
+
ready = await pollUntilReady(created.id, apiUrl, spinner10);
|
|
1344
1344
|
provisioned = ready.branch_state === "ready";
|
|
1345
1345
|
if (provisioned && opts.switch) {
|
|
1346
|
-
|
|
1346
|
+
spinner10?.message("Branch ready. Switching context...");
|
|
1347
1347
|
await runBranchSwitch({ name, apiUrl, json, silent: true });
|
|
1348
|
-
|
|
1348
|
+
spinner10?.stop(`Branch '${name}' is ready and active`);
|
|
1349
1349
|
} else if (provisioned) {
|
|
1350
|
-
|
|
1350
|
+
spinner10?.stop(`Branch '${name}' is ready`);
|
|
1351
1351
|
} else {
|
|
1352
|
-
|
|
1352
|
+
spinner10?.stop(`Branch '${name}' is in '${ready.branch_state}' state`);
|
|
1353
1353
|
}
|
|
1354
1354
|
} catch (err) {
|
|
1355
1355
|
if (provisioned) {
|
|
1356
|
-
|
|
1356
|
+
spinner10?.stop(
|
|
1357
1357
|
`Branch '${name}' is ready, but switching context failed \u2014 run \`insforge branch switch ${name}\` to retry`,
|
|
1358
1358
|
1
|
|
1359
1359
|
);
|
|
1360
1360
|
} else {
|
|
1361
|
-
|
|
1361
|
+
spinner10?.stop(`Branch '${name}' creation failed`, 1);
|
|
1362
1362
|
}
|
|
1363
1363
|
throw err;
|
|
1364
1364
|
}
|
|
@@ -1382,7 +1382,7 @@ function registerBranchCreateCommand(branch) {
|
|
|
1382
1382
|
}
|
|
1383
1383
|
});
|
|
1384
1384
|
}
|
|
1385
|
-
async function pollUntilReady(branchId, apiUrl,
|
|
1385
|
+
async function pollUntilReady(branchId, apiUrl, spinner10) {
|
|
1386
1386
|
const start = Date.now();
|
|
1387
1387
|
let lastState = "";
|
|
1388
1388
|
while (Date.now() - start < POLL_TIMEOUT_MS) {
|
|
@@ -1391,8 +1391,8 @@ async function pollUntilReady(branchId, apiUrl, spinner9) {
|
|
|
1391
1391
|
if (branch2.branch_state === "deleted" || branch2.branch_state === "conflicted") {
|
|
1392
1392
|
throw new CLIError(`Branch creation failed (state: ${branch2.branch_state})`);
|
|
1393
1393
|
}
|
|
1394
|
-
if (
|
|
1395
|
-
|
|
1394
|
+
if (spinner10 && branch2.branch_state !== lastState) {
|
|
1395
|
+
spinner10.message(`Provisioning branch (state: ${branch2.branch_state})...`);
|
|
1396
1396
|
lastState = branch2.branch_state;
|
|
1397
1397
|
}
|
|
1398
1398
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
@@ -1865,11 +1865,22 @@ async function getJwtSecret() {
|
|
|
1865
1865
|
return null;
|
|
1866
1866
|
}
|
|
1867
1867
|
}
|
|
1868
|
+
function spliceDatabasePassword(maskedUrl, password3) {
|
|
1869
|
+
return maskedUrl.replace(/^(postgresql:\/\/[^:]+:)[^@]+(@)/, `$1${password3}$2`);
|
|
1870
|
+
}
|
|
1868
1871
|
async function getDatabaseConnectionString() {
|
|
1869
1872
|
try {
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
+
const [urlRes, pwRes] = await Promise.all([
|
|
1874
|
+
ossFetch("/api/metadata/database-connection-string"),
|
|
1875
|
+
ossFetch("/api/metadata/database-password")
|
|
1876
|
+
]);
|
|
1877
|
+
const urlBody = await urlRes.json();
|
|
1878
|
+
const pwBody = await pwRes.json();
|
|
1879
|
+
const masked = urlBody.connectionURL;
|
|
1880
|
+
const password3 = pwBody.databasePassword;
|
|
1881
|
+
if (typeof masked !== "string" || !masked) return null;
|
|
1882
|
+
if (typeof password3 !== "string" || !password3) return null;
|
|
1883
|
+
return spliceDatabasePassword(masked, password3);
|
|
1873
1884
|
} catch {
|
|
1874
1885
|
return null;
|
|
1875
1886
|
}
|
|
@@ -2376,8 +2387,8 @@ async function startDirectDeployment(deploymentId, startBody) {
|
|
|
2376
2387
|
});
|
|
2377
2388
|
await response.json();
|
|
2378
2389
|
}
|
|
2379
|
-
async function pollDeployment(deploymentId,
|
|
2380
|
-
|
|
2390
|
+
async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
|
|
2391
|
+
spinner10?.message("Building and deploying...");
|
|
2381
2392
|
const startTime = Date.now();
|
|
2382
2393
|
let deployment = null;
|
|
2383
2394
|
while (Date.now() - startTime < POLL_TIMEOUT_MS3) {
|
|
@@ -2393,13 +2404,13 @@ async function pollDeployment(deploymentId, spinner9, syncBeforeRead) {
|
|
|
2393
2404
|
break;
|
|
2394
2405
|
}
|
|
2395
2406
|
if (status === "ERROR" || status === "CANCELED") {
|
|
2396
|
-
|
|
2407
|
+
spinner10?.stop("Deployment failed");
|
|
2397
2408
|
throw new CLIError(
|
|
2398
2409
|
getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`
|
|
2399
2410
|
);
|
|
2400
2411
|
}
|
|
2401
2412
|
const elapsed = Math.round((Date.now() - startTime) / 1e3);
|
|
2402
|
-
|
|
2413
|
+
spinner10?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
|
|
2403
2414
|
} catch (err) {
|
|
2404
2415
|
if (err instanceof CLIError) throw err;
|
|
2405
2416
|
}
|
|
@@ -2409,20 +2420,20 @@ async function pollDeployment(deploymentId, spinner9, syncBeforeRead) {
|
|
|
2409
2420
|
return { deploymentId, deployment, isReady, liveUrl };
|
|
2410
2421
|
}
|
|
2411
2422
|
async function deployProjectDirect(opts, config) {
|
|
2412
|
-
const { sourceDir, startBody = {}, spinner:
|
|
2413
|
-
|
|
2423
|
+
const { sourceDir, startBody = {}, spinner: spinner10 } = opts;
|
|
2424
|
+
spinner10?.start("Scanning source files...");
|
|
2414
2425
|
const localFiles = await collectDeploymentFiles(sourceDir);
|
|
2415
2426
|
if (localFiles.length === 0) {
|
|
2416
2427
|
throw new CLIError("No deployable files found in the source directory.");
|
|
2417
2428
|
}
|
|
2418
|
-
|
|
2429
|
+
spinner10?.message("Creating deployment...");
|
|
2419
2430
|
const createResult = await createDirectDeploymentSession(
|
|
2420
2431
|
config,
|
|
2421
2432
|
localFiles.map(({ path: relativePath, sha, size }) => ({ path: relativePath, sha, size }))
|
|
2422
2433
|
);
|
|
2423
2434
|
const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
|
|
2424
2435
|
const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
|
|
2425
|
-
|
|
2436
|
+
spinner10?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
|
|
2426
2437
|
await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
|
|
2427
2438
|
const localFile = localFileByPath.get(manifestFile.path);
|
|
2428
2439
|
if (!localFile) {
|
|
@@ -2433,18 +2444,18 @@ async function deployProjectDirect(opts, config) {
|
|
|
2433
2444
|
}
|
|
2434
2445
|
await uploadDirectDeploymentFile(createResult.id, manifestFile, localFile);
|
|
2435
2446
|
});
|
|
2436
|
-
|
|
2447
|
+
spinner10?.message("Starting deployment...");
|
|
2437
2448
|
await startDirectDeployment(createResult.id, startBody);
|
|
2438
|
-
return await pollDeployment(createResult.id,
|
|
2449
|
+
return await pollDeployment(createResult.id, spinner10, !isInsforgeCloudOssHost(config.oss_host));
|
|
2439
2450
|
}
|
|
2440
2451
|
async function deployProjectLegacy(opts) {
|
|
2441
|
-
const { sourceDir, startBody = {}, spinner:
|
|
2442
|
-
|
|
2452
|
+
const { sourceDir, startBody = {}, spinner: spinner10 } = opts;
|
|
2453
|
+
spinner10?.message("Creating deployment...");
|
|
2443
2454
|
const createRes = await ossFetch("/api/deployments", { method: "POST" });
|
|
2444
2455
|
const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
|
|
2445
|
-
|
|
2456
|
+
spinner10?.message("Compressing source files...");
|
|
2446
2457
|
const zipBuffer = await createZipBuffer(sourceDir);
|
|
2447
|
-
|
|
2458
|
+
spinner10?.message("Uploading...");
|
|
2448
2459
|
const formData = new FormData();
|
|
2449
2460
|
for (const [key, value] of Object.entries(uploadFields)) {
|
|
2450
2461
|
formData.append(key, value);
|
|
@@ -2455,13 +2466,13 @@ async function deployProjectLegacy(opts) {
|
|
|
2455
2466
|
const uploadErr = await uploadRes.text();
|
|
2456
2467
|
throw new CLIError(`Failed to upload: ${uploadErr}`);
|
|
2457
2468
|
}
|
|
2458
|
-
|
|
2469
|
+
spinner10?.message("Starting deployment...");
|
|
2459
2470
|
const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
|
|
2460
2471
|
method: "POST",
|
|
2461
2472
|
body: JSON.stringify(startBody)
|
|
2462
2473
|
});
|
|
2463
2474
|
await startRes.json();
|
|
2464
|
-
return await pollDeployment(deploymentId,
|
|
2475
|
+
return await pollDeployment(deploymentId, spinner10, false);
|
|
2465
2476
|
}
|
|
2466
2477
|
async function deployProject(opts) {
|
|
2467
2478
|
const config = getProjectConfig();
|
|
@@ -2496,7 +2507,7 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
2496
2507
|
`"${dirName}" is an excluded directory and cannot be used as a deploy source. Please specify your project root or output directory instead.`
|
|
2497
2508
|
);
|
|
2498
2509
|
}
|
|
2499
|
-
const
|
|
2510
|
+
const spinner10 = !json ? clack11.spinner() : null;
|
|
2500
2511
|
const startBody = {};
|
|
2501
2512
|
if (opts.env) {
|
|
2502
2513
|
try {
|
|
@@ -2522,9 +2533,9 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
2522
2533
|
throw new CLIError("Invalid --meta JSON.");
|
|
2523
2534
|
}
|
|
2524
2535
|
}
|
|
2525
|
-
const result = await deployProject({ sourceDir, startBody, spinner:
|
|
2536
|
+
const result = await deployProject({ sourceDir, startBody, spinner: spinner10 });
|
|
2526
2537
|
if (result.isReady) {
|
|
2527
|
-
|
|
2538
|
+
spinner10?.stop("Deployment complete");
|
|
2528
2539
|
if (json) {
|
|
2529
2540
|
outputJson(result.deployment);
|
|
2530
2541
|
} else {
|
|
@@ -2534,7 +2545,7 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
2534
2545
|
clack11.log.info(`Deployment ID: ${result.deploymentId}`);
|
|
2535
2546
|
}
|
|
2536
2547
|
} else {
|
|
2537
|
-
|
|
2548
|
+
spinner10?.stop("Deployment is still building");
|
|
2538
2549
|
if (json) {
|
|
2539
2550
|
outputJson({
|
|
2540
2551
|
id: result.deploymentId,
|
|
@@ -3100,17 +3111,37 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
|
|
|
3100
3111
|
// src/commands/projects/link.ts
|
|
3101
3112
|
var execAsync3 = promisify4(exec3);
|
|
3102
3113
|
async function runNpmInstall(startMessage = "Installing dependencies...") {
|
|
3103
|
-
const
|
|
3104
|
-
|
|
3114
|
+
const spinner10 = clack13.spinner();
|
|
3115
|
+
spinner10.start(startMessage);
|
|
3105
3116
|
try {
|
|
3106
3117
|
await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
3107
|
-
|
|
3118
|
+
spinner10.stop("Dependencies installed");
|
|
3108
3119
|
} catch (err) {
|
|
3109
|
-
|
|
3120
|
+
spinner10.stop("Failed to install dependencies");
|
|
3110
3121
|
clack13.log.warn(`npm install failed: ${err.message}`);
|
|
3111
3122
|
clack13.log.info("Run `npm install` manually to install dependencies.");
|
|
3112
3123
|
}
|
|
3113
3124
|
}
|
|
3125
|
+
async function runNpmSetupIfPresent() {
|
|
3126
|
+
const pkgPath = path5.join(process.cwd(), "package.json");
|
|
3127
|
+
let hasSetup = false;
|
|
3128
|
+
try {
|
|
3129
|
+
const pkg2 = JSON.parse(await fs5.readFile(pkgPath, "utf-8"));
|
|
3130
|
+
hasSetup = typeof pkg2.scripts?.setup === "string";
|
|
3131
|
+
} catch {
|
|
3132
|
+
}
|
|
3133
|
+
if (!hasSetup) return;
|
|
3134
|
+
const spinner10 = clack13.spinner();
|
|
3135
|
+
spinner10.start("Running setup (schema + migrations)...");
|
|
3136
|
+
try {
|
|
3137
|
+
await execAsync3("npm run setup", { cwd: process.cwd(), maxBuffer: 20 * 1024 * 1024 });
|
|
3138
|
+
spinner10.stop("Setup complete");
|
|
3139
|
+
} catch (err) {
|
|
3140
|
+
spinner10.stop("Setup failed");
|
|
3141
|
+
clack13.log.warn(`npm run setup failed: ${err.message.split("\n")[0]}`);
|
|
3142
|
+
clack13.log.info("Inspect the error, fix DATABASE_URL or network access, then run `npm run setup` manually.");
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3114
3145
|
function registerProjectLinkCommand(program2) {
|
|
3115
3146
|
program2.command("link").description("Link current directory to an InsForge project").option("--project-id <id>", "Project ID to link").option("--org-id <id>", "Organization ID").option("--template <template>", "Download a template after linking: react, nextjs, chatbot, crm, e-commerce, todo").option("--auth <provider>", "Wire a third-party auth provider into the chosen template (currently: better-auth)").option("--api-base-url <url>", "API Base URL for direct linking (OSS/Self-hosted)").option("--api-key <key>", "API Key for direct linking (OSS/Self-hosted)").action(async (opts, cmd) => {
|
|
3116
3147
|
const { json, apiUrl } = getRootOpts(cmd);
|
|
@@ -3196,6 +3227,9 @@ function registerProjectLinkCommand(program2) {
|
|
|
3196
3227
|
}
|
|
3197
3228
|
if (templateDownloaded && !json) {
|
|
3198
3229
|
await runNpmInstall();
|
|
3230
|
+
if (opts.auth) {
|
|
3231
|
+
await runNpmSetupIfPresent();
|
|
3232
|
+
}
|
|
3199
3233
|
}
|
|
3200
3234
|
await installSkills(json);
|
|
3201
3235
|
trackCommand("link", "oss-org", { direct: true, template: template2 });
|
|
@@ -3235,8 +3269,8 @@ function registerProjectLinkCommand(program2) {
|
|
|
3235
3269
|
}
|
|
3236
3270
|
if (result.packageJsonPatched && !json) {
|
|
3237
3271
|
await runNpmInstall("Installing new dependencies...");
|
|
3272
|
+
await runNpmSetupIfPresent();
|
|
3238
3273
|
}
|
|
3239
|
-
if (!json) clack13.note(result.nextSteps, "What's next");
|
|
3240
3274
|
} catch (err) {
|
|
3241
3275
|
const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
|
|
3242
3276
|
if (json) console.error(JSON.stringify({ warning: msg }));
|
|
@@ -3390,6 +3424,9 @@ function registerProjectLinkCommand(program2) {
|
|
|
3390
3424
|
}
|
|
3391
3425
|
if (templateDownloaded && !json) {
|
|
3392
3426
|
await runNpmInstall();
|
|
3427
|
+
if (opts.auth) {
|
|
3428
|
+
await runNpmSetupIfPresent();
|
|
3429
|
+
}
|
|
3393
3430
|
}
|
|
3394
3431
|
await installSkills(json);
|
|
3395
3432
|
await reportCliUsage("cli.link", true, 6, projectConfig);
|
|
@@ -3416,8 +3453,8 @@ function registerProjectLinkCommand(program2) {
|
|
|
3416
3453
|
}
|
|
3417
3454
|
if (result.packageJsonPatched && !json) {
|
|
3418
3455
|
await runNpmInstall("Installing new dependencies...");
|
|
3456
|
+
await runNpmSetupIfPresent();
|
|
3419
3457
|
}
|
|
3420
|
-
if (!json) clack13.note(result.nextSteps, "What's next");
|
|
3421
3458
|
} catch (err) {
|
|
3422
3459
|
const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
|
|
3423
3460
|
if (json) console.error(JSON.stringify({ warning: msg }));
|
|
@@ -4236,12 +4273,14 @@ function registerDbConnectionStringCommand(dbCmd2) {
|
|
|
4236
4273
|
const { json } = getRootOpts(cmd);
|
|
4237
4274
|
try {
|
|
4238
4275
|
await requireAuth();
|
|
4239
|
-
const
|
|
4240
|
-
|
|
4276
|
+
const url = await getDatabaseConnectionString();
|
|
4277
|
+
if (!url) {
|
|
4278
|
+
throw new CLIError("Could not fetch the database connection string. This command requires a cloud project (self-hosted instances expose Postgres directly via your docker-compose).");
|
|
4279
|
+
}
|
|
4241
4280
|
if (json) {
|
|
4242
|
-
outputJson(
|
|
4281
|
+
outputJson({ connectionURL: url });
|
|
4243
4282
|
} else {
|
|
4244
|
-
console.log(
|
|
4283
|
+
console.log(url);
|
|
4245
4284
|
}
|
|
4246
4285
|
await reportCliUsage("cli.db.connection-string", true);
|
|
4247
4286
|
} catch (err) {
|
|
@@ -6853,7 +6892,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
|
|
|
6853
6892
|
const s = !json ? clack15.spinner() : null;
|
|
6854
6893
|
s?.start("Collecting diagnostic data...");
|
|
6855
6894
|
const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
|
|
6856
|
-
const cliVersion = "0.1.
|
|
6895
|
+
const cliVersion = "0.1.72";
|
|
6857
6896
|
s?.stop("Data collected");
|
|
6858
6897
|
if (!json) {
|
|
6859
6898
|
console.log(`
|
|
@@ -8216,9 +8255,739 @@ function registerPaymentsCommands(paymentsCmd2) {
|
|
|
8216
8255
|
registerPaymentsHistoryCommand(paymentsCmd2);
|
|
8217
8256
|
}
|
|
8218
8257
|
|
|
8258
|
+
// src/commands/posthog/setup.ts
|
|
8259
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync3 } from "fs";
|
|
8260
|
+
import { join as join16, dirname as dirname2 } from "path";
|
|
8261
|
+
import * as clack16 from "@clack/prompts";
|
|
8262
|
+
import pc3 from "picocolors";
|
|
8263
|
+
|
|
8264
|
+
// src/lib/api/posthog.ts
|
|
8265
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
8266
|
+
async function fetchWithTimeout(url, init, callerSignal) {
|
|
8267
|
+
const ac = new AbortController();
|
|
8268
|
+
const timer = setTimeout(() => ac.abort(), REQUEST_TIMEOUT_MS);
|
|
8269
|
+
const onCallerAbort = () => ac.abort();
|
|
8270
|
+
callerSignal?.addEventListener("abort", onCallerAbort);
|
|
8271
|
+
try {
|
|
8272
|
+
return await fetch(url, { ...init, signal: ac.signal });
|
|
8273
|
+
} finally {
|
|
8274
|
+
clearTimeout(timer);
|
|
8275
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
8276
|
+
}
|
|
8277
|
+
}
|
|
8278
|
+
async function fetchPosthogConnection(projectId, jwt, apiUrl, signal) {
|
|
8279
|
+
const baseUrl = getPlatformApiUrl(apiUrl);
|
|
8280
|
+
const url = `${baseUrl}/integrations/posthog/v1/connection?project_id=${encodeURIComponent(projectId)}`;
|
|
8281
|
+
let res;
|
|
8282
|
+
try {
|
|
8283
|
+
res = await fetchWithTimeout(
|
|
8284
|
+
url,
|
|
8285
|
+
{
|
|
8286
|
+
method: "GET",
|
|
8287
|
+
headers: {
|
|
8288
|
+
Authorization: `Bearer ${jwt}`,
|
|
8289
|
+
Accept: "application/json"
|
|
8290
|
+
}
|
|
8291
|
+
},
|
|
8292
|
+
signal
|
|
8293
|
+
);
|
|
8294
|
+
} catch (err) {
|
|
8295
|
+
return { kind: "error", message: formatFetchError(err, url) };
|
|
8296
|
+
}
|
|
8297
|
+
if (res.status === 404) {
|
|
8298
|
+
return { kind: "not-connected" };
|
|
8299
|
+
}
|
|
8300
|
+
if (res.status === 403) {
|
|
8301
|
+
const body = await res.json().catch(() => ({}));
|
|
8302
|
+
return {
|
|
8303
|
+
kind: "forbidden",
|
|
8304
|
+
message: body.error ?? "Forbidden \u2014 you may not have access to this project."
|
|
8305
|
+
};
|
|
8306
|
+
}
|
|
8307
|
+
if (!res.ok) {
|
|
8308
|
+
const body = await res.json().catch(() => ({}));
|
|
8309
|
+
return {
|
|
8310
|
+
kind: "error",
|
|
8311
|
+
message: body.error ?? `Request failed: HTTP ${res.status}`,
|
|
8312
|
+
status: res.status
|
|
8313
|
+
};
|
|
8314
|
+
}
|
|
8315
|
+
let data;
|
|
8316
|
+
try {
|
|
8317
|
+
data = await res.json();
|
|
8318
|
+
} catch (err) {
|
|
8319
|
+
return {
|
|
8320
|
+
kind: "error",
|
|
8321
|
+
message: `Could not parse connection response: ${err.message}`
|
|
8322
|
+
};
|
|
8323
|
+
}
|
|
8324
|
+
const conn = data ?? {};
|
|
8325
|
+
if (!conn.apiKey) {
|
|
8326
|
+
return { kind: "not-connected" };
|
|
8327
|
+
}
|
|
8328
|
+
if (conn.status && conn.status !== "active") {
|
|
8329
|
+
return { kind: "not-connected" };
|
|
8330
|
+
}
|
|
8331
|
+
return { kind: "connected", connection: conn };
|
|
8332
|
+
}
|
|
8333
|
+
async function pollPosthogConnection(projectId, jwt, opts, apiUrl) {
|
|
8334
|
+
const start = Date.now();
|
|
8335
|
+
let consecutiveErrors = 0;
|
|
8336
|
+
for (; ; ) {
|
|
8337
|
+
if (opts.signal?.aborted) {
|
|
8338
|
+
throw new CLIError("Connection wait cancelled.");
|
|
8339
|
+
}
|
|
8340
|
+
const elapsed = Date.now() - start;
|
|
8341
|
+
if (elapsed >= opts.timeoutMs) {
|
|
8342
|
+
throw new CLIError(
|
|
8343
|
+
"Timed out waiting for PostHog connection. Re-run `insforge posthog setup` after authorizing."
|
|
8344
|
+
);
|
|
8345
|
+
}
|
|
8346
|
+
opts.onTick?.(elapsed);
|
|
8347
|
+
const result = await fetchPosthogConnection(projectId, jwt, apiUrl, opts.signal);
|
|
8348
|
+
switch (result.kind) {
|
|
8349
|
+
case "connected":
|
|
8350
|
+
return result.connection;
|
|
8351
|
+
case "forbidden":
|
|
8352
|
+
throw new CLIError(`Forbidden: ${result.message}`, 5);
|
|
8353
|
+
case "error":
|
|
8354
|
+
consecutiveErrors += 1;
|
|
8355
|
+
if (consecutiveErrors > opts.maxTransientRetries) {
|
|
8356
|
+
throw new CLIError(
|
|
8357
|
+
`Connection check failed after ${opts.maxTransientRetries} retries: ${result.message}`
|
|
8358
|
+
);
|
|
8359
|
+
}
|
|
8360
|
+
break;
|
|
8361
|
+
case "not-connected":
|
|
8362
|
+
consecutiveErrors = 0;
|
|
8363
|
+
break;
|
|
8364
|
+
}
|
|
8365
|
+
await sleep(opts.intervalMs, opts.signal);
|
|
8366
|
+
}
|
|
8367
|
+
}
|
|
8368
|
+
async function startPosthogCliFlow(projectId, jwt, apiUrl) {
|
|
8369
|
+
const baseUrl = getPlatformApiUrl(apiUrl);
|
|
8370
|
+
const url = `${baseUrl}/integrations/posthog/v1/cli-start?p=${encodeURIComponent(projectId)}`;
|
|
8371
|
+
let res;
|
|
8372
|
+
try {
|
|
8373
|
+
res = await fetchWithTimeout(url, {
|
|
8374
|
+
method: "GET",
|
|
8375
|
+
headers: {
|
|
8376
|
+
Authorization: `Bearer ${jwt}`,
|
|
8377
|
+
Accept: "application/json"
|
|
8378
|
+
}
|
|
8379
|
+
});
|
|
8380
|
+
} catch (err) {
|
|
8381
|
+
throw new CLIError(`Failed to start PostHog connect flow: ${formatFetchError(err, url)}`);
|
|
8382
|
+
}
|
|
8383
|
+
if (!res.ok) {
|
|
8384
|
+
const body = await res.json().catch(() => ({}));
|
|
8385
|
+
const msg = body.error ?? res.statusText ?? `HTTP ${res.status}`;
|
|
8386
|
+
if (res.status === 401) {
|
|
8387
|
+
throw new CLIError(`Not authenticated (HTTP 401): ${msg}. Re-run \`insforge login\`.`);
|
|
8388
|
+
}
|
|
8389
|
+
if (res.status === 403) {
|
|
8390
|
+
throw new CLIError(`Forbidden (HTTP 403): ${msg}`, 5);
|
|
8391
|
+
}
|
|
8392
|
+
if (res.status === 404) {
|
|
8393
|
+
throw new CLIError(
|
|
8394
|
+
`PostHog connect flow unavailable (HTTP 404): ${msg}. Check that the project is linked.`
|
|
8395
|
+
);
|
|
8396
|
+
}
|
|
8397
|
+
throw new CLIError(`PostHog cli-start failed (HTTP ${res.status}): ${msg}`);
|
|
8398
|
+
}
|
|
8399
|
+
const data = await res.json().catch(() => ({}));
|
|
8400
|
+
if (data.type === "connected") {
|
|
8401
|
+
return { type: "connected" };
|
|
8402
|
+
}
|
|
8403
|
+
if (data.type === "authorize" && typeof data.authorizeUrl === "string" && data.authorizeUrl) {
|
|
8404
|
+
return { type: "authorize", authorizeUrl: data.authorizeUrl };
|
|
8405
|
+
}
|
|
8406
|
+
throw new CLIError("PostHog cli-start returned an unexpected response shape.");
|
|
8407
|
+
}
|
|
8408
|
+
function sleep(ms, signal) {
|
|
8409
|
+
return new Promise((resolve5, reject) => {
|
|
8410
|
+
if (signal?.aborted) {
|
|
8411
|
+
reject(new CLIError("Connection wait cancelled."));
|
|
8412
|
+
return;
|
|
8413
|
+
}
|
|
8414
|
+
const timer = setTimeout(() => {
|
|
8415
|
+
signal?.removeEventListener("abort", onAbort);
|
|
8416
|
+
resolve5();
|
|
8417
|
+
}, ms);
|
|
8418
|
+
const onAbort = () => {
|
|
8419
|
+
clearTimeout(timer);
|
|
8420
|
+
reject(new CLIError("Connection wait cancelled."));
|
|
8421
|
+
};
|
|
8422
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
8423
|
+
});
|
|
8424
|
+
}
|
|
8425
|
+
|
|
8426
|
+
// src/lib/framework-detect.ts
|
|
8427
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
8428
|
+
import { join as join14 } from "path";
|
|
8429
|
+
function contextFromCwd(cwd) {
|
|
8430
|
+
let pkg2 = null;
|
|
8431
|
+
const pkgPath = join14(cwd, "package.json");
|
|
8432
|
+
if (existsSync10(pkgPath)) {
|
|
8433
|
+
try {
|
|
8434
|
+
pkg2 = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
8435
|
+
} catch {
|
|
8436
|
+
pkg2 = null;
|
|
8437
|
+
}
|
|
8438
|
+
}
|
|
8439
|
+
return {
|
|
8440
|
+
hasDir: (rel) => existsSync10(join14(cwd, rel)),
|
|
8441
|
+
pkg: pkg2
|
|
8442
|
+
};
|
|
8443
|
+
}
|
|
8444
|
+
function hasDep(pkg2, name) {
|
|
8445
|
+
if (!pkg2) return false;
|
|
8446
|
+
return Boolean(pkg2.dependencies?.[name] ?? pkg2.devDependencies?.[name]);
|
|
8447
|
+
}
|
|
8448
|
+
function detectFramework(ctx) {
|
|
8449
|
+
if (hasDep(ctx.pkg, "next")) {
|
|
8450
|
+
const hasApp = ctx.hasDir("app") || ctx.hasDir("src/app");
|
|
8451
|
+
const hasPages = ctx.hasDir("pages") || ctx.hasDir("src/pages");
|
|
8452
|
+
if (hasApp && !hasPages) return "next-app";
|
|
8453
|
+
if (hasPages && !hasApp) return "next-pages";
|
|
8454
|
+
if (hasApp && hasPages) return "next-app";
|
|
8455
|
+
return "next-app";
|
|
8456
|
+
}
|
|
8457
|
+
if (hasDep(ctx.pkg, "vite") && hasDep(ctx.pkg, "react")) {
|
|
8458
|
+
return "vite-react";
|
|
8459
|
+
}
|
|
8460
|
+
if (hasDep(ctx.pkg, "@sveltejs/kit")) {
|
|
8461
|
+
return "sveltekit";
|
|
8462
|
+
}
|
|
8463
|
+
if (hasDep(ctx.pkg, "astro")) {
|
|
8464
|
+
return "astro";
|
|
8465
|
+
}
|
|
8466
|
+
return null;
|
|
8467
|
+
}
|
|
8468
|
+
|
|
8469
|
+
// src/lib/package-manager.ts
|
|
8470
|
+
import { existsSync as existsSync11 } from "fs";
|
|
8471
|
+
import { join as join15 } from "path";
|
|
8472
|
+
import { exec as exec4 } from "child_process";
|
|
8473
|
+
import { promisify as promisify5 } from "util";
|
|
8474
|
+
var execAsync4 = promisify5(exec4);
|
|
8475
|
+
function detectPackageManager(cwd) {
|
|
8476
|
+
if (existsSync11(join15(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
8477
|
+
if (existsSync11(join15(cwd, "yarn.lock"))) return "yarn";
|
|
8478
|
+
if (existsSync11(join15(cwd, "bun.lockb")) || existsSync11(join15(cwd, "bun.lock"))) {
|
|
8479
|
+
return "bun";
|
|
8480
|
+
}
|
|
8481
|
+
return "npm";
|
|
8482
|
+
}
|
|
8483
|
+
function installCommand(pm, pkg2) {
|
|
8484
|
+
switch (pm) {
|
|
8485
|
+
case "pnpm":
|
|
8486
|
+
return `pnpm add ${pkg2}`;
|
|
8487
|
+
case "yarn":
|
|
8488
|
+
return `yarn add ${pkg2}`;
|
|
8489
|
+
case "bun":
|
|
8490
|
+
return `bun add ${pkg2}`;
|
|
8491
|
+
case "npm":
|
|
8492
|
+
default:
|
|
8493
|
+
return `npm install ${pkg2}`;
|
|
8494
|
+
}
|
|
8495
|
+
}
|
|
8496
|
+
function hasPackage(pkg2, name) {
|
|
8497
|
+
if (!pkg2) return false;
|
|
8498
|
+
return Boolean(pkg2.dependencies?.[name] ?? pkg2.devDependencies?.[name]);
|
|
8499
|
+
}
|
|
8500
|
+
async function runInstall(pm, pkgName, cwd) {
|
|
8501
|
+
const cmd = installCommand(pm, pkgName);
|
|
8502
|
+
await execAsync4(cmd, { cwd, maxBuffer: 16 * 1024 * 1024 });
|
|
8503
|
+
}
|
|
8504
|
+
|
|
8505
|
+
// src/lib/env-writer.ts
|
|
8506
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
|
|
8507
|
+
var KEY_LINE_RE = (key) => (
|
|
8508
|
+
// Match `KEY=...` at the start of a line (allowing leading whitespace).
|
|
8509
|
+
// Captures the value side; we only need the value portion to compare.
|
|
8510
|
+
new RegExp(`^\\s*${key.replace(/[$.*+?^()[\\]{}|]/g, "\\$&")}\\s*=\\s*(.*)$`, "m")
|
|
8511
|
+
);
|
|
8512
|
+
function stripQuotes(v) {
|
|
8513
|
+
const t = v.trim();
|
|
8514
|
+
if (t.startsWith('"') && t.endsWith('"') && t.length >= 2 || t.startsWith("'") && t.endsWith("'") && t.length >= 2) {
|
|
8515
|
+
return t.slice(1, -1);
|
|
8516
|
+
}
|
|
8517
|
+
const hash = t.indexOf(" #");
|
|
8518
|
+
return hash >= 0 ? t.slice(0, hash).trimEnd() : t;
|
|
8519
|
+
}
|
|
8520
|
+
function upsertEnvFile(path6, entries) {
|
|
8521
|
+
const exists = existsSync12(path6);
|
|
8522
|
+
let content = exists ? readFileSync9(path6, "utf-8") : "";
|
|
8523
|
+
const result = { added: [], skipped: [], mismatched: [] };
|
|
8524
|
+
const additions = [];
|
|
8525
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
8526
|
+
const re = KEY_LINE_RE(key);
|
|
8527
|
+
const match = content.match(re);
|
|
8528
|
+
if (match) {
|
|
8529
|
+
const existingValue = stripQuotes(match[1] ?? "");
|
|
8530
|
+
if (existingValue === value) {
|
|
8531
|
+
result.skipped.push(key);
|
|
8532
|
+
} else {
|
|
8533
|
+
result.mismatched.push({ key, existingValue, newValue: value });
|
|
8534
|
+
}
|
|
8535
|
+
continue;
|
|
8536
|
+
}
|
|
8537
|
+
additions.push(`${key}=${value}`);
|
|
8538
|
+
result.added.push(key);
|
|
8539
|
+
}
|
|
8540
|
+
if (additions.length > 0) {
|
|
8541
|
+
if (content.length > 0 && !content.endsWith("\n")) {
|
|
8542
|
+
content += "\n";
|
|
8543
|
+
}
|
|
8544
|
+
content += additions.join("\n") + "\n";
|
|
8545
|
+
writeFileSync7(path6, content);
|
|
8546
|
+
} else if (!exists) {
|
|
8547
|
+
}
|
|
8548
|
+
return result;
|
|
8549
|
+
}
|
|
8550
|
+
|
|
8551
|
+
// src/templates/posthog/next-app/posthog-provider.tsx.txt
|
|
8552
|
+
var posthog_provider_tsx_default = "'use client';\n\nimport { useEffect } from 'react';\nimport posthog from 'posthog-js';\n\n// PostHog client-side provider for the Next.js App Router.\n// Initialises posthog-js exactly once on the client; SSR is skipped because\n// `useEffect` only runs in the browser.\nexport function PostHogProvider({ children }: { children: React.ReactNode }) {\n useEffect(() => {\n if (typeof window === 'undefined') return;\n if (posthog.__loaded) return;\n\n const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;\n if (!key) {\n // Fail closed in production: missing env var \u2192 no init, no events.\n // Avoids accidentally firing events without a key in CI/preview builds.\n return;\n }\n\n posthog.init(key, {\n api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || '{{HOST}}',\n capture_pageview: true,\n capture_pageleave: true,\n });\n }, []);\n\n return <>{children}</>;\n}\n";
|
|
8553
|
+
|
|
8554
|
+
// src/templates/posthog/next-app/layout-snippet.tsx.txt
|
|
8555
|
+
var layout_snippet_tsx_default = `// Wrap your <body> children with <PostHogProvider> in app/layout.tsx:
|
|
8556
|
+
//
|
|
8557
|
+
// import { PostHogProvider } from './posthog-provider';
|
|
8558
|
+
//
|
|
8559
|
+
// export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
8560
|
+
// return (
|
|
8561
|
+
// <html lang="en">
|
|
8562
|
+
// <body>
|
|
8563
|
+
// <PostHogProvider>{children}</PostHogProvider>
|
|
8564
|
+
// </body>
|
|
8565
|
+
// </html>
|
|
8566
|
+
// );
|
|
8567
|
+
// }
|
|
8568
|
+
`;
|
|
8569
|
+
|
|
8570
|
+
// src/templates/posthog/next-pages/_app.tsx.txt
|
|
8571
|
+
var app_tsx_default = "import type { AppProps } from 'next/app';\nimport { useEffect } from 'react';\nimport posthog from 'posthog-js';\n\nif (typeof window !== 'undefined') {\n const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;\n if (key && !posthog.__loaded) {\n posthog.init(key, {\n api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || '{{HOST}}',\n capture_pageview: true,\n capture_pageleave: true,\n });\n }\n}\n\nexport default function App({ Component, pageProps }: AppProps) {\n useEffect(() => {\n // Capture pageviews on client-side route changes.\n const handleRouteChange = () => posthog.capture('$pageview');\n if (typeof window !== 'undefined') {\n window.addEventListener('popstate', handleRouteChange);\n return () => window.removeEventListener('popstate', handleRouteChange);\n }\n }, []);\n\n return <Component {...pageProps} />;\n}\n";
|
|
8572
|
+
|
|
8573
|
+
// src/templates/posthog/vite-react/main-snippet.tsx.txt
|
|
8574
|
+
var main_snippet_tsx_default = "// Add this near the top of src/main.tsx, before ReactDOM.createRoot:\nimport posthog from 'posthog-js';\n\nconst posthogKey = import.meta.env.VITE_PUBLIC_POSTHOG_KEY;\nif (posthogKey) {\n posthog.init(posthogKey, {\n api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST || '{{HOST}}',\n capture_pageview: true,\n capture_pageleave: true,\n });\n}\n";
|
|
8575
|
+
|
|
8576
|
+
// src/templates/posthog/sveltekit/hooks.client.ts.txt
|
|
8577
|
+
var hooks_client_ts_default = "import posthog from 'posthog-js';\nimport { browser } from '$app/environment';\nimport { PUBLIC_POSTHOG_KEY, PUBLIC_POSTHOG_HOST } from '$env/static/public';\n\n// `hooks.client.ts` only runs in the browser, so we don't need an explicit\n// `typeof window` guard. The `browser` import is included so future edits\n// (e.g. moving init to a non-client hook) don't accidentally fire on the server.\nif (browser && PUBLIC_POSTHOG_KEY) {\n posthog.init(PUBLIC_POSTHOG_KEY, {\n api_host: PUBLIC_POSTHOG_HOST || '{{HOST}}',\n capture_pageview: true,\n capture_pageleave: true,\n });\n}\n\nexport const handleError = ({ error }: { error: unknown }) => {\n posthog.capture('$exception', { error: String(error) });\n};\n";
|
|
8578
|
+
|
|
8579
|
+
// src/templates/posthog/astro/posthog-init.ts.txt
|
|
8580
|
+
var posthog_init_ts_default = "import posthog from 'posthog-js';\n\n// PostHog client init for Astro. This module runs only in the browser bundle\n// (Astro inlines `client:load` / `<script>` imports into client JS). We still\n// guard with `typeof window` because the same file may be transitively\n// imported during SSR \u2014 the guard prevents init from accidentally running on\n// the server during static generation.\nif (typeof window !== 'undefined') {\n const key = import.meta.env.PUBLIC_POSTHOG_KEY;\n if (key) {\n posthog.init(key, {\n api_host: import.meta.env.PUBLIC_POSTHOG_HOST || '{{HOST}}',\n capture_pageview: 'history_change',\n capture_pageleave: true,\n });\n }\n}\n";
|
|
8581
|
+
|
|
8582
|
+
// src/templates/posthog/index.ts
|
|
8583
|
+
var templates = {
|
|
8584
|
+
"next-app": {
|
|
8585
|
+
provider: posthog_provider_tsx_default,
|
|
8586
|
+
layoutSnippet: layout_snippet_tsx_default
|
|
8587
|
+
},
|
|
8588
|
+
"next-pages": {
|
|
8589
|
+
app: app_tsx_default
|
|
8590
|
+
},
|
|
8591
|
+
"vite-react": {
|
|
8592
|
+
mainSnippet: main_snippet_tsx_default
|
|
8593
|
+
},
|
|
8594
|
+
sveltekit: {
|
|
8595
|
+
hooks: hooks_client_ts_default
|
|
8596
|
+
},
|
|
8597
|
+
astro: {
|
|
8598
|
+
init: posthog_init_ts_default
|
|
8599
|
+
}
|
|
8600
|
+
};
|
|
8601
|
+
function renderTemplate(raw, vars) {
|
|
8602
|
+
return raw.replace(/\{\{([A-Z0-9_]+)\}\}/g, (match, key) => {
|
|
8603
|
+
return Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match;
|
|
8604
|
+
});
|
|
8605
|
+
}
|
|
8606
|
+
|
|
8607
|
+
// src/commands/posthog/setup.ts
|
|
8608
|
+
var POLL_INTERVAL_MS4 = 2e3;
|
|
8609
|
+
var POLL_TIMEOUT_MS4 = 15 * 60 * 1e3;
|
|
8610
|
+
var MAX_TRANSIENT_RETRIES = 5;
|
|
8611
|
+
function registerPosthogSetupCommand(program2) {
|
|
8612
|
+
program2.command("setup").description("Install the PostHog SDK into the current directory app").option("--framework <name>", "Force framework (next-app|next-pages|vite-react|sveltekit|astro)").option("--skip-install", "Do not run the package manager install step").option("--skip-browser", "Do not auto-open the browser; only print the URL").action(async (opts, cmd) => {
|
|
8613
|
+
const { json, apiUrl } = getRootOpts(cmd);
|
|
8614
|
+
try {
|
|
8615
|
+
const result = await runSetup({
|
|
8616
|
+
json,
|
|
8617
|
+
apiUrl,
|
|
8618
|
+
forceFramework: opts.framework,
|
|
8619
|
+
skipInstall: Boolean(opts.skipInstall),
|
|
8620
|
+
skipBrowser: Boolean(opts.skipBrowser)
|
|
8621
|
+
});
|
|
8622
|
+
if (json) {
|
|
8623
|
+
outputJson({ success: true, ...result });
|
|
8624
|
+
}
|
|
8625
|
+
} catch (err) {
|
|
8626
|
+
handleError(err, json);
|
|
8627
|
+
}
|
|
8628
|
+
});
|
|
8629
|
+
}
|
|
8630
|
+
async function runSetup(opts) {
|
|
8631
|
+
const proj = getProjectConfig();
|
|
8632
|
+
if (!proj || !proj.project_id) {
|
|
8633
|
+
throw new ProjectNotLinkedError();
|
|
8634
|
+
}
|
|
8635
|
+
const token = getAccessToken();
|
|
8636
|
+
if (!token) {
|
|
8637
|
+
throw new AuthError("Not logged in. Run `insforge login` first.");
|
|
8638
|
+
}
|
|
8639
|
+
if (!opts.json) {
|
|
8640
|
+
clack16.intro("PostHog setup");
|
|
8641
|
+
outputSuccess(`Linked to InsForge project: ${proj.project_name} (${proj.project_id})`);
|
|
8642
|
+
}
|
|
8643
|
+
const startResult = await startPosthogCliFlow(proj.project_id, token, opts.apiUrl);
|
|
8644
|
+
let conn;
|
|
8645
|
+
if (startResult.type === "connected") {
|
|
8646
|
+
if (!opts.json) {
|
|
8647
|
+
outputSuccess("PostHog already connected (or auto-provisioned for new user). Continuing...");
|
|
8648
|
+
}
|
|
8649
|
+
const fetchResult = await fetchPosthogConnection(proj.project_id, token, opts.apiUrl);
|
|
8650
|
+
if (fetchResult.kind !== "connected") {
|
|
8651
|
+
throw new CLIError(
|
|
8652
|
+
"cli-start reported connected, but /connection returned not-connected. Try again, or check the dashboard."
|
|
8653
|
+
);
|
|
8654
|
+
}
|
|
8655
|
+
conn = fetchResult.connection;
|
|
8656
|
+
} else {
|
|
8657
|
+
conn = await runConnectFlow(proj.project_id, token, startResult.authorizeUrl, opts);
|
|
8658
|
+
}
|
|
8659
|
+
if (!conn.apiKey) {
|
|
8660
|
+
throw new CLIError(
|
|
8661
|
+
"Connection succeeded but cloud-backend returned no apiKey. Try again or check the dashboard."
|
|
8662
|
+
);
|
|
8663
|
+
}
|
|
8664
|
+
const framework = resolveFramework(opts);
|
|
8665
|
+
if (framework === null) {
|
|
8666
|
+
return reportNoFramework(conn, opts);
|
|
8667
|
+
}
|
|
8668
|
+
if (!opts.json) outputSuccess(`Detected framework: ${frameworkLabel(framework)}`);
|
|
8669
|
+
const cwd = process.cwd();
|
|
8670
|
+
const ctx = contextFromCwd(cwd);
|
|
8671
|
+
const pm = detectPackageManager(cwd);
|
|
8672
|
+
const alreadyInstalled = hasPackage(ctx.pkg, "posthog-js");
|
|
8673
|
+
let installedSdk = false;
|
|
8674
|
+
if (alreadyInstalled) {
|
|
8675
|
+
if (!opts.json) outputInfo(pc3.dim("posthog-js is already installed \u2014 skipping install."));
|
|
8676
|
+
} else if (opts.skipInstall) {
|
|
8677
|
+
if (!opts.json) {
|
|
8678
|
+
outputInfo(pc3.yellow(`Skipping install. Run manually: ${installCommand(pm, "posthog-js")}`));
|
|
8679
|
+
}
|
|
8680
|
+
} else {
|
|
8681
|
+
installedSdk = await installSdk(pm, cwd, opts);
|
|
8682
|
+
}
|
|
8683
|
+
const filesWritten = [];
|
|
8684
|
+
const notes = [];
|
|
8685
|
+
const envResult = writeForFramework(framework, conn, cwd, filesWritten, notes, opts);
|
|
8686
|
+
if (!opts.json) {
|
|
8687
|
+
if (notes.length > 0) {
|
|
8688
|
+
for (const n of notes) clack16.log.info(n);
|
|
8689
|
+
}
|
|
8690
|
+
clack16.outro("Done. Run your dev server to start sending events.");
|
|
8691
|
+
}
|
|
8692
|
+
return {
|
|
8693
|
+
framework,
|
|
8694
|
+
installedSdk,
|
|
8695
|
+
filesWritten,
|
|
8696
|
+
envWritten: envResult,
|
|
8697
|
+
notes
|
|
8698
|
+
};
|
|
8699
|
+
}
|
|
8700
|
+
async function runConnectFlow(projectId, token, authorizeUrl, opts) {
|
|
8701
|
+
if (opts.json) {
|
|
8702
|
+
process.stderr.write(`Authorize PostHog: ${authorizeUrl}
|
|
8703
|
+
`);
|
|
8704
|
+
process.stderr.write("Your browser should open automatically. If not, copy the URL above.\n");
|
|
8705
|
+
} else {
|
|
8706
|
+
clack16.log.info("PostHog is not connected to this project yet.");
|
|
8707
|
+
outputInfo("");
|
|
8708
|
+
outputInfo(`Open this URL to authorize PostHog:
|
|
8709
|
+
${pc3.cyan(pc3.underline(authorizeUrl))}`);
|
|
8710
|
+
outputInfo("");
|
|
8711
|
+
}
|
|
8712
|
+
if (!opts.skipBrowser) {
|
|
8713
|
+
try {
|
|
8714
|
+
const open = (await import("open")).default;
|
|
8715
|
+
await open(authorizeUrl);
|
|
8716
|
+
} catch {
|
|
8717
|
+
}
|
|
8718
|
+
}
|
|
8719
|
+
const spinner10 = !opts.json && isInteractive ? clack16.spinner() : null;
|
|
8720
|
+
spinner10?.start("Waiting for connection... (timeout: 15 minutes)");
|
|
8721
|
+
try {
|
|
8722
|
+
const conn = await pollPosthogConnection(
|
|
8723
|
+
projectId,
|
|
8724
|
+
token,
|
|
8725
|
+
{
|
|
8726
|
+
intervalMs: POLL_INTERVAL_MS4,
|
|
8727
|
+
timeoutMs: POLL_TIMEOUT_MS4,
|
|
8728
|
+
maxTransientRetries: MAX_TRANSIENT_RETRIES,
|
|
8729
|
+
onTick: (elapsed) => {
|
|
8730
|
+
if (spinner10) {
|
|
8731
|
+
const secs = Math.floor(elapsed / 1e3);
|
|
8732
|
+
const mins = Math.floor(secs / 60);
|
|
8733
|
+
const remaining = `${mins}m ${secs % 60}s elapsed`;
|
|
8734
|
+
spinner10.message(`Waiting for connection... (${remaining})`);
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8737
|
+
},
|
|
8738
|
+
opts.apiUrl
|
|
8739
|
+
);
|
|
8740
|
+
spinner10?.stop("Connection received from PostHog.");
|
|
8741
|
+
return conn;
|
|
8742
|
+
} catch (err) {
|
|
8743
|
+
spinner10?.stop("Connection wait failed.");
|
|
8744
|
+
throw err;
|
|
8745
|
+
}
|
|
8746
|
+
}
|
|
8747
|
+
function resolveFramework(opts) {
|
|
8748
|
+
if (opts.forceFramework) {
|
|
8749
|
+
const valid = ["next-app", "next-pages", "vite-react", "sveltekit", "astro"];
|
|
8750
|
+
if (!valid.includes(opts.forceFramework)) {
|
|
8751
|
+
throw new CLIError(
|
|
8752
|
+
`Invalid --framework "${opts.forceFramework}". Valid: ${valid.join(", ")}`
|
|
8753
|
+
);
|
|
8754
|
+
}
|
|
8755
|
+
return opts.forceFramework;
|
|
8756
|
+
}
|
|
8757
|
+
return detectFramework(contextFromCwd(process.cwd()));
|
|
8758
|
+
}
|
|
8759
|
+
async function installSdk(pm, cwd, opts) {
|
|
8760
|
+
const cmd = installCommand(pm, "posthog-js");
|
|
8761
|
+
const spinner10 = !opts.json && isInteractive ? clack16.spinner() : null;
|
|
8762
|
+
spinner10?.start(`Installing posthog-js (${cmd})...`);
|
|
8763
|
+
try {
|
|
8764
|
+
await runInstall(pm, "posthog-js", cwd);
|
|
8765
|
+
spinner10?.stop("Installed posthog-js.");
|
|
8766
|
+
return true;
|
|
8767
|
+
} catch (err) {
|
|
8768
|
+
spinner10?.stop("Install failed.");
|
|
8769
|
+
if (!opts.json) {
|
|
8770
|
+
clack16.log.warn(
|
|
8771
|
+
`Could not run \`${cmd}\` automatically: ${err.message}
|
|
8772
|
+
Run it manually, then re-run \`insforge posthog setup\`.`
|
|
8773
|
+
);
|
|
8774
|
+
}
|
|
8775
|
+
return false;
|
|
8776
|
+
}
|
|
8777
|
+
}
|
|
8778
|
+
function writeForFramework(framework, conn, cwd, filesWritten, notes, opts) {
|
|
8779
|
+
const host = conn.host || "https://us.posthog.com";
|
|
8780
|
+
const phc = conn.apiKey ?? "";
|
|
8781
|
+
switch (framework) {
|
|
8782
|
+
case "next-app":
|
|
8783
|
+
return writeNextApp(cwd, phc, host, filesWritten, notes, opts);
|
|
8784
|
+
case "next-pages":
|
|
8785
|
+
return writeNextPages(cwd, phc, host, filesWritten, notes, opts);
|
|
8786
|
+
case "vite-react":
|
|
8787
|
+
return writeViteReact(cwd, phc, host, filesWritten, notes, opts);
|
|
8788
|
+
case "sveltekit":
|
|
8789
|
+
return writeSveltekit(cwd, phc, host, filesWritten, notes, opts);
|
|
8790
|
+
case "astro":
|
|
8791
|
+
return writeAstro(cwd, phc, host, filesWritten, notes, opts);
|
|
8792
|
+
}
|
|
8793
|
+
}
|
|
8794
|
+
function writeNextApp(cwd, phc, host, filesWritten, notes, opts) {
|
|
8795
|
+
const appDir = existsSync13(join16(cwd, "src/app")) ? "src/app" : "app";
|
|
8796
|
+
const providerPath = join16(cwd, appDir, "posthog-provider.tsx");
|
|
8797
|
+
writeIfMissing(
|
|
8798
|
+
providerPath,
|
|
8799
|
+
renderTemplate(templates["next-app"].provider, { HOST: host }),
|
|
8800
|
+
filesWritten,
|
|
8801
|
+
notes,
|
|
8802
|
+
opts
|
|
8803
|
+
);
|
|
8804
|
+
notes.push(
|
|
8805
|
+
`Add the provider to your ${appDir}/layout.tsx:
|
|
8806
|
+
${templates["next-app"].layoutSnippet}`
|
|
8807
|
+
);
|
|
8808
|
+
const envFile = ".env.local";
|
|
8809
|
+
return writeEnv(
|
|
8810
|
+
cwd,
|
|
8811
|
+
envFile,
|
|
8812
|
+
{
|
|
8813
|
+
NEXT_PUBLIC_POSTHOG_KEY: phc,
|
|
8814
|
+
NEXT_PUBLIC_POSTHOG_HOST: host
|
|
8815
|
+
},
|
|
8816
|
+
opts
|
|
8817
|
+
);
|
|
8818
|
+
}
|
|
8819
|
+
function writeNextPages(cwd, phc, host, filesWritten, notes, opts) {
|
|
8820
|
+
const pagesDir = existsSync13(join16(cwd, "src/pages")) ? "src/pages" : "pages";
|
|
8821
|
+
const appPath = join16(cwd, pagesDir, "_app.tsx");
|
|
8822
|
+
writeIfMissing(
|
|
8823
|
+
appPath,
|
|
8824
|
+
renderTemplate(templates["next-pages"].app, { HOST: host }),
|
|
8825
|
+
filesWritten,
|
|
8826
|
+
notes,
|
|
8827
|
+
opts,
|
|
8828
|
+
"pages/_app.tsx already exists. Open it and add `posthog.init(...)` near the top \u2014 see PostHog Next.js docs."
|
|
8829
|
+
);
|
|
8830
|
+
const envFile = ".env.local";
|
|
8831
|
+
return writeEnv(
|
|
8832
|
+
cwd,
|
|
8833
|
+
envFile,
|
|
8834
|
+
{
|
|
8835
|
+
NEXT_PUBLIC_POSTHOG_KEY: phc,
|
|
8836
|
+
NEXT_PUBLIC_POSTHOG_HOST: host
|
|
8837
|
+
},
|
|
8838
|
+
opts
|
|
8839
|
+
);
|
|
8840
|
+
}
|
|
8841
|
+
function writeViteReact(cwd, phc, host, _filesWritten, notes, opts) {
|
|
8842
|
+
notes.push(
|
|
8843
|
+
`Add this snippet near the top of src/main.tsx:
|
|
8844
|
+
${renderTemplate(templates["vite-react"].mainSnippet, { HOST: host })}`
|
|
8845
|
+
);
|
|
8846
|
+
const envFile = ".env";
|
|
8847
|
+
return writeEnv(
|
|
8848
|
+
cwd,
|
|
8849
|
+
envFile,
|
|
8850
|
+
{
|
|
8851
|
+
VITE_PUBLIC_POSTHOG_KEY: phc,
|
|
8852
|
+
VITE_PUBLIC_POSTHOG_HOST: host
|
|
8853
|
+
},
|
|
8854
|
+
opts
|
|
8855
|
+
);
|
|
8856
|
+
}
|
|
8857
|
+
function writeSveltekit(cwd, phc, host, filesWritten, notes, opts) {
|
|
8858
|
+
const hooksPath = join16(cwd, "src/hooks.client.ts");
|
|
8859
|
+
writeIfMissing(
|
|
8860
|
+
hooksPath,
|
|
8861
|
+
renderTemplate(templates.sveltekit.hooks, { HOST: host }),
|
|
8862
|
+
filesWritten,
|
|
8863
|
+
notes,
|
|
8864
|
+
opts,
|
|
8865
|
+
"src/hooks.client.ts already exists. Add `posthog.init(...)` to it \u2014 see PostHog SvelteKit docs."
|
|
8866
|
+
);
|
|
8867
|
+
const envFile = ".env";
|
|
8868
|
+
return writeEnv(
|
|
8869
|
+
cwd,
|
|
8870
|
+
envFile,
|
|
8871
|
+
{
|
|
8872
|
+
PUBLIC_POSTHOG_KEY: phc,
|
|
8873
|
+
PUBLIC_POSTHOG_HOST: host
|
|
8874
|
+
},
|
|
8875
|
+
opts
|
|
8876
|
+
);
|
|
8877
|
+
}
|
|
8878
|
+
function writeAstro(cwd, phc, host, filesWritten, notes, opts) {
|
|
8879
|
+
const initPath = join16(cwd, "src/lib/posthog.ts");
|
|
8880
|
+
writeIfMissing(
|
|
8881
|
+
initPath,
|
|
8882
|
+
renderTemplate(templates.astro.init, { HOST: host }),
|
|
8883
|
+
filesWritten,
|
|
8884
|
+
notes,
|
|
8885
|
+
opts,
|
|
8886
|
+
"src/lib/posthog.ts already exists. Add `posthog.init(...)` per PostHog Astro docs."
|
|
8887
|
+
);
|
|
8888
|
+
notes.push(
|
|
8889
|
+
`Import the init module from your layout to load it on the client:
|
|
8890
|
+
// src/layouts/Layout.astro (inside <head> or <body>)
|
|
8891
|
+
<script>import '../lib/posthog';</script>`
|
|
8892
|
+
);
|
|
8893
|
+
const envFile = ".env";
|
|
8894
|
+
return writeEnv(
|
|
8895
|
+
cwd,
|
|
8896
|
+
envFile,
|
|
8897
|
+
{
|
|
8898
|
+
PUBLIC_POSTHOG_KEY: phc,
|
|
8899
|
+
PUBLIC_POSTHOG_HOST: host
|
|
8900
|
+
},
|
|
8901
|
+
opts
|
|
8902
|
+
);
|
|
8903
|
+
}
|
|
8904
|
+
function writeIfMissing(filePath, contents, filesWritten, notes, opts, conflictNote) {
|
|
8905
|
+
if (existsSync13(filePath)) {
|
|
8906
|
+
const existing = readFileSync10(filePath, "utf-8");
|
|
8907
|
+
if (existing.includes("posthog.init")) {
|
|
8908
|
+
if (!opts.json) {
|
|
8909
|
+
outputInfo(pc3.dim(`${relative3(filePath)} already calls posthog.init \u2014 leaving it alone.`));
|
|
8910
|
+
}
|
|
8911
|
+
return;
|
|
8912
|
+
}
|
|
8913
|
+
if (conflictNote) notes.push(conflictNote);
|
|
8914
|
+
if (!opts.json) {
|
|
8915
|
+
outputInfo(
|
|
8916
|
+
pc3.yellow(
|
|
8917
|
+
`${relative3(filePath)} exists. Skipped writing \u2014 see notes below for manual changes.`
|
|
8918
|
+
)
|
|
8919
|
+
);
|
|
8920
|
+
}
|
|
8921
|
+
return;
|
|
8922
|
+
}
|
|
8923
|
+
mkdirSync3(dirname2(filePath), { recursive: true });
|
|
8924
|
+
writeFileSync8(filePath, contents);
|
|
8925
|
+
filesWritten.push(filePath);
|
|
8926
|
+
if (!opts.json) outputSuccess(`Wrote ${relative3(filePath)}`);
|
|
8927
|
+
}
|
|
8928
|
+
function writeEnv(cwd, envFile, entries, opts) {
|
|
8929
|
+
const path6 = join16(cwd, envFile);
|
|
8930
|
+
const r = upsertEnvFile(path6, entries);
|
|
8931
|
+
if (!opts.json) {
|
|
8932
|
+
if (r.added.length > 0) {
|
|
8933
|
+
outputSuccess(`Wrote ${envFile}: ${r.added.join(", ")}`);
|
|
8934
|
+
}
|
|
8935
|
+
if (r.skipped.length > 0) {
|
|
8936
|
+
outputInfo(
|
|
8937
|
+
pc3.dim(`${envFile}: ${r.skipped.join(", ")} already set (matching) \u2014 left as-is.`)
|
|
8938
|
+
);
|
|
8939
|
+
}
|
|
8940
|
+
for (const m of r.mismatched) {
|
|
8941
|
+
clack16.log.warn(
|
|
8942
|
+
`${envFile} has ${m.key}=${pc3.dim(m.existingValue)}, expected ${m.newValue}. Left existing value untouched.`
|
|
8943
|
+
);
|
|
8944
|
+
}
|
|
8945
|
+
}
|
|
8946
|
+
return {
|
|
8947
|
+
file: envFile,
|
|
8948
|
+
added: r.added,
|
|
8949
|
+
mismatched: r.mismatched.map((m) => m.key)
|
|
8950
|
+
};
|
|
8951
|
+
}
|
|
8952
|
+
function reportNoFramework(conn, opts) {
|
|
8953
|
+
if (!opts.json) {
|
|
8954
|
+
clack16.log.warn("No supported framework detected in this directory.");
|
|
8955
|
+
outputInfo("");
|
|
8956
|
+
outputInfo(`Your PostHog public key: ${pc3.cyan(conn.apiKey ?? "(missing)")}`);
|
|
8957
|
+
outputInfo(`Your PostHog host: ${conn.host ?? "https://us.posthog.com"}`);
|
|
8958
|
+
outputInfo("");
|
|
8959
|
+
outputInfo("See https://posthog.com/docs/libraries to install the SDK manually.");
|
|
8960
|
+
clack16.outro("Done.");
|
|
8961
|
+
}
|
|
8962
|
+
return {
|
|
8963
|
+
framework: null,
|
|
8964
|
+
installedSdk: false,
|
|
8965
|
+
filesWritten: [],
|
|
8966
|
+
envWritten: { file: "", added: [], mismatched: [] },
|
|
8967
|
+
notes: ["No supported framework detected."]
|
|
8968
|
+
};
|
|
8969
|
+
}
|
|
8970
|
+
function frameworkLabel(framework) {
|
|
8971
|
+
switch (framework) {
|
|
8972
|
+
case "next-app":
|
|
8973
|
+
return "Next.js (App Router)";
|
|
8974
|
+
case "next-pages":
|
|
8975
|
+
return "Next.js (Pages Router)";
|
|
8976
|
+
case "vite-react":
|
|
8977
|
+
return "Vite + React";
|
|
8978
|
+
case "sveltekit":
|
|
8979
|
+
return "SvelteKit";
|
|
8980
|
+
case "astro":
|
|
8981
|
+
return "Astro";
|
|
8982
|
+
}
|
|
8983
|
+
}
|
|
8984
|
+
function relative3(p) {
|
|
8985
|
+
return p.replace(process.cwd() + "/", "");
|
|
8986
|
+
}
|
|
8987
|
+
|
|
8219
8988
|
// src/index.ts
|
|
8220
|
-
var __dirname =
|
|
8221
|
-
var pkg = JSON.parse(
|
|
8989
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
8990
|
+
var pkg = JSON.parse(readFileSync11(join17(__dirname, "../package.json"), "utf-8"));
|
|
8222
8991
|
var INSFORGE_LOGO = `
|
|
8223
8992
|
\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
8224
8993
|
\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
|
|
@@ -8300,6 +9069,8 @@ registerComputeDeleteCommand(computeCmd);
|
|
|
8300
9069
|
registerComputeStartCommand(computeCmd);
|
|
8301
9070
|
registerComputeStopCommand(computeCmd);
|
|
8302
9071
|
registerComputeEventsCommand(computeCmd);
|
|
9072
|
+
var posthogCmd = program.command("posthog").description("Manage PostHog product analytics integration");
|
|
9073
|
+
registerPosthogSetupCommand(posthogCmd);
|
|
8303
9074
|
var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
|
|
8304
9075
|
registerSchedulesListCommand(schedulesCmd);
|
|
8305
9076
|
registerSchedulesGetCommand(schedulesCmd);
|
|
@@ -8324,7 +9095,7 @@ async function showInteractiveMenu() {
|
|
|
8324
9095
|
} catch {
|
|
8325
9096
|
}
|
|
8326
9097
|
console.log(INSFORGE_LOGO);
|
|
8327
|
-
|
|
9098
|
+
clack17.intro(`InsForge CLI v${pkg.version}`);
|
|
8328
9099
|
const options = [];
|
|
8329
9100
|
if (!isLoggedIn) {
|
|
8330
9101
|
options.push({ value: "login", label: "Log in to InsForge" });
|
|
@@ -8345,7 +9116,7 @@ async function showInteractiveMenu() {
|
|
|
8345
9116
|
options
|
|
8346
9117
|
});
|
|
8347
9118
|
if (isCancel2(action)) {
|
|
8348
|
-
|
|
9119
|
+
clack17.cancel("Bye!");
|
|
8349
9120
|
process.exit(0);
|
|
8350
9121
|
}
|
|
8351
9122
|
switch (action) {
|