@insforge/cli 0.1.79 → 0.1.81

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";
@@ -1215,9 +1215,24 @@ function trackPayments(subcommand, config, properties) {
1215
1215
  ...properties
1216
1216
  });
1217
1217
  }
1218
+ function trackConfig(subcommand, config, properties) {
1219
+ const distinctId = config?.project_id ?? FAKE_PROJECT_ID;
1220
+ captureEvent(distinctId, "cli_config_invoked", {
1221
+ subcommand,
1222
+ project_id: config?.project_id,
1223
+ project_name: config?.project_name,
1224
+ org_id: config?.org_id,
1225
+ region: config?.region,
1226
+ oss_mode: !config || config.project_id === FAKE_PROJECT_ID,
1227
+ ...properties
1228
+ });
1229
+ }
1218
1230
  async function shutdownAnalytics() {
1231
+ if (!client) return;
1232
+ const c = client;
1233
+ client = null;
1219
1234
  try {
1220
- if (client) await client.shutdown();
1235
+ await c.shutdown();
1221
1236
  } catch {
1222
1237
  }
1223
1238
  }
@@ -6961,7 +6976,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
6961
6976
  const s = !json ? clack15.spinner() : null;
6962
6977
  s?.start("Collecting diagnostic data...");
6963
6978
  const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
6964
- const cliVersion = "0.1.79";
6979
+ const cliVersion = "0.1.81";
6965
6980
  s?.stop("Data collected");
