@insforge/cli 0.1.79 → 0.1.80

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,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync14 } from "fs";
5
- import { join as join18, dirname as dirname3 } from "path";
4
+ import { readFileSync as readFileSync12 } from "fs";
5
+ import { join as join15, dirname as dirname2 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { Command } from "commander";
8
8
  import * as clack18 from "@clack/prompts";
@@ -6961,7 +6961,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
6961
6961
  const s = !json ? clack15.spinner() : null;
6962
6962
  s?.start("Collecting diagnostic data...");
6963
6963
  const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
6964
- const cliVersion = "0.1.79";
6964
+ const cliVersion = "0.1.80";
6965
6965
  s?.stop("Data collected");
6966
6966
  if (!json) {
6967
6967
  console.log(`
@@ -8325,8 +8325,7 @@ function registerPaymentsCommands(paymentsCmd2) {
8325
8325
  }
8326
8326
 
8327
8327
  // src/commands/posthog/setup.ts
8328
- import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync3 } from "fs";
8329
- import { join as join16, dirname as dirname2 } from "path";
8328
+ import { spawnSync as spawnSync2 } from "child_process";
8330
8329
  import * as clack16 from "@clack/prompts";
8331
8330
  import pc3 from "picocolors";
8332
8331
 
@@ -8492,200 +8491,19 @@ function sleep(ms, signal) {
8492
8491
  });
8493
8492
  }
8494
8493
 
8495
- // src/lib/framework-detect.ts
8496
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
8497
- import { join as join14 } from "path";
8498
- function contextFromCwd(cwd) {
8499
- let pkg2 = null;
8500
- const pkgPath = join14(cwd, "package.json");
8501
- if (existsSync10(pkgPath)) {
8502
- try {
8503
- pkg2 = JSON.parse(readFileSync8(pkgPath, "utf-8"));
8504
- } catch {
8505
- pkg2 = null;
8506
- }
8507
- }
8508
- return {
8509
- hasDir: (rel) => existsSync10(join14(cwd, rel)),
8510
- pkg: pkg2
8511
- };
8512
- }
8513
- function hasDep(pkg2, name) {
8514
- if (!pkg2) return false;
8515
- return Boolean(pkg2.dependencies?.[name] ?? pkg2.devDependencies?.[name]);
8516
- }
8517
- function detectFramework(ctx) {
8518
- if (hasDep(ctx.pkg, "next")) {
8519
- const hasApp = ctx.hasDir("app") || ctx.hasDir("src/app");
8520
- const hasPages = ctx.hasDir("pages") || ctx.hasDir("src/pages");
8521
- if (hasApp && !hasPages) return "next-app";
8522
- if (hasPages && !hasApp) return "next-pages";
8523
- if (hasApp && hasPages) return "next-app";
8524
- return "next-app";
8525
- }
8526
- if (hasDep(ctx.pkg, "vite") && hasDep(ctx.pkg, "react")) {
8527
- return "vite-react";
8528
- }
8529
- if (hasDep(ctx.pkg, "@sveltejs/kit")) {
8530
- return "sveltekit";
8531
- }
8532
- if (hasDep(ctx.pkg, "astro")) {
8533
- return "astro";
8534
- }
8535
- return null;
8536
- }
8537
-
8538
- // src/lib/package-manager.ts
8539
- import { existsSync as existsSync11 } from "fs";
8540
- import { join as join15 } from "path";
8541
- import { exec as exec4 } from "child_process";
8542
- import { promisify as promisify5 } from "util";
8543
- var execAsync4 = promisify5(exec4);
8544
- function detectPackageManager(cwd) {
8545
- if (existsSync11(join15(cwd, "pnpm-lock.yaml"))) return "pnpm";
8546
- if (existsSync11(join15(cwd, "yarn.lock"))) return "yarn";
8547
- if (existsSync11(join15(cwd, "bun.lockb")) || existsSync11(join15(cwd, "bun.lock"))) {
8548
- return "bun";
8549
- }
8550
- return "npm";
8551
- }
8552
- function installCommand(pm, pkg2) {
8553
- switch (pm) {
8554
- case "pnpm":
8555
- return `pnpm add ${pkg2}`;
8556
- case "yarn":
8557
- return `yarn add ${pkg2}`;
8558
- case "bun":
8559
- return `bun add ${pkg2}`;
8560
- case "npm":
8561
- default:
8562
- return `npm install ${pkg2}`;
8563
- }
8564
- }
8565
- function hasPackage(pkg2, name) {
8566
- if (!pkg2) return false;
8567
- return Boolean(pkg2.dependencies?.[name] ?? pkg2.devDependencies?.[name]);
8568
- }
8569
- async function runInstall(pm, pkgName, cwd) {
8570
- const cmd = installCommand(pm, pkgName);
8571
- await execAsync4(cmd, { cwd, maxBuffer: 16 * 1024 * 1024 });
8572
- }
8573
-
8574
- // src/lib/env-writer.ts
8575
- import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
8576
- var KEY_LINE_RE = (key) => (
8577
- // Match `KEY=...` at the start of a line (allowing leading whitespace).
8578
- // Captures the value side; we only need the value portion to compare.
8579
- new RegExp(`^\\s*${key.replace(/[$.*+?^()[\\]{}|]/g, "\\$&")}\\s*=\\s*(.*)$`, "m")
8580
- );
8581
- function stripQuotes(v) {
8582
- const t = v.trim();
8583
- if (t.startsWith('"') && t.endsWith('"') && t.length >= 2 || t.startsWith("'") && t.endsWith("'") && t.length >= 2) {
8584
- return t.slice(1, -1);
8585
- }
8586
- const hash = t.indexOf(" #");
8587
- return hash >= 0 ? t.slice(0, hash).trimEnd() : t;
8588
- }
8589
- function upsertEnvFile(path6, entries) {
8590
- const exists = existsSync12(path6);
8591
- let content = exists ? readFileSync9(path6, "utf-8") : "";
8592
- const result = { added: [], skipped: [], mismatched: [] };
8593
- const additions = [];
8594
- for (const [key, value] of Object.entries(entries)) {
8595
- const re = KEY_LINE_RE(key);
8596
- const match = content.match(re);
8597
- if (match) {
8598
- const existingValue = stripQuotes(match[1] ?? "");
8599
- if (existingValue === value) {
8600
- result.skipped.push(key);
8601
- } else {
8602
- result.mismatched.push({ key, existingValue, newValue: value });
8603
- }
8604
- continue;
8605
- }
8606
- additions.push(`${key}=${value}`);
8607
- result.added.push(key);
8608
- }
8609
- if (additions.length > 0) {
8610
- if (content.length > 0 && !content.endsWith("\n")) {
8611
- content += "\n";
8612
- }
8613
- content += additions.join("\n") + "\n";
8614
- writeFileSync7(path6, content);
8615
- } else if (!exists) {
8616
- }
8617
- return result;
8618
- }
8619
-
8620
- // src/templates/posthog/next-app/posthog-provider.tsx.txt
8621
- 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";
8622
-
8623
- // src/templates/posthog/next-app/layout-snippet.tsx.txt
8624
- var layout_snippet_tsx_default = `// Wrap your <body> children with <PostHogProvider> in app/layout.tsx:
8625
- //
8626
- // import { PostHogProvider } from './posthog-provider';
8627
- //
8628
- // export default function RootLayout({ children }: { children: React.ReactNode }) {
8629
- // return (
8630
- // <html lang="en">
8631
- // <body>
8632
- // <PostHogProvider>{children}</PostHogProvider>
8633
- // </body>
8634
- // </html>
8635
- // );
8636
- // }
8637
- `;
8638
-
8639
- // src/templates/posthog/next-pages/_app.tsx.txt
8640
- 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";
8641
-
8642
- // src/templates/posthog/vite-react/main-snippet.tsx.txt
8643
- 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";
8644
-
8645
- // src/templates/posthog/sveltekit/hooks.client.ts.txt
8646
- 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";
8647
-
8648
- // src/templates/posthog/astro/posthog-init.ts.txt
8649
- 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";
8650
-
8651
- // src/templates/posthog/index.ts
8652
- var templates = {
8653
- "next-app": {
8654
- provider: posthog_provider_tsx_default,
8655
- layoutSnippet: layout_snippet_tsx_default
8656
- },
8657
- "next-pages": {
8658
- app: app_tsx_default
8659
- },
8660
- "vite-react": {
8661
- mainSnippet: main_snippet_tsx_default
8662
- },
8663
- sveltekit: {
8664
- hooks: hooks_client_ts_default
8665
- },
8666
- astro: {
8667
- init: posthog_init_ts_default
8668
- }
8669
- };
8670
- function renderTemplate(raw, vars) {
8671
- return raw.replace(/\{\{([A-Z0-9_]+)\}\}/g, (match, key) => {
8672
- return Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match;
8673
- });
8674
- }
8675
-
8676
8494
  // src/commands/posthog/setup.ts
8677
8495
  var POLL_INTERVAL_MS4 = 2e3;
8678
8496
  var POLL_TIMEOUT_MS4 = 15 * 60 * 1e3;
8679
8497
  var MAX_TRANSIENT_RETRIES = 5;
8498
+ var NPX_COMMAND = process.platform === "win32" ? "npx.cmd" : "npx";
8499
+ var WIZARD_COMMAND = `${NPX_COMMAND} -y @posthog/wizard@latest`;
8680
8500
  function registerPosthogSetupCommand(program2) {
8681
- 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) => {
8501
+ program2.command("setup").description("Connect PostHog to your InsForge dashboard, then run the official PostHog wizard to wire it into your app").option("--skip-browser", "Do not auto-open the browser for OAuth; only print the URL").action(async (opts, cmd) => {
8682
8502
  const { json, apiUrl } = getRootOpts(cmd);
8683
8503
  try {
8684
8504
  const result = await runSetup({
8685
8505
  json,
8686
8506
  apiUrl,
8687
- forceFramework: opts.framework,
8688
- skipInstall: Boolean(opts.skipInstall),
8689
8507
  skipBrowser: Boolean(opts.skipBrowser)
8690
8508
  });
8691
8509
  if (json) {
@@ -8709,62 +8527,60 @@ async function runSetup(opts) {
8709
8527
  clack16.intro("PostHog setup");
8710
8528
  outputSuccess(`Linked to InsForge project: ${proj.project_name} (${proj.project_id})`);
8711
8529
  }
8712
- const startResult = await startPosthogCliFlow(proj.project_id, token, opts.apiUrl);
8713
- let conn;
8530
+ const dashboardConnection = await ensureDashboardConnection(proj.project_id, token, opts);
8531
+ if (opts.json) {
8532
+ return {
8533
+ dashboardConnection,
8534
+ wizardSkipped: true,
8535
+ wizardCommand: WIZARD_COMMAND
8536
+ };
8537
+ }
8538
+ outputInfo("Running the official PostHog setup wizard to wire PostHog into your app...");
8539
+ outputInfo(
8540
+ pc3.dim("(it will open a browser for OAuth and let you pick a PostHog project)")
8541
+ );
8542
+ const wizardResult = spawnSync2(NPX_COMMAND, ["-y", "@posthog/wizard@latest"], {
8543
+ stdio: "inherit",
8544
+ env: process.env
8545
+ });
8546
+ if (wizardResult.error) {
8547
+ throw new CLIError(`Failed to launch PostHog wizard: ${wizardResult.error.message}`);
8548
+ }
8549
+ const exitCode = wizardResult.status ?? 1;
8550
+ if (wizardResult.signal === "SIGINT" || exitCode === 130) {
8551
+ clack16.outro("Setup cancelled.");
8552
+ return {
8553
+ dashboardConnection,
8554
+ wizardSkipped: false,
8555
+ wizardExitCode: exitCode
8556
+ };
8557
+ }
8558
+ if (exitCode !== 0) {
8559
+ throw new CLIError(`PostHog wizard exited with code ${exitCode}.`);
8560
+ }
8561
+ clack16.outro("Done. Open the Analytics page in your InsForge dashboard to view data.");
8562
+ return {
8563
+ dashboardConnection,
8564
+ wizardSkipped: false,
8565
+ wizardExitCode: exitCode
8566
+ };
8567
+ }
8568
+ async function ensureDashboardConnection(projectId, token, opts) {
8569
+ const startResult = await startPosthogCliFlow(projectId, token, opts.apiUrl);
8714
8570
  if (startResult.type === "connected") {
8715
8571
  if (!opts.json) {
8716
- outputSuccess("PostHog already connected (or auto-provisioned for new user). Continuing...");
8572
+ outputSuccess("PostHog is already connected to your InsForge dashboard.");
8717
8573
  }
8718
- const fetchResult = await fetchPosthogConnection(proj.project_id, token, opts.apiUrl);
8574
+ const fetchResult = await fetchPosthogConnection(projectId, token, opts.apiUrl);
8719
8575
  if (fetchResult.kind !== "connected") {
8720
8576
  throw new CLIError(
8721
8577
  "cli-start reported connected, but /connection returned not-connected. Try again, or check the dashboard."
8722
8578
  );
8723
8579
  }
8724
- conn = fetchResult.connection;
8725
- } else {
8726
- conn = await runConnectFlow(proj.project_id, token, startResult.authorizeUrl, opts);
8580
+ return "already-connected";
8727
8581
  }
8728
- if (!conn.apiKey) {
8729
- throw new CLIError(
8730
- "Connection succeeded but cloud-backend returned no apiKey. Try again or check the dashboard."
8731
- );
8732
- }
8733
- const framework = resolveFramework(opts);
8734
- if (framework === null) {
8735
- return reportNoFramework(conn, opts);
8736
- }
8737
- if (!opts.json) outputSuccess(`Detected framework: ${frameworkLabel(framework)}`);
8738
- const cwd = process.cwd();
8739
- const ctx = contextFromCwd(cwd);
8740
- const pm = detectPackageManager(cwd);
8741
- const alreadyInstalled = hasPackage(ctx.pkg, "posthog-js");
8742
- let installedSdk = false;
8743
- if (alreadyInstalled) {
8744
- if (!opts.json) outputInfo(pc3.dim("posthog-js is already installed \u2014 skipping install."));
8745
- } else if (opts.skipInstall) {
8746
- if (!opts.json) {
8747
- outputInfo(pc3.yellow(`Skipping install. Run manually: ${installCommand(pm, "posthog-js")}`));
8748
- }
8749
- } else {
8750
- installedSdk = await installSdk(pm, cwd, opts);
8751
- }
8752
- const filesWritten = [];
8753
- const notes = [];
8754
- const envResult = writeForFramework(framework, conn, cwd, filesWritten, notes, opts);
8755
- if (!opts.json) {
8756
- if (notes.length > 0) {
8757
- for (const n of notes) clack16.log.info(n);
8758
- }
8759
- clack16.outro("Done. Run your dev server to start sending events.");
8760
- }
8761
- return {
8762
- framework,
8763
- installedSdk,
8764
- filesWritten,
8765
- envWritten: envResult,
8766
- notes
8767
- };
8582
+ await runConnectFlow(projectId, token, startResult.authorizeUrl, opts);
8583
+ return "newly-connected";
8768
8584
  }
8769
8585
  async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8770
8586
  if (opts.json) {
@@ -8772,7 +8588,7 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8772
8588
  `);
8773
8589
  process.stderr.write("Your browser should open automatically. If not, copy the URL above.\n");
8774
8590
  } else {
8775
- clack16.log.info("PostHog is not connected to this project yet.");
8591
+ clack16.log.info("PostHog is not yet connected to your InsForge dashboard.");
8776
8592
  outputInfo("");
8777
8593
  outputInfo(`Open this URL to authorize PostHog:
8778
8594
  ${pc3.cyan(pc3.underline(authorizeUrl))}`);
@@ -8786,9 +8602,9 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8786
8602
  }
8787
8603
  }
