@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 CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync8 } from "fs";
5
- import { join as join14, dirname as dirname2 } from "path";
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 clack16 from "@clack/prompts";
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 spinner9 = !json ? clack5.spinner() : null;
1332
+ const spinner10 = !json ? clack5.spinner() : null;
1333
1333
  let ready;
1334
1334
  let provisioned = false;
1335
1335
  try {
1336
- spinner9?.start(`Creating branch '${name}'...`);
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
- spinner9?.message(`Branch '${name}' created (appkey: ${created.appkey}). Provisioning...`);
1343
- ready = await pollUntilReady(created.id, apiUrl, spinner9);
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
- spinner9?.message("Branch ready. Switching context...");
1346
+ spinner10?.message("Branch ready. Switching context...");
1347
1347
  await runBranchSwitch({ name, apiUrl, json, silent: true });
1348
- spinner9?.stop(`Branch '${name}' is ready and active`);
1348
+ spinner10?.stop(`Branch '${name}' is ready and active`);
1349
1349
  } else if (provisioned) {
1350
- spinner9?.stop(`Branch '${name}' is ready`);
1350
+ spinner10?.stop(`Branch '${name}' is ready`);
1351
1351
  } else {
1352
- spinner9?.stop(`Branch '${name}' is in '${ready.branch_state}' state`);
1352
+ spinner10?.stop(`Branch '${name}' is in '${ready.branch_state}' state`);
1353
1353
  }
1354
1354
  } catch (err) {
1355
1355
  if (provisioned) {
1356
- spinner9?.stop(
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
- spinner9?.stop(`Branch '${name}' creation failed`, 1);
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, spinner9) {
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 (spinner9 && branch2.branch_state !== lastState) {
1395
- spinner9.message(`Provisioning branch (state: ${branch2.branch_state})...`);
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 res = await ossFetch("/api/metadata/database-connection-string");
1871
- const data = await res.json();
1872
- return typeof data.connectionURL === "string" && data.connectionURL.length > 0 ? data.connectionURL : null;
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, spinner9, syncBeforeRead) {
2380
- spinner9?.message("Building and deploying...");
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
- spinner9?.stop("Deployment failed");
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
- spinner9?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
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: spinner9 } = opts;
2413
- spinner9?.start("Scanning source files...");
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
- spinner9?.message("Creating deployment...");
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
- spinner9?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
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
- spinner9?.message("Starting deployment...");
2447
+ spinner10?.message("Starting deployment...");
2437
2448
  await startDirectDeployment(createResult.id, startBody);
2438
- return await pollDeployment(createResult.id, spinner9, !isInsforgeCloudOssHost(config.oss_host));
2449
+ return await pollDeployment(createResult.id, spinner10, !isInsforgeCloudOssHost(config.oss_host));
2439
2450
  }
2440
2451
  async function deployProjectLegacy(opts) {
2441
- const { sourceDir, startBody = {}, spinner: spinner9 } = opts;
2442
- spinner9?.message("Creating deployment...");
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
- spinner9?.message("Compressing source files...");
2456
+ spinner10?.message("Compressing source files...");
2446
2457
  const zipBuffer = await createZipBuffer(sourceDir);
2447
- spinner9?.message("Uploading...");
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
- spinner9?.message("Starting deployment...");
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, spinner9, false);
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 spinner9 = !json ? clack11.spinner() : null;
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: spinner9 });
2536
+ const result = await deployProject({ sourceDir, startBody, spinner: spinner10 });
2526
2537
  if (result.isReady) {
2527
- spinner9?.stop("Deployment complete");
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
- spinner9?.stop("Deployment is still building");
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 spinner9 = clack13.spinner();
3104
- spinner9.start(startMessage);
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
- spinner9.stop("Dependencies installed");
3118
+ spinner10.stop("Dependencies installed");
3108
3119
  } catch (err) {
3109
- spinner9.stop("Failed to install dependencies");
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 res = await ossFetch("/api/metadata/database-connection-string");
4240
- const body = await res.json();
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(body);
4281
+ outputJson({ connectionURL: url });
4243
4282
  } else {
4244
- console.log(body.connectionURL);
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.70";
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 = dirname2(fileURLToPath(import.meta.url));
8221
- var pkg = JSON.parse(readFileSync8(join14(__dirname, "../package.json"), "utf-8"));
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
- clack16.intro(`InsForge CLI v${pkg.version}`);
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
- clack16.cancel("Bye!");
9119
+ clack17.cancel("Bye!");
8349
9120
  process.exit(0);
8350
9121
  }
8351
9122
  switch (action) {