@insforge/cli 0.1.78 → 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";
@@ -1172,7 +1172,7 @@ import * as clack5 from "@clack/prompts";
1172
1172
 
1173
1173
  // src/lib/analytics.ts
1174
1174
  import { PostHog } from "posthog-node";
1175
- var POSTHOG_API_KEY = "";
1175
+ var POSTHOG_API_KEY = "phc_ueV1ii62wdBTkH7E70ugyeqHIHu8dFDdjs0qq3TZhJz";
1176
1176
  var POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
1177
1177
  var client = null;
1178
1178
  function getClient() {
@@ -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.78";
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";
@@ -9190,9 +8765,67 @@ function validateAuth(input) {
9190
8765
  }
9191
8766
  out.allowed_redirect_urls = v;
9192
8767
  }
8768
+ if ("require_email_verification" in obj) {
8769
+ if (typeof obj.require_email_verification !== "boolean") {
8770
+ throw new ConfigValidationError(
8771
+ "auth.require_email_verification",
8772
+ "must be a boolean"
8773
+ );
8774
+ }
8775
+ out.require_email_verification = obj.require_email_verification;
8776
+ }
8777
+ if ("verify_email_method" in obj) {
8778
+ out.verify_email_method = validateVerificationMethod(
8779
+ "auth.verify_email_method",
8780
+ obj.verify_email_method
8781
+ );
8782
+ }
8783
+ if ("reset_password_method" in obj) {
8784
+ out.reset_password_method = validateVerificationMethod(
8785
+ "auth.reset_password_method",
8786
+ obj.reset_password_method
8787
+ );
8788
+ }
8789
+ if ("password" in obj) out.password = validatePassword(obj.password);
9193
8790
  if ("smtp" in obj) out.smtp = validateSmtp(obj.smtp);
9194
8791
  return out;
9195
8792
  }