6966
6981
  if (!json) {
6967
6982
  console.log(`
@@ -8325,8 +8340,6 @@ function registerPaymentsCommands(paymentsCmd2) {
8325
8340
  }
8326
8341
 
8327
8342
  // 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";
8330
8343
  import * as clack16 from "@clack/prompts";
8331
8344
  import pc3 from "picocolors";
8332
8345
 
@@ -8492,200 +8505,19 @@ function sleep(ms, signal) {
8492
8505
  });
8493
8506
  }
8494
8507
 
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
8508
  // src/commands/posthog/setup.ts
8677
8509
  var POLL_INTERVAL_MS4 = 2e3;
8678
8510
  var POLL_TIMEOUT_MS4 = 15 * 60 * 1e3;
8679
8511
  var MAX_TRANSIENT_RETRIES = 5;
8512
+ var NPX_COMMAND = process.platform === "win32" ? "npx.cmd" : "npx";
8513
+ var WIZARD_COMMAND = `${NPX_COMMAND} -y @posthog/wizard@latest`;
8680
8514
  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) => {
8515
+ 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
8516
  const { json, apiUrl } = getRootOpts(cmd);
8683
8517
  try {
8684
8518
  const result = await runSetup({
8685
8519
  json,
8686
8520
  apiUrl,
8687
- forceFramework: opts.framework,
8688
- skipInstall: Boolean(opts.skipInstall),
8689
8521
  skipBrowser: Boolean(opts.skipBrowser)
8690
8522
  });
8691
8523
  if (json) {
@@ -8709,62 +8541,39 @@ async function runSetup(opts) {
8709
8541
  clack16.intro("PostHog setup");
8710
8542
  outputSuccess(`Linked to InsForge project: ${proj.project_name} (${proj.project_id})`);
8711
8543
  }
8712
- const startResult = await startPosthogCliFlow(proj.project_id, token, opts.apiUrl);
8713
- let conn;
8544
+ const dashboardConnection = await ensureDashboardConnection(proj.project_id, token, opts);
8545
+ if (!opts.json) {
8546
+ clack16.note(
8547
+ `Run this in your terminal to wire PostHog into your app code:
8548
+
8549
+ ${WIZARD_COMMAND}
8550
+
8551
+ Once it completes, open the Analytics page in your InsForge dashboard.`,
8552
+ "Next step"
8553
+ );
8554
+ }
8555
+ return {
8556
+ dashboardConnection,
8557
+ wizardSkipped: true,
8558
+ wizardCommand: WIZARD_COMMAND
8559
+ };
8560
+ }
8561
+ async function ensureDashboardConnection(projectId, token, opts) {
8562
+ const startResult = await startPosthogCliFlow(projectId, token, opts.apiUrl);
8714
8563
  if (startResult.type === "connected") {
8715
8564
  if (!opts.json) {
8716
- outputSuccess("PostHog already connected (or auto-provisioned for new user). Continuing...");
8565
+ outputSuccess("PostHog is already connected to your InsForge dashboard.");
8717
8566
  }
8718
- const fetchResult = await fetchPosthogConnection(proj.project_id, token, opts.apiUrl);
8567
+ const fetchResult = await fetchPosthogConnection(projectId, token, opts.apiUrl);
8719
8568
  if (fetchResult.kind !== "connected") {
8720
8569
  throw new CLIError(
8721
8570
  "cli-start reported connected, but /connection returned not-connected. Try again, or check the dashboard."
8722
8571
  );
8723
8572
  }
8724
- conn = fetchResult.connection;
8725
- } else {
8726
- conn = await runConnectFlow(proj.project_id, token, startResult.authorizeUrl, opts);
8727
- }
8728
- if (!conn.apiKey) {
8729
- throw new CLIError(
8730
- "Connection succeeded but cloud-backend returned no apiKey. Try again or check the dashboard."
8731
- );
8573
+ return "already-connected";
8732
8574
  }
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
- };
8575
+ await runConnectFlow(projectId, token, startResult.authorizeUrl, opts);
8576
+ return "newly-connected";
8768
8577
  }
8769
8578
  async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8770
8579
  if (opts.json) {
@@ -8772,11 +8581,15 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8772
8581
  `);
8773
8582
  process.stderr.write("Your browser should open automatically. If not, copy the URL above.\n");
8774
8583
  } else {
8775
- clack16.log.info("PostHog is not connected to this project yet.");
8776
- outputInfo("");
8777
- outputInfo(`Open this URL to authorize PostHog:
8778
- ${pc3.cyan(pc3.underline(authorizeUrl))}`);
8779
- outputInfo("");
8584
+ clack16.log.info("PostHog is not yet connected to your InsForge dashboard.");
8585
+ if (opts.skipBrowser) {
8586
+ clack16.log.info(`Open this URL to authorize PostHog:
8587
+ ${pc3.cyan(pc3.underline(authorizeUrl))}`);
8588
+ } else {
8589
+ clack16.log.info("Opening browser to authorize PostHog...");
8590
+ clack16.log.info(`If browser doesn't open, visit:
8591
+ ${pc3.cyan(pc3.underline(authorizeUrl))}`);
8592
+ }
8780
8593
  }
8781
8594
  if (!opts.skipBrowser) {
8782
8595
  try {
@@ -8786,9 +8599,13 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8786
8599
  }
8787
8600
  }
8788
8601
  const spinner11 = !opts.json && isInteractive ? clack16.spinner() : null;
8789
- spinner11?.start("Waiting for connection... (timeout: 15 minutes)");
8602
+ if (spinner11) {
8603
+ spinner11.start("Waiting for InsForge dashboard connection... (timeout: 15 minutes)");
8604
+ } else if (!opts.json) {
8605
+ clack16.log.info("Waiting for InsForge dashboard connection (up to 15 minutes)...");
8606
+ }
8790
8607
  try {
8791
- const conn = await pollPosthogConnection(
8608
+ await pollPosthogConnection(
8792
8609
  projectId,
8793
8610
  token,
8794
8611
  {
@@ -8800,262 +8617,29 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8800
8617
  const secs = Math.floor(elapsed / 1e3);
8801
8618
  const mins = Math.floor(secs / 60);
8802
8619
  const remaining = `${mins}m ${secs % 60}s elapsed`;
8803
- spinner11.message(`Waiting for connection... (${remaining})`);
8620
+ spinner11.message(`Waiting for InsForge dashboard connection... (${remaining})`);
8804
8621
  }
8805
8622
  }
8806
8623
  },
8807
8624
  opts.apiUrl
8808
8625
  );