8788
8604
  const spinner11 = !opts.json && isInteractive ? clack16.spinner() : null;
8789
- spinner11?.start("Waiting for connection... (timeout: 15 minutes)");
8605
+ spinner11?.start("Waiting for InsForge dashboard connection... (timeout: 15 minutes)");
8790
8606
  try {
8791
- const conn = await pollPosthogConnection(
8607
+ await pollPosthogConnection(
8792
8608
  projectId,
8793
8609
  token,
8794
8610
  {
@@ -8800,262 +8616,21 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8800
8616
  const secs = Math.floor(elapsed / 1e3);
8801
8617
  const mins = Math.floor(secs / 60);
8802
8618
  const remaining = `${mins}m ${secs % 60}s elapsed`;
8803
- spinner11.message(`Waiting for connection... (${remaining})`);
8619
+ spinner11.message(`Waiting for InsForge dashboard connection... (${remaining})`);
8804
8620
  }
8805
8621
  }
8806
8622
  },
8807
8623
  opts.apiUrl
8808
8624
  );
8809
- spinner11?.stop("Connection received from PostHog.");
8810
- return conn;
8625
+ spinner11?.stop("InsForge dashboard connection received.");
8811
8626
  } catch (err) {
8812
- spinner11?.stop("Connection wait failed.");
8627
+ spinner11?.stop("InsForge dashboard connection wait failed.");
8813
8628
  throw err;
8814
8629
  }