8793
+ function validateVerificationMethod(path6, value) {
8794
+ if (value !== "code" && value !== "link") {
8795
+ throw new ConfigValidationError(path6, 'must be "code" or "link"');
8796
+ }
8797
+ return value;
8798
+ }
8799
+ function validatePassword(input) {
8800
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
8801
+ throw new ConfigValidationError("auth.password", "must be a table");
8802
+ }
8803
+ const obj = input;
8804
+ const out = {};
8805
+ if ("min_length" in obj) {
8806
+ if (typeof obj.min_length !== "number" || !Number.isInteger(obj.min_length) || obj.min_length < 4 || obj.min_length > 128) {
8807
+ throw new ConfigValidationError(
8808
+ "auth.password.min_length",
8809
+ "must be an integer between 4 and 128"
8810
+ );
8811
+ }
8812
+ out.min_length = obj.min_length;
8813
+ }
8814
+ for (const key of [
8815
+ "require_number",
8816
+ "require_lowercase",
8817
+ "require_uppercase",
8818
+ "require_special_char"
8819
+ ]) {
8820
+ if (key in obj) {
8821
+ if (typeof obj[key] !== "boolean") {
8822
+ throw new ConfigValidationError(`auth.password.${key}`, "must be a boolean");
8823
+ }
8824
+ out[key] = obj[key];
8825
+ }
8826
+ }
8827
+ return out;
8828
+ }
9196
8829
  function validateSmtp(input) {
9197
8830
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
9198
8831
  throw new ConfigValidationError("auth.smtp", "must be a table");
@@ -9275,11 +8908,13 @@ function stringifyConfigToml(config) {
9275
8908
  }
9276
8909
  if (config.auth) {
9277
8910
  lines.push("[auth]");
9278
- if (config.auth.allowed_redirect_urls !== void 0) {
9279
- const urls = config.auth.allowed_redirect_urls.map((u) => JSON.stringify(u)).join(", ");
9280
- lines.push(`allowed_redirect_urls = [${urls}]`);
9281
- }
8911
+ renderAuthFlatFields(config.auth, lines);
9282
8912
  lines.push("");
8913
+ if (config.auth.password !== void 0) {
8914
+ lines.push("[auth.password]");
8915
+ renderPasswordFields(config.auth.password, lines);
8916
+ lines.push("");
8917
+ }
9283
8918
  if (config.auth.smtp !== void 0) {
9284
8919
  lines.push("[auth.smtp]");
9285
8920
  renderSmtpFields(config.auth.smtp, lines);
@@ -9295,6 +8930,34 @@ function stringifyConfigToml(config) {
9295
8930
  }
9296
8931
  return lines.join("\n").replace(/\n+$/, "\n");
9297
8932
  }
8933
+ function renderAuthFlatFields(auth, lines) {
8934
+ if (auth.allowed_redirect_urls !== void 0) {
8935
+ const urls = auth.allowed_redirect_urls.map((u) => JSON.stringify(u)).join(", ");
8936
+ lines.push(`allowed_redirect_urls = [${urls}]`);
8937
+ }
8938
+ if (auth.require_email_verification !== void 0) {
8939
+ lines.push(`require_email_verification = ${auth.require_email_verification}`);
8940
+ }
8941
+ if (auth.verify_email_method !== void 0) {
8942
+ lines.push(`verify_email_method = ${JSON.stringify(auth.verify_email_method)}`);
8943
+ }
8944
+ if (auth.reset_password_method !== void 0) {
8945
+ lines.push(`reset_password_method = ${JSON.stringify(auth.reset_password_method)}`);
8946
+ }
8947
+ }
8948
+ function renderPasswordFields(pw, lines) {
8949
+ if (pw.min_length !== void 0) lines.push(`min_length = ${pw.min_length}`);
8950
+ if (pw.require_number !== void 0) lines.push(`require_number = ${pw.require_number}`);
8951
+ if (pw.require_lowercase !== void 0) {
8952
+ lines.push(`require_lowercase = ${pw.require_lowercase}`);
8953
+ }
8954
+ if (pw.require_uppercase !== void 0) {
8955
+ lines.push(`require_uppercase = ${pw.require_uppercase}`);
8956
+ }
8957
+ if (pw.require_special_char !== void 0) {
8958
+ lines.push(`require_special_char = ${pw.require_special_char}`);
8959
+ }
8960
+ }
9298
8961
  function renderSmtpFields(smtp, lines) {
9299
8962
  if (smtp.enabled !== void 0) lines.push(`enabled = ${smtp.enabled}`);
9300
8963
  if (smtp.host !== void 0) lines.push(`host = ${JSON.stringify(smtp.host)}`);
@@ -9318,6 +8981,132 @@ function renderSmtpFields(smtp, lines) {
9318
8981
  }
9319
8982
  }
9320
8983
 
8984
+ // src/lib/config-metadata.ts
8985
+ function liveFromMetadata(raw) {
8986
+ const live = { auth: {} };
8987
+ const a = isPlainObject(raw.auth) ? raw.auth : void 0;
8988
+ if (a && "allowedRedirectUrls" in a) {
8989
+ live.auth.allowed_redirect_urls = asStringArray(a.allowedRedirectUrls) ?? [];
8990
+ }
8991
+ if (a && "requireEmailVerification" in a) {
8992
+ live.auth.require_email_verification = a.requireEmailVerification ?? false;
8993
+ }
8994
+ if (a && "verifyEmailMethod" in a && (a.verifyEmailMethod === "code" || a.verifyEmailMethod === "link")) {
8995
+ live.auth.verify_email_method = a.verifyEmailMethod;
8996
+ }
8997
+ if (a && "resetPasswordMethod" in a && (a.resetPasswordMethod === "code" || a.resetPasswordMethod === "link")) {
8998
+ live.auth.reset_password_method = a.resetPasswordMethod;
8999
+ }
9000
+ if (a && ("passwordMinLength" in a || "requireNumber" in a || "requireLowercase" in a || "requireUppercase" in a || "requireSpecialChar" in a)) {
9001
+ live.auth.password = {
9002
+ min_length: a.passwordMinLength ?? 8,
9003
+ require_number: a.requireNumber ?? false,
9004
+ require_lowercase: a.requireLowercase ?? false,
9005
+ require_uppercase: a.requireUppercase ?? false,
9006
+ require_special_char: a.requireSpecialChar ?? false
9007
+ };
9008
+ }
9009
+ if (isPlainObject(a?.smtpConfig)) {
9010
+ const s = a.smtpConfig;
9011
+ live.auth.smtp = {
9012
+ enabled: s.enabled ?? false,
9013
+ host: s.host ?? "",
9014
+ port: s.port ?? 587,
9015
+ username: s.username ?? "",
9016
+ hasPassword: s.hasPassword ?? false,
9017
+ sender_email: s.senderEmail ?? "",
9018
+ sender_name: s.senderName ?? "",
9019
+ min_interval_seconds: s.minIntervalSeconds ?? 60
9020
+ };
9021
+ }
9022
+ const d = isPlainObject(raw.deployments) ? raw.deployments : void 0;
9023
+ if (d) {
9024
+ live.deployments = {
9025
+ subdomain: typeof d.customSlug === "string" && d.customSlug ? d.customSlug : null
9026
+ };
9027
+ }
9028
+ return live;
9029
+ }
9030
+ function isPlainObject(v) {
9031
+ return v !== null && typeof v === "object" && !Array.isArray(v);
9032
+ }
9033
+ function asStringArray(v) {
9034
+ return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : null;
9035
+ }
9036
+ function configFromMetadata(raw) {
9037
+ const config = {};
9038
+ const skipped = [];
9039
+ const a = isPlainObject(raw.auth) ? raw.auth : void 0;
9040
+ if (a && "allowedRedirectUrls" in a) {
9041
+ config.auth = config.auth ?? {};
9042
+ config.auth.allowed_redirect_urls = asStringArray(a.allowedRedirectUrls) ?? [];
9043
+ } else {
9044
+ skipped.push("auth.allowed_redirect_urls");
9045
+ }
9046
+ if (a && "requireEmailVerification" in a) {
9047
+ config.auth = config.auth ?? {};
9048
+ config.auth.require_email_verification = a.requireEmailVerification ?? false;
9049
+ } else {
9050
+ skipped.push("auth.require_email_verification");
9051
+ }
9052
+ if (a && "verifyEmailMethod" in a && (a.verifyEmailMethod === "code" || a.verifyEmailMethod === "link")) {
9053
+ config.auth = config.auth ?? {};
9054
+ config.auth.verify_email_method = a.verifyEmailMethod;
9055
+ } else {
9056
+ skipped.push("auth.verify_email_method");
9057
+ }
9058
+ if (a && "resetPasswordMethod" in a && (a.resetPasswordMethod === "code" || a.resetPasswordMethod === "link")) {
9059
+ config.auth = config.auth ?? {};
9060
+ config.auth.reset_password_method = a.resetPasswordMethod;
9061
+ } else {
9062
+ skipped.push("auth.reset_password_method");
9063
+ }
9064
+ if (a && ("passwordMinLength" in a || "requireNumber" in a || "requireLowercase" in a || "requireUppercase" in a || "requireSpecialChar" in a)) {
9065
+ config.auth = config.auth ?? {};
9066
+ config.auth.password = {};
9067
+ if ("passwordMinLength" in a) config.auth.password.min_length = a.passwordMinLength ?? 8;
9068
+ if ("requireNumber" in a) config.auth.password.require_number = a.requireNumber ?? false;
9069
+ if ("requireLowercase" in a) config.auth.password.require_lowercase = a.requireLowercase ?? false;
9070
+ if ("requireUppercase" in a) config.auth.password.require_uppercase = a.requireUppercase ?? false;
9071
+ if ("requireSpecialChar" in a) {
9072
+ config.auth.password.require_special_char = a.requireSpecialChar ?? false;
9073
+ }
9074
+ } else {
9075
+ skipped.push("auth.password");
9076
+ }
9077
+ if (a && "smtpConfig" in a) {
9078
+ const s = a.smtpConfig;
9079
+ if (isPlainObject(s)) {
9080
+ config.auth = config.auth ?? {};
9081
+ config.auth.smtp = {
9082
+ enabled: s.enabled ?? false,
9083
+ host: s.host ?? "",
9084
+ port: s.port ?? 587,
9085
+ username: s.username ?? "",
9086
+ // When backend has a password set, emit a deterministic env() placeholder
9087
+ // so the user knows which secret to define. We do NOT round-trip the
9088
+ // value (it never leaves the backend). Re-applying this TOML force-resends
9089
+ // from the secrets store — see config-diff.ts for the force-resend rationale.
9090
+ ...s.hasPassword ? { password: "env(SMTP_PASSWORD)" } : {},
9091
+ sender_email: s.senderEmail ?? "",
9092
+ sender_name: s.senderName ?? "",
9093
+ min_interval_seconds: s.minIntervalSeconds ?? 60
9094
+ };
9095
+ }
9096
+ } else {
9097
+ skipped.push("auth.smtp");
9098
+ }
9099
+ const d = isPlainObject(raw.deployments) ? raw.deployments : void 0;
9100
+ if (d) {
9101
+ if (typeof d.customSlug === "string" && d.customSlug) {
9102
+ config.deployments = { subdomain: d.customSlug };
9103
+ }
9104
+ } else {
9105
+ skipped.push("deployments.subdomain");
9106
+ }
9107
+ return { config, skipped };
9108
+ }
9109
+
9321
9110
  // src/commands/config/export.ts
9322
9111
  function registerConfigExportCommand(cfg) {
9323
9112
  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) => {
@@ -9325,7 +9114,7 @@ function registerConfigExportCommand(cfg) {
9325
9114
  try {
9326
9115
  await requireAuth();
9327
9116
  const target = resolve5(process.cwd(), opts.out);
9328
- if (existsSync14(target) && !opts.force) {
9117
+ if (existsSync10(target) && !opts.force) {
9329
9118
  if (json) {
9330
9119
  throw new CLIError(
9331
9120
  `${opts.out} exists. Re-run with --force to overwrite.`,
@@ -9344,46 +9133,9 @@ function registerConfigExportCommand(cfg) {
9344
9133
  }
9345
9134
  const res = await ossFetch("/api/metadata");
9346
9135
  const raw = await res.json();
9347
- const config = {};
9348
- const skipped = [];
9349
- const authSlice = raw?.auth;
9350
- if (authSlice && typeof authSlice === "object" && "allowedRedirectUrls" in authSlice) {
9351
- config.auth = config.auth ?? {};
9352
- config.auth.allowed_redirect_urls = authSlice.allowedRedirectUrls ?? [];
9353
- } else {
9354
- skipped.push("auth.allowed_redirect_urls");
9355
- }
9356
- if (authSlice && typeof authSlice === "object" && "smtpConfig" in authSlice && authSlice.smtpConfig) {
9357
- const s = authSlice.smtpConfig;
9358
- config.auth = config.auth ?? {};
9359
- config.auth.smtp = {
9360
- enabled: s.enabled ?? false,
9361
- host: s.host ?? "",
9362
- port: s.port ?? 587,
9363
- username: s.username ?? "",
9364
- // When backend has a password set, emit a deterministic env()
9365
- // placeholder so the user knows which secret to define. We do
9366
- // NOT round-trip the value (it never leaves the backend).
9367
- // Re-applying this TOML force-resends from the secrets store
9368
- // — see config-diff.ts for the force-resend rationale.
9369
- ...s.hasPassword ? { password: "env(SMTP_PASSWORD)" } : {},
9370
- sender_email: s.senderEmail ?? "",
9371
- sender_name: s.senderName ?? "",
9372
- min_interval_seconds: s.minIntervalSeconds ?? 60
9373
- };
9374
- } else {
9375
- skipped.push("auth.smtp");
9376
- }
9377
- const deploymentsSlice = raw?.deployments;
9378
- if (deploymentsSlice && typeof deploymentsSlice === "object") {
9379
- if (typeof deploymentsSlice.customSlug === "string" && deploymentsSlice.customSlug) {
9380
- config.deployments = { subdomain: deploymentsSlice.customSlug };
9381
- }
9382
- } else {
9383
- skipped.push("deployments.subdomain");
9384
- }
9136
+ const { config, skipped } = configFromMetadata(raw);
9385
9137
  const toml = stringifyConfigToml(config);
9386
- writeFileSync9(target, toml, "utf8");
9138
+ writeFileSync7(target, toml, "utf8");
9387
9139
  if (json) {
9388
9140
  console.log(JSON.stringify({ written: target, config, skipped }, null, 2));
9389
9141
  } else {
@@ -9405,7 +9157,7 @@ function registerConfigExportCommand(cfg) {
9405
9157
  }
9406
9158
 
9407
9159
  // src/commands/config/plan.ts
9408
- import { readFileSync as readFileSync11 } from "fs";
9160
+ import { readFileSync as readFileSync8 } from "fs";
9409
9161
  import { resolve as resolve6 } from "path";
9410
9162
  import pc5 from "picocolors";
9411
9163
 
@@ -9427,6 +9179,48 @@ function diffConfig({ live, file }) {
9427
9179
  });
9428
9180
  }
9429
9181
  }
9182
+ if (fileAuth && "require_email_verification" in fileAuth) {
9183
+ const fromV = liveAuth.require_email_verification ?? false;
9184
+ const toV = fileAuth.require_email_verification ?? false;
9185
+ if (fromV !== toV) {
9186
+ changes.push({
9187
+ section: "auth",
9188
+ op: "modify",
9189
+ key: "require_email_verification",
9190
+ from: fromV,
9191
+ to: toV
9192
+ });
9193
+ }
9194
+ }
9195
+ if (fileAuth && "verify_email_method" in fileAuth && fileAuth.verify_email_method) {
9196
+ const fromV = liveAuth.verify_email_method ?? "code";
9197
+ const toV = fileAuth.verify_email_method;
9198
+ if (fromV !== toV) {
9199
+ changes.push({
9200
+ section: "auth",
9201
+ op: "modify",
9202
+ key: "verify_email_method",
9203
+ from: fromV,
9204
+ to: toV
9205
+ });
9206
+ }
9207
+ }
9208
+ if (fileAuth && "reset_password_method" in fileAuth && fileAuth.reset_password_method) {
9209
+ const fromV = liveAuth.reset_password_method ?? "code";
9210
+ const toV = fileAuth.reset_password_method;
9211
+ if (fromV !== toV) {
9212
+ changes.push({
9213
+ section: "auth",
9214
+ op: "modify",
9215
+ key: "reset_password_method",
9216
+ from: fromV,
9217
+ to: toV
9218
+ });
9219
+ }
9220
+ }
9221
+ if (fileAuth?.password) {
9222
+ diffPassword(liveAuth.password, fileAuth.password, changes);
9223
+ }
9430
9224
  if (fileAuth?.smtp !== void 0) {
9431
9225
  const smtpChange = diffSmtp(liveAuth.smtp, fileAuth.smtp);
9432
9226
  if (smtpChange) changes.push(smtpChange);
@@ -9436,7 +9230,7 @@ function diffConfig({ live, file }) {
9436
9230
  if (fileDeployments && "subdomain" in fileDeployments) {
9437
9231
  const fromV = liveDeployments.subdomain ?? null;
9438
9232
  const rawTo = fileDeployments.subdomain;
9439
- const toV = rawTo === null || rawTo === "" ? null : rawTo;
9233
+ const toV = rawTo === null || rawTo === void 0 || rawTo === "" ? null : rawTo;
9440
9234
  if (fromV !== toV) {
9441
9235
  changes.push({
9442
9236
  section: "deployments",
@@ -9449,6 +9243,36 @@ function diffConfig({ live, file }) {
9449
9243
  }
9450
9244
  return { changes, summary: summarize(changes) };
9451
9245
  }
9246
+ function diffPassword(live, file, changes) {
9247
+ const liveView = live ?? EMPTY_PASSWORD_POLICY;
9248
+ if (file.min_length !== void 0 && liveView.min_length !== file.min_length) {
9249
+ changes.push({
9250
+ section: "auth.password",
9251
+ op: "modify",
9252
+ key: "min_length",
9253
+ from: liveView.min_length,
9254
+ to: file.min_length
9255
+ });
9256
+ }
9257
+ for (const key of [
9258
+ "require_number",
9259
+ "require_lowercase",
9260
+ "require_uppercase",
9261
+ "require_special_char"
9262
+ ]) {
9263
+ const fromV = liveView[key];
9264
+ const toV = file[key];
9265
+ if (toV !== void 0 && fromV !== toV) {
9266
+ changes.push({
9267
+ section: "auth.password",
9268
+ op: "modify",
9269
+ key,
9270
+ from: fromV,
9271
+ to: toV
9272
+ });
9273
+ }
9274
+ }
9275
+ }
9452
9276
  function diffSmtp(live, fileSmtp) {
9453
9277
  const livedView = renderLiveSmtp(live);
9454
9278
  const tomlView = renderFileSmtp(fileSmtp);
@@ -9507,6 +9331,13 @@ var EMPTY_SMTP_VIEW = {
9507
9331
  sender_name: "",
9508
9332
  min_interval_seconds: 60
9509
9333
  };
9334
+ var EMPTY_PASSWORD_POLICY = {
9335
+ min_length: 8,
9336
+ require_number: false,
9337
+ require_lowercase: false,
9338
+ require_uppercase: false,
9339
+ require_special_char: false
9340
+ };
9510
9341
  function summarize(changes) {
9511
9342
  const s = { add: 0, modify: 0, remove: 0, kept: 0 };
9512
9343
  for (const c of changes) {
@@ -9582,10 +9413,22 @@ function formatChange(c) {
9582
9413
  // src/lib/config-capabilities.ts
9583
9414
  function metadataSupports(raw, change) {
9584
9415
  if (change.section === "auth" && change.key === "allowed_redirect_urls") {
9585
- return raw?.auth !== void 0 && raw.auth !== null && typeof raw.auth === "object" && "allowedRedirectUrls" in raw.auth;
9416
+ return hasAuthKey(raw, "allowedRedirectUrls");
9417
+ }
9418
+ if (change.section === "auth" && change.key === "require_email_verification") {
9419
+ return hasAuthKey(raw, "requireEmailVerification");
9420
+ }
9421
+ if (change.section === "auth" && change.key === "verify_email_method") {
9422
+ return hasAuthKey(raw, "verifyEmailMethod");
9423
+ }
9424
+ if (change.section === "auth" && change.key === "reset_password_method") {
9425
+ return hasAuthKey(raw, "resetPasswordMethod");
9426
+ }
9427
+ if (change.section === "auth.password") {
9428
+ return hasAuthKey(raw, AUTH_PASSWORD_WIRE_KEY[change.key]);
9586
9429
  }
9587
9430
  if (change.section === "auth.smtp") {
9588
- return raw?.auth !== void 0 && raw.auth !== null && typeof raw.auth === "object" && "smtpConfig" in raw.auth;
9431
+ return hasAuthKey(raw, "smtpConfig");
9589
9432
  }
9590
9433
  if (change.section === "deployments" && change.key === "subdomain") {
9591
9434
  return raw?.deployments !== void 0 && raw.deployments !== null && typeof raw.deployments === "object";
@@ -9594,10 +9437,24 @@ function metadataSupports(raw, change) {
9594
9437
  void _exhaustive;
9595
9438
  return false;
9596
9439
  }
9440
+ function hasAuthKey(raw, key) {
9441
+ const auth = raw?.auth;
9442
+ return auth !== void 0 && auth !== null && typeof auth === "object" && key in auth;
9443
+ }
9444
+ var AUTH_PASSWORD_WIRE_KEY = {
9445
+ min_length: "passwordMinLength",
9446
+ require_number: "requireNumber",
9447
+ require_lowercase: "requireLowercase",
9448
+ require_uppercase: "requireUppercase",
9449
+ require_special_char: "requireSpecialChar"
9450
+ };
9597
9451
  function changePath(change) {
9598
9452
  if (change.section === "auth.smtp") return "auth.smtp";
9599
9453
  return `${change.section}.${change.key}`;
9600
9454
  }
9455
+ function authPasswordWireKey(key) {
9456
+ return AUTH_PASSWORD_WIRE_KEY[key];
9457
+ }
9601
9458
 
9602
9459
  // src/commands/config/plan.ts
9603
9460
  function registerConfigPlanCommand(cfg) {
@@ -9606,13 +9463,11 @@ function registerConfigPlanCommand(cfg) {
9606
9463
  try {
9607
9464
  await requireAuth();
9608
9465
  const tomlPath = resolve6(process.cwd(), opts.file);
9609
- const tomlSource = readFileSync11(tomlPath, "utf8");
9466
+ const tomlSource = readFileSync8(tomlPath, "utf8");
9610
9467
  const file = parseConfigToml(tomlSource);
9611
9468
  const res = await ossFetch("/api/metadata");
9612
9469
  const raw = await res.json();
9613
- const live = {
9614
- auth: { allowed_redirect_urls: raw.auth?.allowedRedirectUrls ?? [] }
9615
- };
9470
+ const live = liveFromMetadata(raw);
9616
9471
  const result = diffConfig({ live, file });
9617
9472
  const skipped = result.changes.filter((c) => !metadataSupports(raw, c)).map((c) => changePath(c));
9618
9473
  if (json) {
@@ -9636,7 +9491,7 @@ function registerConfigPlanCommand(cfg) {
9636
9491
  }
9637
9492
 
9638
9493
  // src/commands/config/apply.ts
9639
- import { readFileSync as readFileSync12 } from "fs";
9494
+ import { readFileSync as readFileSync9 } from "fs";
9640
9495
  import { resolve as resolve7 } from "path";
9641
9496
  import * as p2 from "@clack/prompts";
9642
9497
  import pc6 from "picocolors";
@@ -9646,7 +9501,7 @@ function registerConfigApplyCommand(cfg) {
9646
9501
  try {
9647
9502
  await requireAuth();
9648
9503
  const tomlPath = resolve7(process.cwd(), opts.file);
9649
- const tomlSource = readFileSync12(tomlPath, "utf8");
9504
+ const tomlSource = readFileSync9(tomlPath, "utf8");
9650
9505
  const file = parseConfigToml(tomlSource);
9651
9506
  const res = await ossFetch("/api/metadata");
9652
9507
  const raw = await res.json();
@@ -9722,29 +9577,6 @@ function registerConfigApplyCommand(cfg) {
9722
9577
  }
9723
9578
  });
9724
9579
  }
9725
- function liveFromMetadata(raw) {
9726
- const live = { auth: {} };
9727
- if (raw.auth?.allowedRedirectUrls !== void 0) {
9728
- live.auth.allowed_redirect_urls = raw.auth.allowedRedirectUrls;
9729
- }
9730
- if (raw.auth?.smtpConfig) {
9731
- const s = raw.auth.smtpConfig;
9732
- live.auth.smtp = {
9733
- enabled: s.enabled ?? false,
9734
- host: s.host ?? "",
9735
- port: s.port ?? 587,
9736
- username: s.username ?? "",
9737
- hasPassword: s.hasPassword ?? false,
9738
- sender_email: s.senderEmail ?? "",
9739
- sender_name: s.senderName ?? "",
9740
- min_interval_seconds: s.minIntervalSeconds ?? 60
9741
- };
9742
- }
9743
- if (raw.deployments) {
9744
- live.deployments = { subdomain: raw.deployments.customSlug ?? null };
9745
- }
9746
- return live;
9747
- }
9748
9580
  async function applyChange(change) {
9749
9581
  if (change.section === "auth" && change.key === "allowed_redirect_urls") {
9750
9582
  await ossFetch("/api/auth/config", {
@@ -9753,6 +9585,35 @@ async function applyChange(change) {
9753
9585
  });
9754
9586
  return;
9755
9587
  }
9588
+ if (change.section === "auth" && change.key === "require_email_verification") {
9589
+ await ossFetch("/api/auth/config", {
9590
+ method: "PUT",
9591
+ body: JSON.stringify({ requireEmailVerification: change.to })
9592
+ });
9593
+ return;
9594
+ }
9595
+ if (change.section === "auth" && change.key === "verify_email_method") {
9596
+ await ossFetch("/api/auth/config", {
9597
+ method: "PUT",
9598
+ body: JSON.stringify({ verifyEmailMethod: change.to })
9599
+ });
9600
+ return;
9601
+ }
9602
+ if (change.section === "auth" && change.key === "reset_password_method") {
9603
+ await ossFetch("/api/auth/config", {
9604
+ method: "PUT",
9605
+ body: JSON.stringify({ resetPasswordMethod: change.to })
9606
+ });
9607
+ return;
9608
+ }
9609
+ if (change.section === "auth.password") {
9610
+ const wireKey = authPasswordWireKey(change.key);
9611
+ await ossFetch("/api/auth/config", {
9612
+ method: "PUT",
9613
+ body: JSON.stringify({ [wireKey]: change.to })
9614
+ });
9615
+ return;
9616
+ }
9756
9617
  if (change.section === "auth.smtp") {
9757
9618
  const to = change.to;
9758
9619
  const body = {
@@ -9797,8 +9658,8 @@ function registerConfigCommand(program2) {
9797
9658
  }
9798
9659
 
9799
9660
  // src/commands/ai/setup.ts
9800
- import { appendFileSync as appendFileSync2, existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
9801
- 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";
9802
9663
  import * as clack17 from "@clack/prompts";
9803
9664
  import pc7 from "picocolors";
9804
9665
 
@@ -9819,6 +9680,52 @@ async function getOpenRouterApiKey() {
9819
9680
  };
9820
9681
  }
9821
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
+
9822
9729
  // src/commands/ai/setup.ts
9823
9730
  var DEFAULT_ENV_FILE = ".env.local";
9824
9731
  var OPENROUTER_ENV_KEY = "OPENROUTER_API_KEY";
@@ -9910,7 +9817,7 @@ async function runAiSetup(opts) {
9910
9817
  };
9911
9818
  }
9912
9819
  function displayPath(path6) {
9913
- const rel = relative4(process.cwd(), path6);
9820
+ const rel = relative3(process.cwd(), path6);
9914
9821
  if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
9915
9822
  return path6;
9916
9823
  }
@@ -9924,12 +9831,12 @@ function isLocalEnvFile(envFile) {
9924
9831
  function ensureLocalEnvIgnored(cwd, envFile) {
9925
9832
  if (!isLocalEnvFile(envFile)) return false;
9926
9833
  const envPath = resolve8(cwd, envFile);
9927
- const relEnvPath = relative4(cwd, envPath);
9834
+ const relEnvPath = relative3(cwd, envPath);
9928
9835
  if (!relEnvPath || relEnvPath.startsWith("..") || isAbsolute(relEnvPath)) {
9929
9836
  return false;
9930
9837
  }
9931
- const gitignorePath = join17(cwd, ".gitignore");
9932
- const existing = existsSync15(gitignorePath) ? readFileSync13(gitignorePath, "utf-8") : "";
9838
+ const gitignorePath = join14(cwd, ".gitignore");
9839
+ const existing = existsSync12(gitignorePath) ? readFileSync11(gitignorePath, "utf-8") : "";
9933
9840
  const lines = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
9934
9841
  const envBasename = envFile.replace(/\\/g, "/").split("/").pop() ?? envFile;
9935
9842
  if (lines.has(".env*") || lines.has(".env.*") || lines.has(".env*.local") || lines.has(".env.local") && envBasename === ".env.local") {
@@ -9949,8 +9856,8 @@ function registerAiCommands(aiCmd2) {
9949
9856
  }
9950
9857
 
9951
9858
  // src/index.ts
9952
- var __dirname = dirname3(fileURLToPath(import.meta.url));
9953
- 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"));
9954
9861
  var INSFORGE_LOGO = `
9955
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
9956
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