8809
- spinner11?.stop("Connection received from PostHog.");
8810
- return conn;
8811
- } catch (err) {
8812
- spinner11?.stop("Connection wait failed.");
8813
- throw err;
8814
- }
8815
- }
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
- );
8626
+ if (spinner11) {
8627
+ spinner11.stop("InsForge dashboard connection received.");
8628
+ } else if (!opts.json) {
8629
+ clack16.log.success("InsForge dashboard connection received.");
8823
8630
  }
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
8631
  } 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
- );
8632
+ if (spinner11) {
8633
+ spinner11.stop("InsForge dashboard connection wait failed.");
8634
+ } else if (!opts.json) {
8635
+ clack16.log.error("InsForge dashboard connection wait failed.");
8843
8636
  }
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";
8637
+ throw err;
9051
8638
  }
9052
8639
  }
9053
- function relative3(p3) {
9054
- return p3.replace(process.cwd() + "/", "");
9055
- }
9056
8640
 
9057
8641
  // src/commands/config/export.ts
9058
- import { writeFileSync as writeFileSync9, existsSync as existsSync14 } from "fs";
8642
+ import { writeFileSync as writeFileSync7, existsSync as existsSync10 } from "fs";
9059
8643
  import { resolve as resolve5 } from "path";
9060
8644
  import * as p from "@clack/prompts";
9061
8645
  import pc4 from "picocolors";