8815
8630
  }
8816
- function resolveFramework(opts) {
8817
- if (opts.forceFramework) {
8818
- const valid = ["next-app", "next-pages", "vite-react", "sveltekit", "astro"];
8819
- if (!valid.includes(opts.forceFramework)) {
8820
- throw new CLIError(
8821
- `Invalid --framework "${opts.forceFramework}". Valid: ${valid.join(", ")}`
8822
- );
8823
- }
8824
- return opts.forceFramework;
8825
- }
8826
- return detectFramework(contextFromCwd(process.cwd()));
8827
- }
8828
- async function installSdk(pm, cwd, opts) {
8829
- const cmd = installCommand(pm, "posthog-js");
8830
- const spinner11 = !opts.json && isInteractive ? clack16.spinner() : null;
8831
- spinner11?.start(`Installing posthog-js (${cmd})...`);
8832
- try {
8833
- await runInstall(pm, "posthog-js", cwd);
8834
- spinner11?.stop("Installed posthog-js.");
8835
- return true;
8836
- } catch (err) {
8837
- spinner11?.stop("Install failed.");
8838
- if (!opts.json) {
8839
- clack16.log.warn(
8840
- `Could not run \`${cmd}\` automatically: ${err.message}
8841
- Run it manually, then re-run \`insforge posthog setup\`.`
8842
- );
8843
- }
8844
- return false;
8845
- }
8846
- }
8847
- function writeForFramework(framework, conn, cwd, filesWritten, notes, opts) {
8848
- const host = conn.host || "https://us.posthog.com";
8849
- const phc = conn.apiKey ?? "";
8850
- switch (framework) {
8851
- case "next-app":
8852
- return writeNextApp(cwd, phc, host, filesWritten, notes, opts);
8853
- case "next-pages":
8854
- return writeNextPages(cwd, phc, host, filesWritten, notes, opts);
8855
- case "vite-react":
8856
- return writeViteReact(cwd, phc, host, filesWritten, notes, opts);
8857
- case "sveltekit":
8858
- return writeSveltekit(cwd, phc, host, filesWritten, notes, opts);
8859
- case "astro":
8860
- return writeAstro(cwd, phc, host, filesWritten, notes, opts);
8861
- }
8862
- }
8863
- function writeNextApp(cwd, phc, host, filesWritten, notes, opts) {
8864
- const appDir = existsSync13(join16(cwd, "src/app")) ? "src/app" : "app";
8865
- const providerPath = join16(cwd, appDir, "posthog-provider.tsx");
8866
- writeIfMissing(
8867
- providerPath,
8868
- renderTemplate(templates["next-app"].provider, { HOST: host }),
8869
- filesWritten,
8870
- notes,
8871
- opts
8872
- );
8873
- notes.push(
8874
- `Add the provider to your ${appDir}/layout.tsx:
8875
- ${templates["next-app"].layoutSnippet}`
8876
- );
8877
- const envFile = ".env.local";
8878
- return writeEnv(
8879
- cwd,
8880
- envFile,
8881
- {
8882
- NEXT_PUBLIC_POSTHOG_KEY: phc,
8883
- NEXT_PUBLIC_POSTHOG_HOST: host
8884
- },
8885
- opts
8886
- );
8887
- }
8888
- function writeNextPages(cwd, phc, host, filesWritten, notes, opts) {
8889
- const pagesDir = existsSync13(join16(cwd, "src/pages")) ? "src/pages" : "pages";
8890
- const appPath = join16(cwd, pagesDir, "_app.tsx");
8891
- writeIfMissing(
8892
- appPath,
8893
- renderTemplate(templates["next-pages"].app, { HOST: host }),
8894
- filesWritten,
8895
- notes,
8896
- opts,
8897
- "pages/_app.tsx already exists. Open it and add `posthog.init(...)` near the top \u2014 see PostHog Next.js docs."
8898
- );
8899
- const envFile = ".env.local";
8900
- return writeEnv(
8901
- cwd,
8902
- envFile,
8903
- {
8904
- NEXT_PUBLIC_POSTHOG_KEY: phc,
8905
- NEXT_PUBLIC_POSTHOG_HOST: host
8906
- },
8907
- opts
8908
- );
8909
- }
8910
- function writeViteReact(cwd, phc, host, _filesWritten, notes, opts) {
8911
- notes.push(
8912
- `Add this snippet near the top of src/main.tsx:
8913
- ${renderTemplate(templates["vite-react"].mainSnippet, { HOST: host })}`
8914
- );
8915
- const envFile = ".env";
8916
- return writeEnv(
8917
- cwd,
8918
- envFile,
8919
- {
8920
- VITE_PUBLIC_POSTHOG_KEY: phc,
8921
- VITE_PUBLIC_POSTHOG_HOST: host
8922
- },
8923
- opts
8924
- );
8925
- }
8926
- function writeSveltekit(cwd, phc, host, filesWritten, notes, opts) {
8927
- const hooksPath = join16(cwd, "src/hooks.client.ts");
8928
- writeIfMissing(
8929
- hooksPath,
8930
- renderTemplate(templates.sveltekit.hooks, { HOST: host }),
8931
- filesWritten,
8932
- notes,
8933
- opts,
8934
- "src/hooks.client.ts already exists. Add `posthog.init(...)` to it \u2014 see PostHog SvelteKit docs."
8935
- );
8936
- const envFile = ".env";
8937
- return writeEnv(
8938
- cwd,
8939
- envFile,
8940
- {
8941
- PUBLIC_POSTHOG_KEY: phc,
8942
- PUBLIC_POSTHOG_HOST: host
8943
- },
8944
- opts
8945
- );
8946
- }
8947
- function writeAstro(cwd, phc, host, filesWritten, notes, opts) {
8948
- const initPath = join16(cwd, "src/lib/posthog.ts");
8949
- writeIfMissing(
8950
- initPath,
8951
- renderTemplate(templates.astro.init, { HOST: host }),
8952
- filesWritten,
8953
- notes,
8954
- opts,
8955
- "src/lib/posthog.ts already exists. Add `posthog.init(...)` per PostHog Astro docs."
8956
- );
8957
- notes.push(
8958
- `Import the init module from your layout to load it on the client:
8959
- // src/layouts/Layout.astro (inside <head> or <body>)
8960
- <script>import '../lib/posthog';</script>`
8961
- );
8962
- const envFile = ".env";
8963
- return writeEnv(
8964
- cwd,
8965
- envFile,
8966
- {
8967
- PUBLIC_POSTHOG_KEY: phc,
8968
- PUBLIC_POSTHOG_HOST: host
8969
- },
8970
- opts
8971
- );
8972
- }
8973
- function writeIfMissing(filePath, contents, filesWritten, notes, opts, conflictNote) {
8974
- if (existsSync13(filePath)) {
8975
- const existing = readFileSync10(filePath, "utf-8");
8976
- if (existing.includes("posthog.init")) {
8977
- if (!opts.json) {
8978
- outputInfo(pc3.dim(`${relative3(filePath)} already calls posthog.init \u2014 leaving it alone.`));
8979
- }
8980
- return;
8981
- }
8982
- if (conflictNote) notes.push(conflictNote);
8983
- if (!opts.json) {
8984
- outputInfo(
8985
- pc3.yellow(
8986
- `${relative3(filePath)} exists. Skipped writing \u2014 see notes below for manual changes.`
8987
- )
8988
- );
8989
- }
8990
- return;
8991
- }
8992
- mkdirSync3(dirname2(filePath), { recursive: true });
8993
- writeFileSync8(filePath, contents);
8994
- filesWritten.push(filePath);
8995
- if (!opts.json) outputSuccess(`Wrote ${relative3(filePath)}`);
8996
- }
8997
- function writeEnv(cwd, envFile, entries, opts) {
8998
- const path6 = join16(cwd, envFile);
8999
- const r = upsertEnvFile(path6, entries);
9000
- if (!opts.json) {
9001
- if (r.added.length > 0) {
9002
- outputSuccess(`Wrote ${envFile}: ${r.added.join(", ")}`);
9003
- }
9004
- if (r.skipped.length > 0) {
9005
- outputInfo(
9006
- pc3.dim(`${envFile}: ${r.skipped.join(", ")} already set (matching) \u2014 left as-is.`)
9007
- );
9008
- }
9009
- for (const m of r.mismatched) {
9010
- clack16.log.warn(
9011
- `${envFile} has ${m.key}=${pc3.dim(m.existingValue)}, expected ${m.newValue}. Left existing value untouched.`
9012
- );
9013
- }
9014
- }
9015
- return {
9016
- file: envFile,
9017
- added: r.added,
9018
- mismatched: r.mismatched.map((m) => m.key)
9019
- };
9020
- }
9021
- function reportNoFramework(conn, opts) {
9022
- if (!opts.json) {
9023
- clack16.log.warn("No supported framework detected in this directory.");
9024
- outputInfo("");
9025
- outputInfo(`Your PostHog public key: ${pc3.cyan(conn.apiKey ?? "(missing)")}`);
9026
- outputInfo(`Your PostHog host: ${conn.host ?? "https://us.posthog.com"}`);
9027
- outputInfo("");
9028
- outputInfo("See https://posthog.com/docs/libraries to install the SDK manually.");
9029
- clack16.outro("Done.");
9030
- }
9031
- return {
9032
- framework: null,
9033
- installedSdk: false,
9034
- filesWritten: [],
9035
- envWritten: { file: "", added: [], mismatched: [] },
9036
- notes: ["No supported framework detected."]
9037
- };
9038
- }
9039
- function frameworkLabel(framework) {
9040
- switch (framework) {
9041
- case "next-app":
9042
- return "Next.js (App Router)";
9043
- case "next-pages":
9044
- return "Next.js (Pages Router)";
9045
- case "vite-react":
9046
- return "Vite + React";
9047
- case "sveltekit":
9048
- return "SvelteKit";
9049
- case "astro":
9050
- return "Astro";
9051
- }
9052
- }
9053
- function relative3(p3) {
9054
- return p3.replace(process.cwd() + "/", "");
9055
- }
9056
8631
 