@@ -9536,10 +9120,12 @@ function configFromMetadata(raw) {
9536
9120
  function registerConfigExportCommand(cfg) {
9537
9121
  cfg.command("export").description("Pull live project config and write insforge.toml").option("--out <path>", "output path", "insforge.toml").option("--force", "overwrite without confirmation").action(async (opts, cmd) => {
9538
9122
  const { json } = getRootOpts(cmd);
9123
+ let projectConfig = null;
9539
9124
  try {
9125
+ projectConfig = getProjectConfig();
9540
9126
  await requireAuth();
9541
9127
  const target = resolve5(process.cwd(), opts.out);
9542
- if (existsSync14(target) && !opts.force) {
9128
+ if (existsSync10(target) && !opts.force) {
9543
9129
  if (json) {
9544
9130
  throw new CLIError(
9545
9131
  `${opts.out} exists. Re-run with --force to overwrite.`,
@@ -9553,6 +9139,12 @@ function registerConfigExportCommand(cfg) {
9553
9139
  });
9554
9140
  if (!ok || p.isCancel(ok)) {
9555
9141
  console.log("Aborted.");
9142
+ await reportCliUsage("cli.config.export", true);
9143
+ trackConfig("export", projectConfig, {
9144
+ json_mode: !!json,
9145
+ force: !!opts.force,
9146
+ outcome: "aborted"
9147
+ });
9556
9148
  return;
9557
9149
  }
9558
9150
  }
@@ -9560,7 +9152,7 @@ function registerConfigExportCommand(cfg) {
9560
9152
  const raw = await res.json();
9561
9153
  const { config, skipped } = configFromMetadata(raw);
9562
9154
  const toml = stringifyConfigToml(config);
9563
- writeFileSync9(target, toml, "utf8");
9155
+ writeFileSync7(target, toml, "utf8");
9564
9156
  if (json) {
9565
9157
  console.log(JSON.stringify({ written: target, config, skipped }, null, 2));
9566
9158
  } else {
@@ -9574,15 +9166,29 @@ function registerConfigExportCommand(cfg) {
9574
9166
  }
9575
9167
  }
9576
9168
  await reportCliUsage("cli.config.export", true);
9169
+ trackConfig("export", projectConfig, {
9170
+ json_mode: !!json,
9171
+ force: !!opts.force,
9172
+ skipped_count: skipped.length,
9173
+ outcome: "success"
9174
+ });
9577
9175
  } catch (err) {
9578
9176
  await reportCliUsage("cli.config.export", false);
9177
+ trackConfig("export", projectConfig, {
9178
+ json_mode: !!json,
9179
+ force: !!opts.force,
9180
+ outcome: "error"
9181
+ });
9182
+ await shutdownAnalytics();
9579
9183
  handleError(err, json);
9184
+ } finally {
9185
+ await shutdownAnalytics();
9580
9186
  }
9581
9187
  });
9582
9188
  }
9583
9189
 
9584
9190
  // src/commands/config/plan.ts
9585
- import { readFileSync as readFileSync11 } from "fs";
9191
+ import { readFileSync as readFileSync8 } from "fs";
9586
9192
  import { resolve as resolve6 } from "path";
9587
9193
  import pc5 from "picocolors";
9588
9194
 
@@ -9885,10 +9491,12 @@ function authPasswordWireKey(key) {
9885
9491
  function registerConfigPlanCommand(cfg) {
9886
9492
  cfg.command("plan").description("Show diff between insforge.toml and live project state").option("--file <path>", "path to insforge.toml", "insforge.toml").action(async (opts, cmd) => {
9887
9493
  const { json } = getRootOpts(cmd);
9494
+ let projectConfig = null;
9888
9495
  try {
9496
+ projectConfig = getProjectConfig();
9889
9497
  await requireAuth();
9890
9498
  const tomlPath = resolve6(process.cwd(), opts.file);
9891
- const tomlSource = readFileSync11(tomlPath, "utf8");
9499
+ const tomlSource = readFileSync8(tomlPath, "utf8");
9892
9500
  const file = parseConfigToml(tomlSource);
9893
9501
  const res = await ossFetch("/api/metadata");
9894
9502
  const raw = await res.json();
@@ -9908,31 +9516,52 @@ function registerConfigPlanCommand(cfg) {
9908
9516
  }
9909
9517
  }
9910
9518
  await reportCliUsage("cli.config.plan", true);
9519
+ trackConfig("plan", projectConfig, {
9520
+ json_mode: !!json,
9521
+ changes_count: result.changes.length,
9522
+ skipped_count: skipped.length,
9523
+ sections_changed: Array.from(
9524
+ new Set(result.changes.map((c) => changePath(c)))
9525
+ ),
9526
+ outcome: "success"
9527
+ });
9911
9528
  } catch (err) {
9912
9529
  await reportCliUsage("cli.config.plan", false);
9530
+ trackConfig("plan", projectConfig, {
9531
+ json_mode: !!json,
9532
+ outcome: "error"
9533
+ });
9534
+ await shutdownAnalytics();
9913
9535
  handleError(err, json);
9536
+ } finally {
9537
+ await shutdownAnalytics();
9914
9538
  }
9915
9539
  });
9916
9540
  }
9917
9541
 
9918
9542
  // src/commands/config/apply.ts
9919
- import { readFileSync as readFileSync12 } from "fs";
9543
+ import { readFileSync as readFileSync9 } from "fs";
9920
9544
  import { resolve as resolve7 } from "path";
9921
9545
  import * as p2 from "@clack/prompts";
9922
9546
  import pc6 from "picocolors";
9923
9547
  function registerConfigApplyCommand(cfg) {
9924
9548
  cfg.command("apply").description("Apply insforge.toml to the live project").option("--file <path>", "path to insforge.toml", "insforge.toml").option("--dry-run", "show plan, do not apply").option("--auto-approve", "skip confirmation prompt").action(async (opts, cmd) => {
9925
9549
  const { json, yes } = getRootOpts(cmd);
9550
+ let projectConfig = null;
9926
9551
  try {
9552
+ projectConfig = getProjectConfig();
9927
9553
  await requireAuth();
9928
9554
  const tomlPath = resolve7(process.cwd(), opts.file);
9929
- const tomlSource = readFileSync12(tomlPath, "utf8");
9555
+ const tomlSource = readFileSync9(tomlPath, "utf8");
9930
9556
  const file = parseConfigToml(tomlSource);
9931
9557
  const res = await ossFetch("/api/metadata");
9932
9558
  const raw = await res.json();
9933
9559
  const live = liveFromMetadata(raw);
9934
9560
  const result = diffConfig({ live, file });
9935
9561
  const approved = opts.autoApprove || yes;
9562
+ const sectionsChanged = Array.from(
9563
+ new Set(result.changes.map((c) => changePath(c)))
9564
+ );
9936
9565
  if (!json) {
9937
9566
  console.log(formatPlan(result));
9938
9567
  }
@@ -9943,6 +9572,13 @@ function registerConfigApplyCommand(cfg) {
9943
9572
  );
9944
9573
  }
9945
9574
  await reportCliUsage("cli.config.apply", true);
9575
+ trackConfig("apply", projectConfig, {
9576
+ dry_run: !!opts.dryRun,
9577
+ json_mode: !!json,
9578
+ changes_count: result.changes.length,
9579
+ sections_changed: sectionsChanged,
9580
+ outcome: result.changes.length === 0 ? "no_changes" : "dry_run"
9581
+ });
9946
9582
  return;
9947
9583
  }
9948
9584
  if (!approved) {
@@ -9960,6 +9596,12 @@ function registerConfigApplyCommand(cfg) {
9960
9596
  if (!ok || p2.isCancel(ok)) {
9961
9597
  console.log("Aborted.");
9962
9598
  await reportCliUsage("cli.config.apply", true);
9599
+ trackConfig("apply", projectConfig, {
9600
+ json_mode: !!json,
9601
+ changes_count: result.changes.length,
9602
+ sections_changed: sectionsChanged,
9603
+ outcome: "aborted"
9604
+ });
9963
9605
  return;
9964
9606
  }
9965
9607
  }
@@ -9996,9 +9638,25 @@ function registerConfigApplyCommand(cfg) {
9996
9638
  }
9997
9639
  }
9998
9640
  await reportCliUsage("cli.config.apply", true);
9641
+ trackConfig("apply", projectConfig, {
9642
+ auto_approved: !!approved,
9643
+ json_mode: !!json,
9644
+ changes_count: result.changes.length,
9645
+ applied_count: applied.length,
9646
+ skipped_count: skipped.length,
9647
+ sections_changed: sectionsChanged,
9648
+ outcome: applied.length > 0 ? "applied" : "all_skipped"
9649
+ });
9999
9650
  } catch (err) {
10000
9651
  await reportCliUsage("cli.config.apply", false);
9652
+ trackConfig("apply", projectConfig, {
9653
+ json_mode: !!json,
9654
+ outcome: "error"
9655
+ });
9656
+ await shutdownAnalytics();
10001
9657
  handleError(err, json);
9658
+ } finally {
9659
+ await shutdownAnalytics();
10002
9660
  }
10003
9661
  });
10004
9662
  }
@@ -10083,8 +9741,8 @@ function registerConfigCommand(program2) {
10083
9741
  }
10084
9742
 
10085
9743
  // 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";
9744
+ import { appendFileSync as appendFileSync2, existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
9745
+ import { isAbsolute, join as join14, relative as relative3, resolve as resolve8 } from "path";
10088
9746
  import * as clack17 from "@clack/prompts";
10089
9747
  import pc7 from "picocolors";
10090
9748
 
@@ -10105,6 +9763,52 @@ async function getOpenRouterApiKey() {
10105
9763
  };
10106
9764
  }
10107
9765
 
9766
+ // src/lib/env-writer.ts
9767
+ import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
9768
+ var KEY_LINE_RE = (key) => (
9769
+ // Match `KEY=...` at the start of a line (allowing leading whitespace).
9770
+ // Captures the value side; we only need the value portion to compare.
9771
+ new RegExp(`^\\s*${key.replace(/[$.*+?^()[\\]{}|]/g, "\\$&")}\\s*=\\s*(.*)$`, "m")
9772
+ );
9773
+ function stripQuotes(v) {
9774
+ const t = v.trim();
9775
+ if (t.startsWith('"') && t.endsWith('"') && t.length >= 2 || t.startsWith("'") && t.endsWith("'") && t.length >= 2) {
9776
+ return t.slice(1, -1);
9777
+ }
9778
+ const hash = t.indexOf(" #");
9779
+ return hash >= 0 ? t.slice(0, hash).trimEnd() : t;
9780
+ }
9781
+ function upsertEnvFile(path6, entries) {
9782
+ const exists = existsSync11(path6);
9783
+ let content = exists ? readFileSync10(path6, "utf-8") : "";
9784
+ const result = { added: [], skipped: [], mismatched: [] };
9785
+ const additions = [];
9786
+ for (const [key, value] of Object.entries(entries)) {
9787
+ const re = KEY_LINE_RE(key);
9788
+ const match = content.match(re);
9789
+ if (match) {
9790
+ const existingValue = stripQuotes(match[1] ?? "");
9791
+ if (existingValue === value) {
9792
+ result.skipped.push(key);
9793
+ } else {
9794
+ result.mismatched.push({ key, existingValue, newValue: value });
9795
+ }
9796
+ continue;
9797
+ }
9798
+ additions.push(`${key}=${value}`);
9799
+ result.added.push(key);
9800
+ }
9801
+ if (additions.length > 0) {
9802
+ if (content.length > 0 && !content.endsWith("\n")) {
9803
+ content += "\n";
9804
+ }
9805
+ content += additions.join("\n") + "\n";
9806
+ writeFileSync8(path6, content);
9807
+ } else if (!exists) {
9808
+ }
9809
+ return result;
9810
+ }
9811
+
10108
9812
  // src/commands/ai/setup.ts
10109
9813
  var DEFAULT_ENV_FILE = ".env.local";
10110
9814
  var OPENROUTER_ENV_KEY = "OPENROUTER_API_KEY";
@@ -10196,7 +9900,7 @@ async function runAiSetup(opts) {
10196
9900
  };
10197
9901
  }
10198
9902
  function displayPath(path6) {
10199
- const rel = relative4(process.cwd(), path6);
9903
+ const rel = relative3(process.cwd(), path6);
10200
9904
  if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
10201
9905
  return path6;
10202
9906
  }
@@ -10210,12 +9914,12 @@ function isLocalEnvFile(envFile) {
10210
9914
  function ensureLocalEnvIgnored(cwd, envFile) {
10211
9915
  if (!isLocalEnvFile(envFile)) return false;
10212
9916
  const envPath = resolve8(cwd, envFile);
10213
- const relEnvPath = relative4(cwd, envPath);
9917
+ const relEnvPath = relative3(cwd, envPath);
10214
9918
  if (!relEnvPath || relEnvPath.startsWith("..") || isAbsolute(relEnvPath)) {
10215
9919
  return false;
10216
9920
  }
10217
- const gitignorePath = join17(cwd, ".gitignore");
10218
- const existing = existsSync15(gitignorePath) ? readFileSync13(gitignorePath, "utf-8") : "";
9921
+ const gitignorePath = join14(cwd, ".gitignore");
9922
+ const existing = existsSync12(gitignorePath) ? readFileSync11(gitignorePath, "utf-8") : "";
10219
9923
  const lines = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
10220
9924
  const envBasename = envFile.replace(/\\/g, "/").split("/").pop() ?? envFile;
10221
9925
  if (lines.has(".env*") || lines.has(".env.*") || lines.has(".env*.local") || lines.has(".env.local") && envBasename === ".env.local") {
@@ -10235,8 +9939,8 @@ function registerAiCommands(aiCmd2) {
10235
9939
  }
10236
9940
 
10237
9941
  // src/index.ts
10238
- var __dirname = dirname3(fileURLToPath(import.meta.url));
10239
- var pkg = JSON.parse(readFileSync14(join18(__dirname, "../package.json"), "utf-8"));
9942
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
9943
+ var pkg = JSON.parse(readFileSync12(join15(__dirname, "../package.json"), "utf-8"));
10240
9944
  var INSFORGE_LOGO = `
10241
9945
  \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
9946
  \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