9057
8632
  // src/commands/config/export.ts
9058
- import { writeFileSync as writeFileSync9, existsSync as existsSync14 } from "fs";
8633
+ import { writeFileSync as writeFileSync7, existsSync as existsSync10 } from "fs";
9059
8634
  import { resolve as resolve5 } from "path";
9060
8635
  import * as p from "@clack/prompts";
9061
8636
  import pc4 from "picocolors";
@@ -9539,7 +9114,7 @@ function registerConfigExportCommand(cfg) {
9539
9114
  try {
9540
9115
  await requireAuth();
9541
9116
  const target = resolve5(process.cwd(), opts.out);
9542
- if (existsSync14(target) && !opts.force) {
9117
+ if (existsSync10(target) && !opts.force) {
9543
9118
  if (json) {
9544
9119
  throw new CLIError(
9545
9120
  `${opts.out} exists. Re-run with --force to overwrite.`,
@@ -9560,7 +9135,7 @@ function registerConfigExportCommand(cfg) {
9560
9135
  const raw = await res.json();
9561
9136
  const { config, skipped } = configFromMetadata(raw);
9562
9137
  const toml = stringifyConfigToml(config);
9563
- writeFileSync9(target, toml, "utf8");
9138
+ writeFileSync7(target, toml, "utf8");
9564
9139
  if (json) {
9565
9140
  console.log(JSON.stringify({ written: target, config, skipped }, null, 2));
9566
9141
  } else {
@@ -9582,7 +9157,7 @@ function registerConfigExportCommand(cfg) {
9582
9157
  }
9583
9158
 
9584
9159
  // src/commands/config/plan.ts
9585
- import { readFileSync as readFileSync11 } from "fs";
9160
+ import { readFileSync as readFileSync8 } from "fs";
9586
9161
  import { resolve as resolve6 } from "path";
9587
9162
  import pc5 from "picocolors";
9588
9163
 
@@ -9888,7 +9463,7 @@ function registerConfigPlanCommand(cfg) {
9888
9463
  try {
9889
9464
  await requireAuth();
9890
9465
  const tomlPath = resolve6(process.cwd(), opts.file);
9891
- const tomlSource = readFileSync11(tomlPath, "utf8");
9466
+ const tomlSource = readFileSync8(tomlPath, "utf8");
9892
9467
  const file = parseConfigToml(tomlSource);
9893
9468
  const res = await ossFetch("/api/metadata");
9894
9469
  const raw = await res.json();
@@ -9916,7 +9491,7 @@ function registerConfigPlanCommand(cfg) {
9916
9491
  }
9917
9492
 
9918
9493
  // src/commands/config/apply.ts
9919
- import { readFileSync as readFileSync12 } from "fs";
9494
+ import { readFileSync as readFileSync9 } from "fs";
9920
9495
  import { resolve as resolve7 } from "path";
9921
9496
  import * as p2 from "@clack/prompts";
9922
9497
  import pc6 from "picocolors";
@@ -9926,7 +9501,7 @@ function registerConfigApplyCommand(cfg) {
9926
9501
  try {
9927
9502
  await requireAuth();
9928
9503
  const tomlPath = resolve7(process.cwd(), opts.file);
9929
- const tomlSource = readFileSync12(tomlPath, "utf8");
9504
+ const tomlSource = readFileSync9(tomlPath, "utf8");
9930
9505
  const file = parseConfigToml(tomlSource);
9931
9506
  const res = await ossFetch("/api/metadata");
9932
9507
  const raw = await res.json();
@@ -10083,8 +9658,8 @@ function registerConfigCommand(program2) {
10083
9658
  }
10084
9659
 
10085
9660
  // src/commands/ai/setup.ts
10086
- import { appendFileSync as appendFileSync2, existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
10087
- import { isAbsolute, join as join17, relative as relative4, resolve as resolve8 } from "path";
9661
+ import { appendFileSync as appendFileSync2, existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
9662
+ import { isAbsolute, join as join14, relative as relative3, resolve as resolve8 } from "path";
10088
9663
  import * as clack17 from "@clack/prompts";
10089
9664
  import pc7 from "picocolors";
10090
9665
 
@@ -10105,6 +9680,52 @@ async function getOpenRouterApiKey() {
10105
9680
  };
10106
9681
  }
10107
9682
 
9683
+ // src/lib/env-writer.ts
9684
+ import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
9685
+ var KEY_LINE_RE = (key) => (
9686
+ // Match `KEY=...` at the start of a line (allowing leading whitespace).
9687
+ // Captures the value side; we only need the value portion to compare.
9688
+ new RegExp(`^\\s*${key.replace(/[$.*+?^()[\\]{}|]/g, "\\$&")}\\s*=\\s*(.*)$`, "m")
9689
+ );
9690
+ function stripQuotes(v) {
9691
+ const t = v.trim();
9692
+ if (t.startsWith('"') && t.endsWith('"') && t.length >= 2 || t.startsWith("'") && t.endsWith("'") && t.length >= 2) {
9693
+ return t.slice(1, -1);
9694
+ }
9695
+ const hash = t.indexOf(" #");
9696
+ return hash >= 0 ? t.slice(0, hash).trimEnd() : t;
9697
+ }
9698
+ function upsertEnvFile(path6, entries) {
9699
+ const exists = existsSync11(path6);
9700
+ let content = exists ? readFileSync10(path6, "utf-8") : "";
9701
+ const result = { added: [], skipped: [], mismatched: [] };
9702
+ const additions = [];
9703
+ for (const [key, value] of Object.entries(entries)) {
9704
+ const re = KEY_LINE_RE(key);
9705
+ const match = content.match(re);
9706
+ if (match) {
9707
+ const existingValue = stripQuotes(match[1] ?? "");
9708
+ if (existingValue === value) {
9709
+ result.skipped.push(key);
9710
+ } else {
9711
+ result.mismatched.push({ key, existingValue, newValue: value });
9712
+ }
9713
+ continue;
9714
+ }
9715
+ additions.push(`${key}=${value}`);
9716
+ result.added.push(key);
9717
+ }
9718
+ if (additions.length > 0) {
9719
+ if (content.length > 0 && !content.endsWith("\n")) {
9720
+ content += "\n";
9721
+ }
9722
+ content += additions.join("\n") + "\n";
9723
+ writeFileSync8(path6, content);
9724
+ } else if (!exists) {
9725
+ }
9726
+ return result;
9727
+ }
9728
+
10108
9729
  // src/commands/ai/setup.ts
10109
9730
  var DEFAULT_ENV_FILE = ".env.local";
10110
9731
  var OPENROUTER_ENV_KEY = "OPENROUTER_API_KEY";
@@ -10196,7 +9817,7 @@ async function runAiSetup(opts) {
10196
9817
  };
10197
9818
  }
10198
9819
  function displayPath(path6) {
10199
- const rel = relative4(process.cwd(), path6);
9820
+ const rel = relative3(process.cwd(), path6);
10200
9821
  if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
10201
9822
  return path6;
10202
9823
  }
@@ -10210,12 +9831,12 @@ function isLocalEnvFile(envFile) {
10210
9831
  function ensureLocalEnvIgnored(cwd, envFile) {
10211
9832
  if (!isLocalEnvFile(envFile)) return false;
10212
9833
  const envPath = resolve8(cwd, envFile);
10213
- const relEnvPath = relative4(cwd, envPath);
9834
+ const relEnvPath = relative3(cwd, envPath);
10214
9835
  if (!relEnvPath || relEnvPath.startsWith("..") || isAbsolute(relEnvPath)) {
10215
9836
  return false;
10216
9837
  }
10217
- const gitignorePath = join17(cwd, ".gitignore");
10218
- const existing = existsSync15(gitignorePath) ? readFileSync13(gitignorePath, "utf-8") : "";
9838
+ const gitignorePath = join14(cwd, ".gitignore");
9839
+ const existing = existsSync12(gitignorePath) ? readFileSync11(gitignorePath, "utf-8") : "";
10219
9840
  const lines = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
10220
9841
  const envBasename = envFile.replace(/\\/g, "/").split("/").pop() ?? envFile;
10221
9842
  if (lines.has(".env*") || lines.has(".env.*") || lines.has(".env*.local") || lines.has(".env.local") && envBasename === ".env.local") {
@@ -10235,8 +9856,8 @@ function registerAiCommands(aiCmd2) {
10235
9856
  }
10236
9857
 
10237
9858
  // src/index.ts
10238
- var __dirname = dirname3(fileURLToPath(import.meta.url));
10239
- var pkg = JSON.parse(readFileSync14(join18(__dirname, "../package.json"), "utf-8"));
9859
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
9860
+ var pkg = JSON.parse(readFileSync12(join15(__dirname, "../package.json"), "utf-8"));
10240
9861
  var INSFORGE_LOGO = `
10241
9862
  \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
10242
9863
  \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