@insforge/cli 0.1.76 → 0.1.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync13 } from "fs";
5
- import { join as join17, dirname as dirname3 } from "path";
4
+ import { readFileSync as readFileSync14 } from "fs";
5
+ import { join as join18, dirname as dirname3 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { Command } from "commander";
8
- import * as clack17 from "@clack/prompts";
8
+ import * as clack18 from "@clack/prompts";
9
9
 
10
10
  // src/lib/prompts.ts
11
11
  import * as readline from "readline";
@@ -41,8 +41,8 @@ var LineReader = class {
41
41
  this.output.write(prompt);
42
42
  if (this.queue.length > 0) return this.queue.shift();
43
43
  if (this.closed) return null;
44
- return new Promise((resolve8) => {
45
- this.waiter = resolve8;
44
+ return new Promise((resolve9) => {
45
+ this.waiter = resolve9;
46
46
  });
47
47
  }
48
48
  close() {
@@ -424,8 +424,8 @@ function startCallbackServer() {
424
424
  return new Promise((resolveServer) => {
425
425
  let resolveResult;
426
426
  let rejectResult;
427
- const resultPromise = new Promise((resolve8, reject) => {
428
- resolveResult = resolve8;
427
+ const resultPromise = new Promise((resolve9, reject) => {
428
+ resolveResult = resolve9;
429
429
  rejectResult = reject;
430
430
  });
431
431
  const server = createServer((req, res) => {
@@ -1329,36 +1329,36 @@ function registerBranchCreateCommand(branch) {
1329
1329
  throw new CLIError(`Invalid --mode: ${opts.mode} (must be "full" or "schema-only")`);
1330
1330
  }
1331
1331
  const mode = opts.mode;
1332
- const spinner10 = !json ? clack5.spinner() : null;
1332
+ const spinner11 = !json ? clack5.spinner() : null;
1333
1333
  let ready;
1334
1334
  let provisioned = false;
1335
1335
  try {
1336
- spinner10?.start(`Creating branch '${name}'...`);
1336
+ spinner11?.start(`Creating branch '${name}'...`);
1337
1337
  const created = await createBranchApi(project.project_id, { mode, name }, apiUrl);
1338
1338
  captureEvent(project.project_id, "cli_branch_create", {
1339
1339
  mode,
1340
1340
  parent_project_id: project.project_id
1341
1341
  });
1342
- spinner10?.message(`Branch '${name}' created (appkey: ${created.appkey}). Provisioning...`);
1343
- ready = await pollUntilReady(created.id, apiUrl, spinner10);
1342
+ spinner11?.message(`Branch '${name}' created (appkey: ${created.appkey}). Provisioning...`);
1343
+ ready = await pollUntilReady(created.id, apiUrl, spinner11);
1344
1344
  provisioned = ready.branch_state === "ready";
1345
1345
  if (provisioned && opts.switch) {
1346
- spinner10?.message("Branch ready. Switching context...");
1346
+ spinner11?.message("Branch ready. Switching context...");
1347
1347
  await runBranchSwitch({ name, apiUrl, json, silent: true });
1348
- spinner10?.stop(`Branch '${name}' is ready and active`);
1348
+ spinner11?.stop(`Branch '${name}' is ready and active`);
1349
1349
  } else if (provisioned) {
1350
- spinner10?.stop(`Branch '${name}' is ready`);
1350
+ spinner11?.stop(`Branch '${name}' is ready`);
1351
1351
  } else {
1352
- spinner10?.stop(`Branch '${name}' is in '${ready.branch_state}' state`);
1352
+ spinner11?.stop(`Branch '${name}' is in '${ready.branch_state}' state`);
1353
1353
  }
1354
1354
  } catch (err) {
1355
1355
  if (provisioned) {
1356
- spinner10?.stop(
1356
+ spinner11?.stop(
1357
1357
  `Branch '${name}' is ready, but switching context failed \u2014 run \`insforge branch switch ${name}\` to retry`,
1358
1358
  1
1359
1359
  );
1360
1360
  } else {
1361
- spinner10?.stop(`Branch '${name}' creation failed`, 1);
1361
+ spinner11?.stop(`Branch '${name}' creation failed`, 1);
1362
1362
  }
1363
1363
  throw err;
1364
1364
  }
@@ -1382,7 +1382,7 @@ function registerBranchCreateCommand(branch) {
1382
1382
  }
1383
1383
  });
1384
1384
  }
1385
- async function pollUntilReady(branchId, apiUrl, spinner10) {
1385
+ async function pollUntilReady(branchId, apiUrl, spinner11) {
1386
1386
  const start = Date.now();
1387
1387
  let lastState = "";
1388
1388
  while (Date.now() - start < POLL_TIMEOUT_MS) {
@@ -1391,8 +1391,8 @@ async function pollUntilReady(branchId, apiUrl, spinner10) {
1391
1391
  if (branch2.branch_state === "deleted" || branch2.branch_state === "conflicted") {
1392
1392
  throw new CLIError(`Branch creation failed (state: ${branch2.branch_state})`);
1393
1393
  }
1394
- if (spinner10 && branch2.branch_state !== lastState) {
1395
- spinner10.message(`Provisioning branch (state: ${branch2.branch_state})...`);
1394
+ if (spinner11 && branch2.branch_state !== lastState) {
1395
+ spinner11.message(`Provisioning branch (state: ${branch2.branch_state})...`);
1396
1396
  lastState = branch2.branch_state;
1397
1397
  }
1398
1398
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
@@ -1946,6 +1946,9 @@ ${err.nextActions}`;
1946
1946
  if (res.status === 404 && isRouteLevel404 && path6 === "/api/database/migrations") {
1947
1947
  message = "Database migrations are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin about database migration support.";
1948
1948
  }
1949
+ if (res.status === 404 && isRouteLevel404 && path6.startsWith("/api/ai")) {
1950
+ message = "AI Model Gateway setup is not available on this backend.\nUpgrade your InsForge project to a version with Model Gateway support, or keep using the legacy @insforge/sdk AI modules for projects that still rely on the older AI API surface.";
1951
+ }
1949
1952
  throw new CLIError(message);
1950
1953
  }
1951
1954
  return res;
@@ -2346,11 +2349,11 @@ async function collectDeploymentFiles(sourceDir) {
2346
2349
  return files;
2347
2350
  }
2348
2351
  async function createZipBuffer(sourceDir) {
2349
- return new Promise((resolve8, reject) => {
2352
+ return new Promise((resolve9, reject) => {
2350
2353
  const archive = archiver("zip", { zlib: { level: 9 } });
2351
2354
  const chunks = [];
2352
2355
  archive.on("data", (chunk) => chunks.push(chunk));
2353
- archive.on("end", () => resolve8(Buffer.concat(chunks)));
2356
+ archive.on("end", () => resolve9(Buffer.concat(chunks)));
2354
2357
  archive.on("error", (err) => reject(err));
2355
2358
  archive.directory(sourceDir, false, (entry) => {
2356
2359
  if (shouldExclude(entry.name)) return false;
@@ -2427,12 +2430,12 @@ async function startDirectDeployment(deploymentId, startBody) {
2427
2430
  });
2428
2431
  await response.json();
2429
2432
  }
2430
- async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
2431
- spinner10?.message("Building and deploying...");
2433
+ async function pollDeployment(deploymentId, spinner11, syncBeforeRead) {
2434
+ spinner11?.message("Building and deploying...");
2432
2435
  const startTime = Date.now();
2433
2436
  let deployment = null;
2434
2437
  while (Date.now() - startTime < POLL_TIMEOUT_MS3) {
2435
- await new Promise((resolve8) => setTimeout(resolve8, POLL_INTERVAL_MS3));
2438
+ await new Promise((resolve9) => setTimeout(resolve9, POLL_INTERVAL_MS3));
2436
2439
  try {
2437
2440
  if (syncBeforeRead) {
2438
2441
  await ossFetch(`/api/deployments/${deploymentId}/sync`, { method: "POST" });
@@ -2444,13 +2447,13 @@ async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
2444
2447
  break;
2445
2448
  }
2446
2449
  if (status === "ERROR" || status === "CANCELED") {
2447
- spinner10?.stop("Deployment failed");
2450
+ spinner11?.stop("Deployment failed");
2448
2451
  throw new CLIError(
2449
2452
  getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`
2450
2453
  );
2451
2454
  }
2452
2455
  const elapsed = Math.round((Date.now() - startTime) / 1e3);
2453
- spinner10?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
2456
+ spinner11?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
2454
2457
  } catch (err) {
2455
2458
  if (err instanceof CLIError) throw err;
2456
2459
  }
@@ -2460,20 +2463,20 @@ async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
2460
2463
  return { deploymentId, deployment, isReady, liveUrl };
2461
2464
  }
2462
2465
  async function deployProjectDirect(opts, config) {
2463
- const { sourceDir, startBody = {}, spinner: spinner10 } = opts;
2464
- spinner10?.start("Scanning source files...");
2466
+ const { sourceDir, startBody = {}, spinner: spinner11 } = opts;
2467
+ spinner11?.start("Scanning source files...");
2465
2468
  const localFiles = await collectDeploymentFiles(sourceDir);
2466
2469
  if (localFiles.length === 0) {
2467
2470
  throw new CLIError("No deployable files found in the source directory.");
2468
2471
  }
2469
- spinner10?.message("Creating deployment...");
2472
+ spinner11?.message("Creating deployment...");
2470
2473
  const createResult = await createDirectDeploymentSession(
2471
2474
  config,
2472
2475
  localFiles.map(({ path: relativePath, sha, size }) => ({ path: relativePath, sha, size }))
2473
2476
  );
2474
2477
  const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
2475
2478
  const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
2476
- spinner10?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
2479
+ spinner11?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
2477
2480
  await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
2478
2481
  const localFile = localFileByPath.get(manifestFile.path);
2479
2482
  if (!localFile) {
@@ -2484,18 +2487,18 @@ async function deployProjectDirect(opts, config) {
2484
2487
  }
2485
2488
  await uploadDirectDeploymentFile(createResult.id, manifestFile, localFile);
2486
2489
  });
2487
- spinner10?.message("Starting deployment...");
2490
+ spinner11?.message("Starting deployment...");
2488
2491
  await startDirectDeployment(createResult.id, startBody);
2489
- return await pollDeployment(createResult.id, spinner10, !isInsforgeCloudOssHost(config.oss_host));
2492
+ return await pollDeployment(createResult.id, spinner11, !isInsforgeCloudOssHost(config.oss_host));
2490
2493
  }
2491
2494
  async function deployProjectLegacy(opts) {
2492
- const { sourceDir, startBody = {}, spinner: spinner10 } = opts;
2493
- spinner10?.message("Creating deployment...");
2495
+ const { sourceDir, startBody = {}, spinner: spinner11 } = opts;
2496
+ spinner11?.message("Creating deployment...");
2494
2497
  const createRes = await ossFetch("/api/deployments", { method: "POST" });
2495
2498
  const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
2496
- spinner10?.message("Compressing source files...");
2499
+ spinner11?.message("Compressing source files...");
2497
2500
  const zipBuffer = await createZipBuffer(sourceDir);
2498
- spinner10?.message("Uploading...");
2501
+ spinner11?.message("Uploading...");
2499
2502
  const formData = new FormData();
2500
2503
  for (const [key, value] of Object.entries(uploadFields)) {
2501
2504
  formData.append(key, value);
@@ -2506,13 +2509,13 @@ async function deployProjectLegacy(opts) {
2506
2509
  const uploadErr = await uploadRes.text();
2507
2510
  throw new CLIError(`Failed to upload: ${uploadErr}`);
2508
2511
  }
2509
- spinner10?.message("Starting deployment...");
2512
+ spinner11?.message("Starting deployment...");
2510
2513
  const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
2511
2514
  method: "POST",
2512
2515
  body: JSON.stringify(startBody)
2513
2516
  });
2514
2517
  await startRes.json();
2515
- return await pollDeployment(deploymentId, spinner10, false);
2518
+ return await pollDeployment(deploymentId, spinner11, false);
2516
2519
  }
2517
2520
  async function deployProject(opts) {
2518
2521
  const config = getProjectConfig();
@@ -2547,7 +2550,7 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
2547
2550
  `"${dirName}" is an excluded directory and cannot be used as a deploy source. Please specify your project root or output directory instead.`
2548
2551
  );
2549
2552
  }
2550
- const spinner10 = !json ? clack11.spinner() : null;
2553
+ const spinner11 = !json ? clack11.spinner() : null;
2551
2554
  const startBody = {};
2552
2555
  if (opts.env) {
2553
2556
  try {
@@ -2573,9 +2576,9 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
2573
2576
  throw new CLIError("Invalid --meta JSON.");
2574
2577
  }
2575
2578
  }
2576
- const result = await deployProject({ sourceDir, startBody, spinner: spinner10 });
2579
+ const result = await deployProject({ sourceDir, startBody, spinner: spinner11 });
2577
2580
  if (result.isReady) {
2578
- spinner10?.stop("Deployment complete");
2581
+ spinner11?.stop("Deployment complete");
2579
2582
  if (json) {
2580
2583
  outputJson(result.deployment);
2581
2584
  } else {
@@ -2585,7 +2588,7 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
2585
2588
  clack11.log.info(`Deployment ID: ${result.deploymentId}`);
2586
2589
  }
2587
2590
  } else {
2588
- spinner10?.stop("Deployment is still building");
2591
+ spinner11?.stop("Deployment is still building");
2589
2592
  if (json) {
2590
2593
  outputJson({
2591
2594
  id: result.deploymentId,
@@ -3151,13 +3154,13 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
3151
3154
  // src/commands/projects/link.ts
3152
3155
  var execAsync3 = promisify4(exec3);
3153
3156
  async function runNpmInstall(startMessage = "Installing dependencies...") {
3154
- const spinner10 = clack13.spinner();
3155
- spinner10.start(startMessage);
3157
+ const spinner11 = clack13.spinner();
3158
+ spinner11.start(startMessage);
3156
3159
  try {
3157
3160
  await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
3158
- spinner10.stop("Dependencies installed");
3161
+ spinner11.stop("Dependencies installed");
3159
3162
  } catch (err) {
3160
- spinner10.stop("Failed to install dependencies");
3163
+ spinner11.stop("Failed to install dependencies");
3161
3164
  clack13.log.warn(`npm install failed: ${err.message}`);
3162
3165
  clack13.log.info("Run `npm install` manually to install dependencies.");
3163
3166
  }
@@ -3171,13 +3174,13 @@ async function runNpmSetupIfPresent() {
3171
3174
  } catch {
3172
3175
  }
3173
3176
  if (!hasSetup) return;
3174
- const spinner10 = clack13.spinner();
3175
- spinner10.start("Running setup (schema + migrations)...");
3177
+ const spinner11 = clack13.spinner();
3178
+ spinner11.start("Running setup (schema + migrations)...");
3176
3179
  try {
3177
3180
  await execAsync3("npm run setup", { cwd: process.cwd(), maxBuffer: 20 * 1024 * 1024 });
3178
- spinner10.stop("Setup complete");
3181
+ spinner11.stop("Setup complete");
3179
3182
  } catch (err) {
3180
- spinner10.stop("Setup failed");
3183
+ spinner11.stop("Setup failed");
3181
3184
  clack13.log.warn(`npm run setup failed: ${err.message.split("\n")[0]}`);
3182
3185
  clack13.log.info("Inspect the error, fix DATABASE_URL or network access, then run `npm run setup` manually.");
3183
3186
  }
@@ -3203,7 +3206,7 @@ function registerProjectLinkCommand(program2) {
3203
3206
  outputJson({ success: true, skills_only: true });
3204
3207
  } else {
3205
3208
  clack13.note(
3206
- `Open your coding agent (Claude Code, Codex, Cursor, etc.) and ask it to build something. It will walk you through provisioning an InsForge project when needed.`,
3209
+ `Open your coding agent (Claude Code, Codex, Cursor, etc.) and ask it to build something. It will walk you through provisioning an InsForge project when needed. If you're not signed in yet, your browser will open for sign-in at that point.`,
3207
3210
  "What's next"
3208
3211
  );
3209
3212
  }
@@ -6017,7 +6020,7 @@ primary_region = "${opts.region}"
6017
6020
  };
6018
6021
  }
6019
6022
  function flyctlBuildAndPush(opts) {
6020
- return new Promise((resolve8, reject) => {
6023
+ return new Promise((resolve9, reject) => {
6021
6024
  const cleanupStub = ensureFlyTomlStub({
6022
6025
  dir: opts.dir,
6023
6026
  appId: opts.appId,
@@ -6073,7 +6076,7 @@ function flyctlBuildAndPush(opts) {
6073
6076
  )
6074
6077
  );
6075
6078
  }
6076
- resolve8({ imageRef: `registry.fly.io/${opts.appId}@${m[1]}` });
6079
+ resolve9({ imageRef: `registry.fly.io/${opts.appId}@${m[1]}` });
6077
6080
  });
6078
6081
  });
6079
6082
  }
@@ -6958,7 +6961,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
6958
6961
  const s = !json ? clack15.spinner() : null;
6959
6962
  s?.start("Collecting diagnostic data...");
6960
6963
  const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
6961
- const cliVersion = "0.1.76";
6964
+ const cliVersion = "0.1.79";
6962
6965
  s?.stop("Data collected");
6963
6966
  if (!json) {
6964
6967
  console.log(`
@@ -8472,14 +8475,14 @@ async function startPosthogCliFlow(projectId, jwt, apiUrl) {
8472
8475
  throw new CLIError("PostHog cli-start returned an unexpected response shape.");
8473
8476
  }
8474
8477
  function sleep(ms, signal) {
8475
- return new Promise((resolve8, reject) => {
8478
+ return new Promise((resolve9, reject) => {
8476
8479
  if (signal?.aborted) {
8477
8480
  reject(new CLIError("Connection wait cancelled."));
8478
8481
  return;
8479
8482
  }
8480
8483
  const timer = setTimeout(() => {
8481
8484
  signal?.removeEventListener("abort", onAbort);
8482
- resolve8();
8485
+ resolve9();
8483
8486
  }, ms);
8484
8487
  const onAbort = () => {
8485
8488
  clearTimeout(timer);
@@ -8782,8 +8785,8 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8782
8785
  } catch {
8783
8786
  }
8784
8787
  }
8785
- const spinner10 = !opts.json && isInteractive ? clack16.spinner() : null;
8786
- spinner10?.start("Waiting for connection... (timeout: 15 minutes)");
8788
+ const spinner11 = !opts.json && isInteractive ? clack16.spinner() : null;
8789
+ spinner11?.start("Waiting for connection... (timeout: 15 minutes)");
8787
8790
  try {
8788
8791
  const conn = await pollPosthogConnection(
8789
8792
  projectId,
@@ -8793,20 +8796,20 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8793
8796
  timeoutMs: POLL_TIMEOUT_MS4,
8794
8797
  maxTransientRetries: MAX_TRANSIENT_RETRIES,
8795
8798
  onTick: (elapsed) => {
8796
- if (spinner10) {
8799
+ if (spinner11) {
8797
8800
  const secs = Math.floor(elapsed / 1e3);
8798
8801
  const mins = Math.floor(secs / 60);
8799
8802
  const remaining = `${mins}m ${secs % 60}s elapsed`;
8800
- spinner10.message(`Waiting for connection... (${remaining})`);
8803
+ spinner11.message(`Waiting for connection... (${remaining})`);
8801
8804
  }
8802
8805
  }
8803
8806
  },
8804
8807
  opts.apiUrl
8805
8808
  );
8806
- spinner10?.stop("Connection received from PostHog.");
8809
+ spinner11?.stop("Connection received from PostHog.");
8807
8810
  return conn;
8808
8811
  } catch (err) {
8809
- spinner10?.stop("Connection wait failed.");
8812
+ spinner11?.stop("Connection wait failed.");
8810
8813
  throw err;
8811
8814
  }
8812
8815
  }
@@ -8824,14 +8827,14 @@ function resolveFramework(opts) {
8824
8827
  }
8825
8828
  async function installSdk(pm, cwd, opts) {
8826
8829
  const cmd = installCommand(pm, "posthog-js");
8827
- const spinner10 = !opts.json && isInteractive ? clack16.spinner() : null;
8828
- spinner10?.start(`Installing posthog-js (${cmd})...`);
8830
+ const spinner11 = !opts.json && isInteractive ? clack16.spinner() : null;
8831
+ spinner11?.start(`Installing posthog-js (${cmd})...`);
8829
8832
  try {
8830
8833
  await runInstall(pm, "posthog-js", cwd);
8831
- spinner10?.stop("Installed posthog-js.");
8834
+ spinner11?.stop("Installed posthog-js.");
8832
8835
  return true;
8833
8836
  } catch (err) {
8834
- spinner10?.stop("Install failed.");
8837
+ spinner11?.stop("Install failed.");
8835
8838
  if (!opts.json) {
8836
8839
  clack16.log.warn(
8837
8840
  `Could not run \`${cmd}\` automatically: ${err.message}
@@ -9060,6 +9063,75 @@ import pc4 from "picocolors";
9060
9063
  // src/lib/config-toml.ts
9061
9064
  import * as smolToml from "smol-toml";
9062
9065
 
9066
+ // src/lib/config-secrets.ts
9067
+ var ENV_REF_PATTERN = /^env\(([A-Z_][A-Z0-9_]*)\)$/;
9068
+ function parseEnvRef(value) {
9069
+ const match = value.match(ENV_REF_PATTERN);
9070
+ return match ? match[1] : null;
9071
+ }
9072
+ function validateSensitiveString(path6, value, suggestedSecretName) {
9073
+ if (typeof value !== "string") {
9074
+ throw new ConfigValidationError(path6, "must be a string");
9075
+ }
9076
+ if (parseEnvRef(value) !== null) {
9077
+ return value;
9078
+ }
9079
+ throw new ConfigValidationError(
9080
+ path6,
9081
+ `sensitive field must be an env() reference; got literal value.
9082
+ fix:
9083
+ 1. insforge secrets add ${suggestedSecretName} "<value>"
9084
+ 2. update insforge.toml:
9085
+ ${path6.split(".").pop()} = "env(${suggestedSecretName})"
9086
+ 3. insforge config apply`
9087
+ );
9088
+ }
9089
+ async function resolveEnvRef(envRef, fieldPath) {
9090
+ const secretName = parseEnvRef(envRef);
9091
+ if (!secretName) {
9092
+ throw new ConfigValidationError(
9093
+ fieldPath,
9094
+ `expected env() reference, got "${envRef}"`
9095
+ );
9096
+ }
9097
+ let res;
9098
+ try {
9099
+ res = await ossFetch(`/api/secrets/${encodeURIComponent(secretName)}`);
9100
+ } catch (err) {
9101
+ const message = err.message ?? "";
9102
+ if (/not found/i.test(message)) {
9103
+ throw new CLIError(
9104
+ `${fieldPath} references env(${secretName}) but no such secret exists.
9105
+ fix: insforge secrets add ${secretName} "<value>"`,
9106
+ 1,
9107
+ "SECRET_NOT_FOUND"
9108
+ );
9109
+ }
9110
+ throw new CLIError(
9111
+ `failed to resolve env(${secretName}) for ${fieldPath}: ${message}`,
9112
+ 1,
9113
+ "SECRET_LOOKUP_FAILED"
9114
+ );
9115
+ }
9116
+ if (!res.ok) {
9117
+ throw new CLIError(
9118
+ `failed to resolve env(${secretName}) for ${fieldPath}: HTTP ${res.status}`,
9119
+ 1,
9120
+ "SECRET_LOOKUP_FAILED"
9121
+ );
9122
+ }
9123
+ const body = await res.json();
9124
+ if (typeof body.value !== "string" || body.value.length === 0) {
9125
+ throw new CLIError(
9126
+ `env(${secretName}) resolved to an empty value (secret may be inactive).
9127
+ fix: insforge secrets update ${secretName} --active true`,
9128
+ 1,
9129
+ "SECRET_EMPTY"
9130
+ );
9131
+ }
9132
+ return body.value;
9133
+ }
9134
+
9063
9135
  // src/lib/config-schema.ts
9064
9136
  var ConfigValidationError = class extends Error {
9065
9137
  constructor(path6, message) {
@@ -9081,6 +9153,25 @@ function validateConfig(input) {
9081
9153
  out.project_id = obj.project_id;
9082
9154
  }
9083
9155
  if ("auth" in obj) out.auth = validateAuth(obj.auth);
9156
+ if ("deployments" in obj) out.deployments = validateDeployments(obj.deployments);
9157
+ return out;
9158
+ }
9159
+ function validateDeployments(input) {
9160
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
9161
+ throw new ConfigValidationError("deployments", "must be an object");
9162
+ }
9163
+ const obj = input;
9164
+ const out = {};
9165
+ if ("subdomain" in obj) {
9166
+ const v = obj.subdomain;
9167
+ if (v !== null && typeof v !== "string") {
9168
+ throw new ConfigValidationError(
9169
+ "deployments.subdomain",
9170
+ "must be a string or null"
9171
+ );
9172
+ }
9173
+ out.subdomain = v;
9174
+ }
9084
9175
  return out;
9085
9176
  }
9086
9177
  function validateAuth(input) {
@@ -9099,6 +9190,128 @@ function validateAuth(input) {
9099
9190
  }
9100
9191
  out.allowed_redirect_urls = v;
9101
9192
  }
9193
+ if ("require_email_verification" in obj) {
9194
+ if (typeof obj.require_email_verification !== "boolean") {
9195
+ throw new ConfigValidationError(
9196
+ "auth.require_email_verification",
9197
+ "must be a boolean"
9198
+ );
9199
+ }
9200
+ out.require_email_verification = obj.require_email_verification;
9201
+ }
9202
+ if ("verify_email_method" in obj) {
9203
+ out.verify_email_method = validateVerificationMethod(
9204
+ "auth.verify_email_method",
9205
+ obj.verify_email_method
9206
+ );
9207
+ }
9208
+ if ("reset_password_method" in obj) {
9209
+ out.reset_password_method = validateVerificationMethod(
9210
+ "auth.reset_password_method",
9211
+ obj.reset_password_method
9212
+ );
9213
+ }
9214
+ if ("password" in obj) out.password = validatePassword(obj.password);
9215
+ if ("smtp" in obj) out.smtp = validateSmtp(obj.smtp);
9216
+ return out;
9217
+ }
9218
+ function validateVerificationMethod(path6, value) {
9219
+ if (value !== "code" && value !== "link") {
9220
+ throw new ConfigValidationError(path6, 'must be "code" or "link"');
9221
+ }
9222
+ return value;
9223
+ }
9224
+ function validatePassword(input) {
9225
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
9226
+ throw new ConfigValidationError("auth.password", "must be a table");
9227
+ }
9228
+ const obj = input;
9229
+ const out = {};
9230
+ if ("min_length" in obj) {
9231
+ if (typeof obj.min_length !== "number" || !Number.isInteger(obj.min_length) || obj.min_length < 4 || obj.min_length > 128) {
9232
+ throw new ConfigValidationError(
9233
+ "auth.password.min_length",
9234
+ "must be an integer between 4 and 128"
9235
+ );
9236
+ }
9237
+ out.min_length = obj.min_length;
9238
+ }
9239
+ for (const key of [
9240
+ "require_number",
9241
+ "require_lowercase",
9242
+ "require_uppercase",
9243
+ "require_special_char"
9244
+ ]) {
9245
+ if (key in obj) {
9246
+ if (typeof obj[key] !== "boolean") {
9247
+ throw new ConfigValidationError(`auth.password.${key}`, "must be a boolean");
9248
+ }
9249
+ out[key] = obj[key];
9250
+ }
9251
+ }
9252
+ return out;
9253
+ }
9254
+ function validateSmtp(input) {
9255
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
9256
+ throw new ConfigValidationError("auth.smtp", "must be a table");
9257
+ }
9258
+ const obj = input;
9259
+ const out = {};
9260
+ if ("enabled" in obj) {
9261
+ if (typeof obj.enabled !== "boolean") {
9262
+ throw new ConfigValidationError("auth.smtp.enabled", "must be a boolean");
9263
+ }
9264
+ out.enabled = obj.enabled;
9265
+ }
9266
+ if ("host" in obj) {
9267
+ if (typeof obj.host !== "string") {
9268
+ throw new ConfigValidationError("auth.smtp.host", "must be a string");
9269
+ }
9270
+ out.host = obj.host;
9271
+ }
9272
+ if ("port" in obj) {
9273
+ if (typeof obj.port !== "number" || !Number.isInteger(obj.port) || obj.port < 1 || obj.port > 65535) {
9274
+ throw new ConfigValidationError(
9275
+ "auth.smtp.port",
9276
+ "must be an integer between 1 and 65535"
9277
+ );
9278
+ }
9279
+ out.port = obj.port;
9280
+ }
9281
+ if ("username" in obj) {
9282
+ if (typeof obj.username !== "string") {
9283
+ throw new ConfigValidationError("auth.smtp.username", "must be a string");
9284
+ }
9285
+ out.username = obj.username;
9286
+ }
9287
+ if ("password" in obj) {
9288
+ out.password = validateSensitiveString(
9289
+ "auth.smtp.password",
9290
+ obj.password,
9291
+ "SMTP_PASSWORD"
9292
+ );
9293
+ }
9294
+ if ("sender_email" in obj) {
9295
+ if (typeof obj.sender_email !== "string") {
9296
+ throw new ConfigValidationError("auth.smtp.sender_email", "must be a string");
9297
+ }
9298
+ out.sender_email = obj.sender_email;
9299
+ }
9300
+ if ("sender_name" in obj) {
9301
+ if (typeof obj.sender_name !== "string") {
9302
+ throw new ConfigValidationError("auth.smtp.sender_name", "must be a string");
9303
+ }
9304
+ out.sender_name = obj.sender_name;
9305
+ }
9306
+ if ("min_interval_seconds" in obj) {
9307
+ if (typeof obj.min_interval_seconds !== "number" || !Number.isInteger(obj.min_interval_seconds) || obj.min_interval_seconds < 0) {
9308
+ throw new ConfigValidationError(
9309
+ "auth.smtp.min_interval_seconds",
9310
+ "must be a non-negative integer"
9311
+ );
9312
+ }
9313
+ out.min_interval_seconds = obj.min_interval_seconds;
9314
+ }
9102
9315
  return out;
9103
9316
  }
9104
9317
 
@@ -9120,14 +9333,204 @@ function stringifyConfigToml(config) {
9120
9333
  }
9121
9334
  if (config.auth) {
9122
9335
  lines.push("[auth]");
9123
- if (config.auth.allowed_redirect_urls !== void 0) {
9124
- const urls = config.auth.allowed_redirect_urls.map((u) => JSON.stringify(u)).join(", ");
9125
- lines.push(`allowed_redirect_urls = [${urls}]`);
9126
- }
9336
+ renderAuthFlatFields(config.auth, lines);
9127
9337
  lines.push("");
9338
+ if (config.auth.password !== void 0) {
9339
+ lines.push("[auth.password]");
9340
+ renderPasswordFields(config.auth.password, lines);
9341
+ lines.push("");
9342
+ }
9343
+ if (config.auth.smtp !== void 0) {
9344
+ lines.push("[auth.smtp]");
9345
+ renderSmtpFields(config.auth.smtp, lines);
9346
+ lines.push("");
9347
+ }
9348
+ }
9349
+ if (config.deployments) {
9350
+ if (typeof config.deployments.subdomain === "string" && config.deployments.subdomain !== "") {
9351
+ lines.push("[deployments]");
9352
+ lines.push(`subdomain = ${JSON.stringify(config.deployments.subdomain)}`);
9353
+ lines.push("");
9354
+ }
9128
9355
  }
9129
9356
  return lines.join("\n").replace(/\n+$/, "\n");
9130
9357
  }
9358
+ function renderAuthFlatFields(auth, lines) {
9359
+ if (auth.allowed_redirect_urls !== void 0) {
9360
+ const urls = auth.allowed_redirect_urls.map((u) => JSON.stringify(u)).join(", ");
9361
+ lines.push(`allowed_redirect_urls = [${urls}]`);
9362
+ }
9363
+ if (auth.require_email_verification !== void 0) {
9364
+ lines.push(`require_email_verification = ${auth.require_email_verification}`);
9365
+ }
9366
+ if (auth.verify_email_method !== void 0) {
9367
+ lines.push(`verify_email_method = ${JSON.stringify(auth.verify_email_method)}`);
9368
+ }
9369
+ if (auth.reset_password_method !== void 0) {
9370
+ lines.push(`reset_password_method = ${JSON.stringify(auth.reset_password_method)}`);
9371
+ }
9372
+ }
9373
+ function renderPasswordFields(pw, lines) {
9374
+ if (pw.min_length !== void 0) lines.push(`min_length = ${pw.min_length}`);
9375
+ if (pw.require_number !== void 0) lines.push(`require_number = ${pw.require_number}`);
9376
+ if (pw.require_lowercase !== void 0) {
9377
+ lines.push(`require_lowercase = ${pw.require_lowercase}`);
9378
+ }
9379
+ if (pw.require_uppercase !== void 0) {
9380
+ lines.push(`require_uppercase = ${pw.require_uppercase}`);
9381
+ }
9382
+ if (pw.require_special_char !== void 0) {
9383
+ lines.push(`require_special_char = ${pw.require_special_char}`);
9384
+ }
9385
+ }
9386
+ function renderSmtpFields(smtp, lines) {
9387
+ if (smtp.enabled !== void 0) lines.push(`enabled = ${smtp.enabled}`);
9388
+ if (smtp.host !== void 0) lines.push(`host = ${JSON.stringify(smtp.host)}`);
9389
+ if (smtp.port !== void 0) lines.push(`port = ${smtp.port}`);
9390
+ if (smtp.username !== void 0) lines.push(`username = ${JSON.stringify(smtp.username)}`);
9391
+ if (smtp.password !== void 0) {
9392
+ const secretName = parseEnvRef(smtp.password) ?? "SMTP_PASSWORD";
9393
+ lines.push(
9394
+ `# password is managed via secrets \u2014 run \`insforge secrets add ${secretName} "<value>"\``
9395
+ );
9396
+ lines.push(`password = ${JSON.stringify(smtp.password)}`);
9397
+ }
9398
+ if (smtp.sender_email !== void 0) {
9399
+ lines.push(`sender_email = ${JSON.stringify(smtp.sender_email)}`);
9400
+ }
9401
+ if (smtp.sender_name !== void 0) {
9402
+ lines.push(`sender_name = ${JSON.stringify(smtp.sender_name)}`);
9403
+ }
9404
+ if (smtp.min_interval_seconds !== void 0) {
9405
+ lines.push(`min_interval_seconds = ${smtp.min_interval_seconds}`);
9406
+ }
9407
+ }
9408
+
9409
+ // src/lib/config-metadata.ts
9410
+ function liveFromMetadata(raw) {
9411
+ const live = { auth: {} };
9412
+ const a = isPlainObject(raw.auth) ? raw.auth : void 0;
9413
+ if (a && "allowedRedirectUrls" in a) {
9414
+ live.auth.allowed_redirect_urls = asStringArray(a.allowedRedirectUrls) ?? [];
9415
+ }
9416
+ if (a && "requireEmailVerification" in a) {
9417
+ live.auth.require_email_verification = a.requireEmailVerification ?? false;
9418
+ }
9419
+ if (a && "verifyEmailMethod" in a && (a.verifyEmailMethod === "code" || a.verifyEmailMethod === "link")) {
9420
+ live.auth.verify_email_method = a.verifyEmailMethod;
9421
+ }
9422
+ if (a && "resetPasswordMethod" in a && (a.resetPasswordMethod === "code" || a.resetPasswordMethod === "link")) {
9423
+ live.auth.reset_password_method = a.resetPasswordMethod;
9424
+ }
9425
+ if (a && ("passwordMinLength" in a || "requireNumber" in a || "requireLowercase" in a || "requireUppercase" in a || "requireSpecialChar" in a)) {
9426
+ live.auth.password = {
9427
+ min_length: a.passwordMinLength ?? 8,
9428
+ require_number: a.requireNumber ?? false,
9429
+ require_lowercase: a.requireLowercase ?? false,
9430
+ require_uppercase: a.requireUppercase ?? false,
9431
+ require_special_char: a.requireSpecialChar ?? false
9432
+ };
9433
+ }
9434
+ if (isPlainObject(a?.smtpConfig)) {
9435
+ const s = a.smtpConfig;
9436
+ live.auth.smtp = {
9437
+ enabled: s.enabled ?? false,
9438
+ host: s.host ?? "",
9439
+ port: s.port ?? 587,
9440
+ username: s.username ?? "",
9441
+ hasPassword: s.hasPassword ?? false,
9442
+ sender_email: s.senderEmail ?? "",
9443
+ sender_name: s.senderName ?? "",
9444
+ min_interval_seconds: s.minIntervalSeconds ?? 60
9445
+ };
9446
+ }
9447
+ const d = isPlainObject(raw.deployments) ? raw.deployments : void 0;
9448
+ if (d) {
9449
+ live.deployments = {
9450
+ subdomain: typeof d.customSlug === "string" && d.customSlug ? d.customSlug : null
9451
+ };
9452
+ }
9453
+ return live;
9454
+ }
9455
+ function isPlainObject(v) {
9456
+ return v !== null && typeof v === "object" && !Array.isArray(v);
9457
+ }
9458
+ function asStringArray(v) {
9459
+ return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : null;
9460
+ }
9461
+ function configFromMetadata(raw) {
9462
+ const config = {};
9463
+ const skipped = [];
9464
+ const a = isPlainObject(raw.auth) ? raw.auth : void 0;
9465
+ if (a && "allowedRedirectUrls" in a) {
9466
+ config.auth = config.auth ?? {};
9467
+ config.auth.allowed_redirect_urls = asStringArray(a.allowedRedirectUrls) ?? [];
9468
+ } else {
9469
+ skipped.push("auth.allowed_redirect_urls");
9470
+ }
9471
+ if (a && "requireEmailVerification" in a) {
9472
+ config.auth = config.auth ?? {};
9473
+ config.auth.require_email_verification = a.requireEmailVerification ?? false;
9474
+ } else {
9475
+ skipped.push("auth.require_email_verification");
9476
+ }
9477
+ if (a && "verifyEmailMethod" in a && (a.verifyEmailMethod === "code" || a.verifyEmailMethod === "link")) {
9478
+ config.auth = config.auth ?? {};
9479
+ config.auth.verify_email_method = a.verifyEmailMethod;
9480
+ } else {
9481
+ skipped.push("auth.verify_email_method");
9482
+ }
9483
+ if (a && "resetPasswordMethod" in a && (a.resetPasswordMethod === "code" || a.resetPasswordMethod === "link")) {
9484
+ config.auth = config.auth ?? {};
9485
+ config.auth.reset_password_method = a.resetPasswordMethod;
9486
+ } else {
9487
+ skipped.push("auth.reset_password_method");
9488
+ }
9489
+ if (a && ("passwordMinLength" in a || "requireNumber" in a || "requireLowercase" in a || "requireUppercase" in a || "requireSpecialChar" in a)) {
9490
+ config.auth = config.auth ?? {};
9491
+ config.auth.password = {};
9492
+ if ("passwordMinLength" in a) config.auth.password.min_length = a.passwordMinLength ?? 8;
9493
+ if ("requireNumber" in a) config.auth.password.require_number = a.requireNumber ?? false;
9494
+ if ("requireLowercase" in a) config.auth.password.require_lowercase = a.requireLowercase ?? false;
9495
+ if ("requireUppercase" in a) config.auth.password.require_uppercase = a.requireUppercase ?? false;
9496
+ if ("requireSpecialChar" in a) {
9497
+ config.auth.password.require_special_char = a.requireSpecialChar ?? false;
9498
+ }
9499
+ } else {
9500
+ skipped.push("auth.password");
9501
+ }
9502
+ if (a && "smtpConfig" in a) {
9503
+ const s = a.smtpConfig;
9504
+ if (isPlainObject(s)) {
9505
+ config.auth = config.auth ?? {};
9506
+ config.auth.smtp = {
9507
+ enabled: s.enabled ?? false,
9508
+ host: s.host ?? "",
9509
+ port: s.port ?? 587,
9510
+ username: s.username ?? "",
9511
+ // When backend has a password set, emit a deterministic env() placeholder
9512
+ // so the user knows which secret to define. We do NOT round-trip the
9513
+ // value (it never leaves the backend). Re-applying this TOML force-resends
9514
+ // from the secrets store — see config-diff.ts for the force-resend rationale.
9515
+ ...s.hasPassword ? { password: "env(SMTP_PASSWORD)" } : {},
9516
+ sender_email: s.senderEmail ?? "",
9517
+ sender_name: s.senderName ?? "",
9518
+ min_interval_seconds: s.minIntervalSeconds ?? 60
9519
+ };
9520
+ }
9521
+ } else {
9522
+ skipped.push("auth.smtp");
9523
+ }
9524
+ const d = isPlainObject(raw.deployments) ? raw.deployments : void 0;
9525
+ if (d) {
9526
+ if (typeof d.customSlug === "string" && d.customSlug) {
9527
+ config.deployments = { subdomain: d.customSlug };
9528
+ }
9529
+ } else {
9530
+ skipped.push("deployments.subdomain");
9531
+ }
9532
+ return { config, skipped };
9533
+ }
9131
9534
 
9132
9535
  // src/commands/config/export.ts
9133
9536
  function registerConfigExportCommand(cfg) {
@@ -9155,16 +9558,7 @@ function registerConfigExportCommand(cfg) {
9155
9558
  }
9156
9559
  const res = await ossFetch("/api/metadata");
9157
9560
  const raw = await res.json();
9158
- const config = {};
9159
- const skipped = [];
9160
- const authSlice = raw?.auth;
9161
- if (authSlice && typeof authSlice === "object" && "allowedRedirectUrls" in authSlice) {
9162
- config.auth = {
9163
- allowed_redirect_urls: authSlice.allowedRedirectUrls ?? []
9164
- };
9165
- } else {
9166
- skipped.push("auth.allowed_redirect_urls");
9167
- }
9561
+ const { config, skipped } = configFromMetadata(raw);
9168
9562
  const toml = stringifyConfigToml(config);
9169
9563
  writeFileSync9(target, toml, "utf8");
9170
9564
  if (json) {
@@ -9210,8 +9604,165 @@ function diffConfig({ live, file }) {
9210
9604
  });
9211
9605
  }
9212
9606
  }
9607
+ if (fileAuth && "require_email_verification" in fileAuth) {
9608
+ const fromV = liveAuth.require_email_verification ?? false;
9609
+ const toV = fileAuth.require_email_verification ?? false;
9610
+ if (fromV !== toV) {
9611
+ changes.push({
9612
+ section: "auth",
9613
+ op: "modify",
9614
+ key: "require_email_verification",
9615
+ from: fromV,
9616
+ to: toV
9617
+ });
9618
+ }
9619
+ }
9620
+ if (fileAuth && "verify_email_method" in fileAuth && fileAuth.verify_email_method) {
9621
+ const fromV = liveAuth.verify_email_method ?? "code";
9622
+ const toV = fileAuth.verify_email_method;
9623
+ if (fromV !== toV) {
9624
+ changes.push({
9625
+ section: "auth",
9626
+ op: "modify",
9627
+ key: "verify_email_method",
9628
+ from: fromV,
9629
+ to: toV
9630
+ });
9631
+ }
9632
+ }
9633
+ if (fileAuth && "reset_password_method" in fileAuth && fileAuth.reset_password_method) {
9634
+ const fromV = liveAuth.reset_password_method ?? "code";
9635
+ const toV = fileAuth.reset_password_method;
9636
+ if (fromV !== toV) {
9637
+ changes.push({
9638
+ section: "auth",
9639
+ op: "modify",
9640
+ key: "reset_password_method",
9641
+ from: fromV,
9642
+ to: toV
9643
+ });
9644
+ }
9645
+ }
9646
+ if (fileAuth?.password) {
9647
+ diffPassword(liveAuth.password, fileAuth.password, changes);
9648
+ }
9649
+ if (fileAuth?.smtp !== void 0) {
9650
+ const smtpChange = diffSmtp(liveAuth.smtp, fileAuth.smtp);
9651
+ if (smtpChange) changes.push(smtpChange);
9652
+ }
9653
+ const fileDeployments = file.deployments;
9654
+ const liveDeployments = live.deployments ?? {};
9655
+ if (fileDeployments && "subdomain" in fileDeployments) {
9656
+ const fromV = liveDeployments.subdomain ?? null;
9657
+ const rawTo = fileDeployments.subdomain;
9658
+ const toV = rawTo === null || rawTo === void 0 || rawTo === "" ? null : rawTo;
9659
+ if (fromV !== toV) {
9660
+ changes.push({
9661
+ section: "deployments",
9662
+ op: "modify",
9663
+ key: "subdomain",
9664
+ from: fromV,
9665
+ to: toV
9666
+ });
9667
+ }
9668
+ }
9213
9669
  return { changes, summary: summarize(changes) };
9214
9670
  }
9671
+ function diffPassword(live, file, changes) {
9672
+ const liveView = live ?? EMPTY_PASSWORD_POLICY;
9673
+ if (file.min_length !== void 0 && liveView.min_length !== file.min_length) {
9674
+ changes.push({
9675
+ section: "auth.password",
9676
+ op: "modify",
9677
+ key: "min_length",
9678
+ from: liveView.min_length,
9679
+ to: file.min_length
9680
+ });
9681
+ }
9682
+ for (const key of [
9683
+ "require_number",
9684
+ "require_lowercase",
9685
+ "require_uppercase",
9686
+ "require_special_char"
9687
+ ]) {
9688
+ const fromV = liveView[key];
9689
+ const toV = file[key];
9690
+ if (toV !== void 0 && fromV !== toV) {
9691
+ changes.push({
9692
+ section: "auth.password",
9693
+ op: "modify",
9694
+ key,
9695
+ from: fromV,
9696
+ to: toV
9697
+ });
9698
+ }
9699
+ }
9700
+ }
9701
+ function diffSmtp(live, fileSmtp) {
9702
+ const livedView = renderLiveSmtp(live);
9703
+ const tomlView = renderFileSmtp(fileSmtp);
9704
+ const envRef = fileSmtp.password ? parseEnvRef(fileSmtp.password) : null;
9705
+ const nonPasswordFieldsChanged = livedView.enabled !== tomlView.enabled || livedView.host !== tomlView.host || livedView.port !== tomlView.port || livedView.username !== tomlView.username || livedView.sender_email !== tomlView.sender_email || livedView.sender_name !== tomlView.sender_name || livedView.min_interval_seconds !== tomlView.min_interval_seconds;
9706
+ if (!nonPasswordFieldsChanged && envRef === null) {
9707
+ return null;
9708
+ }
9709
+ return {
9710
+ section: "auth.smtp",
9711
+ op: "modify",
9712
+ key: "config",
9713
+ from: livedView,
9714
+ to: tomlView,
9715
+ passwordEnvRef: envRef ?? void 0
9716
+ };
9717
+ }
9718
+ function renderLiveSmtp(live) {
9719
+ const empty = EMPTY_SMTP_VIEW;
9720
+ if (!live) return empty;
9721
+ return {
9722
+ enabled: live.enabled,
9723
+ host: live.host,
9724
+ port: live.port,
9725
+ username: live.username,
9726
+ password: live.hasPassword ? "(set)" : "(unset)",
9727
+ sender_email: live.sender_email,
9728
+ sender_name: live.sender_name,
9729
+ min_interval_seconds: live.min_interval_seconds
9730
+ };
9731
+ }
9732
+ function renderFileSmtp(file) {
9733
+ return {
9734
+ enabled: file.enabled ?? false,
9735
+ host: file.host ?? "",
9736
+ port: file.port ?? 587,
9737
+ username: file.username ?? "",
9738
+ password: renderFilePassword(file.password),
9739
+ sender_email: file.sender_email ?? "",
9740
+ sender_name: file.sender_name ?? "",
9741
+ min_interval_seconds: file.min_interval_seconds ?? 60
9742
+ };
9743
+ }
9744
+ function renderFilePassword(value) {
9745
+ if (value === void 0) return "(unchanged)";
9746
+ const ref = parseEnvRef(value);
9747
+ return ref ? `env(${ref})` : "(invalid)";
9748
+ }
9749
+ var EMPTY_SMTP_VIEW = {
9750
+ enabled: false,
9751
+ host: "",
9752
+ port: 587,
9753
+ username: "",
9754
+ password: "(unset)",
9755
+ sender_email: "",
9756
+ sender_name: "",
9757
+ min_interval_seconds: 60
9758
+ };
9759
+ var EMPTY_PASSWORD_POLICY = {
9760
+ min_length: 8,
9761
+ require_number: false,
9762
+ require_lowercase: false,
9763
+ require_uppercase: false,
9764
+ require_special_char: false
9765
+ };
9215
9766
  function summarize(changes) {
9216
9767
  const s = { add: 0, modify: 0, remove: 0, kept: 0 };
9217
9768
  for (const c of changes) {
@@ -9253,19 +9804,82 @@ function formatPlan(result) {
9253
9804
  return lines.join("\n");
9254
9805
  }
9255
9806
  function formatChange(c) {
9807
+ if (c.section === "auth.smtp") {
9808
+ const lines = [`~ smtp config:`];
9809
+ const from = c.from;
9810
+ const to = c.to;
9811
+ for (const key of [
9812
+ "enabled",
9813
+ "host",
9814
+ "port",
9815
+ "username",
9816
+ "password",
9817
+ "sender_email",
9818
+ "sender_name",
9819
+ "min_interval_seconds"
9820
+ ]) {
9821
+ if (from[key] !== to[key]) {
9822
+ lines.push(` ${key}: ${JSON.stringify(from[key])} \u2192 ${JSON.stringify(to[key])}`);
9823
+ }
9824
+ }
9825
+ if (c.passwordEnvRef) {
9826
+ lines.push(` (password force-resent from env(${c.passwordEnvRef}))`);
9827
+ }
9828
+ return lines.join("\n ");
9829
+ }
9830
+ if (c.section === "deployments" && c.key === "subdomain") {
9831
+ const fromLabel = c.from === null ? "(unset)" : JSON.stringify(c.from);
9832
+ const toLabel = c.to === null ? "(unset)" : JSON.stringify(c.to);
9833
+ return `~ ${c.key}: ${fromLabel} \u2192 ${toLabel}`;
9834
+ }
9256
9835
  return `~ ${c.key}: ${JSON.stringify(c.from)} \u2192 ${JSON.stringify(c.to)}`;
9257
9836
  }
9258
9837
 
9259
9838
  // src/lib/config-capabilities.ts
9260
9839
  function metadataSupports(raw, change) {
9261
9840
  if (change.section === "auth" && change.key === "allowed_redirect_urls") {
9262
- return raw?.auth !== void 0 && raw.auth !== null && typeof raw.auth === "object" && "allowedRedirectUrls" in raw.auth;
9841
+ return hasAuthKey(raw, "allowedRedirectUrls");
9842
+ }
9843
+ if (change.section === "auth" && change.key === "require_email_verification") {
9844
+ return hasAuthKey(raw, "requireEmailVerification");
9845
+ }
9846
+ if (change.section === "auth" && change.key === "verify_email_method") {
9847
+ return hasAuthKey(raw, "verifyEmailMethod");
9848
+ }
9849
+ if (change.section === "auth" && change.key === "reset_password_method") {
9850
+ return hasAuthKey(raw, "resetPasswordMethod");
9851
+ }
9852
+ if (change.section === "auth.password") {
9853
+ return hasAuthKey(raw, AUTH_PASSWORD_WIRE_KEY[change.key]);
9263
9854
  }
9855
+ if (change.section === "auth.smtp") {
9856
+ return hasAuthKey(raw, "smtpConfig");
9857
+ }
9858
+ if (change.section === "deployments" && change.key === "subdomain") {
9859
+ return raw?.deployments !== void 0 && raw.deployments !== null && typeof raw.deployments === "object";
9860
+ }
9861
+ const _exhaustive = change;
9862
+ void _exhaustive;
9264
9863
  return false;
9265
9864
  }
9865
+ function hasAuthKey(raw, key) {
9866
+ const auth = raw?.auth;
9867
+ return auth !== void 0 && auth !== null && typeof auth === "object" && key in auth;
9868
+ }
9869
+ var AUTH_PASSWORD_WIRE_KEY = {
9870
+ min_length: "passwordMinLength",
9871
+ require_number: "requireNumber",
9872
+ require_lowercase: "requireLowercase",
9873
+ require_uppercase: "requireUppercase",
9874
+ require_special_char: "requireSpecialChar"
9875
+ };
9266
9876
  function changePath(change) {
9877
+ if (change.section === "auth.smtp") return "auth.smtp";
9267
9878
  return `${change.section}.${change.key}`;
9268
9879
  }
9880
+ function authPasswordWireKey(key) {
9881
+ return AUTH_PASSWORD_WIRE_KEY[key];
9882
+ }
9269
9883
 
9270
9884
  // src/commands/config/plan.ts
9271
9885
  function registerConfigPlanCommand(cfg) {
@@ -9278,9 +9892,7 @@ function registerConfigPlanCommand(cfg) {
9278
9892
  const file = parseConfigToml(tomlSource);
9279
9893
  const res = await ossFetch("/api/metadata");
9280
9894
  const raw = await res.json();
9281
- const live = {
9282
- auth: { allowed_redirect_urls: raw.auth?.allowedRedirectUrls ?? [] }
9283
- };
9895
+ const live = liveFromMetadata(raw);
9284
9896
  const result = diffConfig({ live, file });
9285
9897
  const skipped = result.changes.filter((c) => !metadataSupports(raw, c)).map((c) => changePath(c));
9286
9898
  if (json) {
@@ -9318,9 +9930,7 @@ function registerConfigApplyCommand(cfg) {
9318
9930
  const file = parseConfigToml(tomlSource);
9319
9931
  const res = await ossFetch("/api/metadata");
9320
9932
  const raw = await res.json();
9321
- const live = {
9322
- auth: { allowed_redirect_urls: raw.auth?.allowedRedirectUrls ?? [] }
9323
- };
9933
+ const live = liveFromMetadata(raw);
9324
9934
  const result = diffConfig({ live, file });
9325
9935
  const approved = opts.autoApprove || yes;
9326
9936
  if (!json) {
@@ -9400,7 +10010,68 @@ async function applyChange(change) {
9400
10010
  });
9401
10011
  return;
9402
10012
  }
9403
- throw new Error(`Unsupported change type: ${change.section}.${change.key}`);
10013
+ if (change.section === "auth" && change.key === "require_email_verification") {
10014
+ await ossFetch("/api/auth/config", {
10015
+ method: "PUT",
10016
+ body: JSON.stringify({ requireEmailVerification: change.to })
10017
+ });
10018
+ return;
10019
+ }
10020
+ if (change.section === "auth" && change.key === "verify_email_method") {
10021
+ await ossFetch("/api/auth/config", {
10022
+ method: "PUT",
10023
+ body: JSON.stringify({ verifyEmailMethod: change.to })
10024
+ });
10025
+ return;
10026
+ }
10027
+ if (change.section === "auth" && change.key === "reset_password_method") {
10028
+ await ossFetch("/api/auth/config", {
10029
+ method: "PUT",
10030
+ body: JSON.stringify({ resetPasswordMethod: change.to })
10031
+ });
10032
+ return;
10033
+ }
10034
+ if (change.section === "auth.password") {
10035
+ const wireKey = authPasswordWireKey(change.key);
10036
+ await ossFetch("/api/auth/config", {
10037
+ method: "PUT",
10038
+ body: JSON.stringify({ [wireKey]: change.to })
10039
+ });
10040
+ return;
10041
+ }
10042
+ if (change.section === "auth.smtp") {
10043
+ const to = change.to;
10044
+ const body = {
10045
+ enabled: to.enabled,
10046
+ host: to.host,
10047
+ port: to.port,
10048
+ username: to.username,
10049
+ senderEmail: to.sender_email,
10050
+ senderName: to.sender_name,
10051
+ minIntervalSeconds: to.min_interval_seconds
10052
+ };
10053
+ if (change.passwordEnvRef) {
10054
+ const value = await resolveEnvRef(
10055
+ `env(${change.passwordEnvRef})`,
10056
+ "auth.smtp.password"
10057
+ );
10058
+ body.password = value;
10059
+ }
10060
+ await ossFetch("/api/auth/smtp-config", {
10061
+ method: "PUT",
10062
+ body: JSON.stringify(body)
10063
+ });
10064
+ return;
10065
+ }
10066
+ if (change.section === "deployments" && change.key === "subdomain") {
10067
+ await ossFetch("/api/deployments/slug", {
10068
+ method: "PUT",
10069
+ body: JSON.stringify({ slug: change.to })
10070
+ });
10071
+ return;
10072
+ }
10073
+ const _exhaustive = change;
10074
+ throw new Error(`Unsupported change: ${JSON.stringify(_exhaustive)}`);
9404
10075
  }
9405
10076
 
9406
10077
  // src/commands/config/index.ts
@@ -9411,9 +10082,161 @@ function registerConfigCommand(program2) {
9411
10082
  registerConfigApplyCommand(cfg);
9412
10083
  }
9413
10084
 
10085
+ // 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";
10088
+ import * as clack17 from "@clack/prompts";
10089
+ import pc7 from "picocolors";
10090
+
10091
+ // src/lib/api/ai.ts
10092
+ async function getOpenRouterApiKey() {
10093
+ const res = await ossFetch("/api/ai/openrouter/api-key");
10094
+ const data = await res.json();
10095
+ const apiKey = typeof data.apiKey === "string" ? data.apiKey.trim() : "";
10096
+ const maskedKey = typeof data.maskedKey === "string" ? data.maskedKey.trim() : void 0;
10097
+ if (apiKey.length === 0) {
10098
+ throw new CLIError(
10099
+ "AI gateway returned no OpenRouter API key. Open the InsForge dashboard AI page and verify Model Gateway is configured."
10100
+ );
10101
+ }
10102
+ return {
10103
+ apiKey,
10104
+ maskedKey
10105
+ };
10106
+ }
10107
+
10108
+ // src/commands/ai/setup.ts
10109
+ var DEFAULT_ENV_FILE = ".env.local";
10110
+ var OPENROUTER_ENV_KEY = "OPENROUTER_API_KEY";
10111
+ function registerAiSetupCommand(aiCmd2) {
10112
+ aiCmd2.command("setup").description("Write the linked project OpenRouter key to a local env file").option("--env-file <path>", `Env file to update (default: ${DEFAULT_ENV_FILE})`).action(async (opts, cmd) => {
10113
+ const { json } = getRootOpts(cmd);
10114
+ try {
10115
+ const result = await runAiSetup({
10116
+ envFile: opts.envFile,
10117
+ json
10118
+ });
10119
+ if (json) {
10120
+ outputJson({ success: true, ...result });
10121
+ }
10122
+ } catch (err) {
10123
+ handleError(err, json);
10124
+ } finally {
10125
+ await shutdownAnalytics();
10126
+ }
10127
+ });
10128
+ }
10129
+ async function runAiSetup(opts) {
10130
+ const project = getProjectConfig();
10131
+ if (!project) {
10132
+ throw new ProjectNotLinkedError();
10133
+ }
10134
+ if (!opts.json) {
10135
+ clack17.intro("AI setup");
10136
+ outputSuccess(`Linked to InsForge project: ${project.project_name} (${project.project_id})`);
10137
+ }
10138
+ const spinner11 = !opts.json && isInteractive ? clack17.spinner() : null;
10139
+ spinner11?.start("Fetching OpenRouter key...");
10140
+ let key;
10141
+ try {
10142
+ key = await getOpenRouterApiKey();
10143
+ spinner11?.stop("Fetched OpenRouter key.");
10144
+ } catch (err) {
10145
+ spinner11?.stop("Could not fetch OpenRouter key.");
10146
+ throw err;
10147
+ }
10148
+ const envFile = opts.envFile ?? DEFAULT_ENV_FILE;
10149
+ const envPath = resolve8(process.cwd(), envFile);
10150
+ const envLabel = displayPath(envPath);
10151
+ const update = upsertEnvFile(envPath, { [OPENROUTER_ENV_KEY]: key.apiKey });
10152
+ const gitignoreUpdated = ensureLocalEnvIgnored(process.cwd(), envFile);
10153
+ captureEvent(project.project_id, "cli_ai_setup", {
10154
+ project_id: project.project_id,
10155
+ project_name: project.project_name,
10156
+ org_id: project.org_id,
10157
+ region: project.region,
10158
+ env_file: envLabel,
10159
+ added: update.added.includes(OPENROUTER_ENV_KEY),
10160
+ skipped: update.skipped.includes(OPENROUTER_ENV_KEY),
10161
+ mismatched: update.mismatched.some((m) => m.key === OPENROUTER_ENV_KEY)
10162
+ });
10163
+ if (!opts.json) {
10164
+ if (update.added.length > 0) {
10165
+ outputSuccess(`Wrote ${envLabel}: ${update.added.join(", ")}`);
10166
+ }
10167
+ if (update.skipped.length > 0) {
10168
+ outputInfo(pc7.dim(`${envLabel}: ${update.skipped.join(", ")} already set (matching) - left as-is.`));
10169
+ }
10170
+ for (const m of update.mismatched) {
10171
+ clack17.log.warn(
10172
+ `${envLabel} already has ${m.key}; left existing value untouched. Remove it or pass --env-file to write elsewhere.`
10173
+ );
10174
+ }
10175
+ if (gitignoreUpdated) {
10176
+ outputInfo(pc7.dim("Added .env*.local to .gitignore."));
10177
+ }
10178
+ if (!isLocalEnvFile(envFile)) {
10179
+ clack17.log.warn(
10180
+ `${envLabel} may be committed unless it is listed in .gitignore. Keep ${OPENROUTER_ENV_KEY} server-only.`
10181
+ );
10182
+ }
10183
+ outputInfo("");
10184
+ outputInfo("Use this key only from server-side code as process.env.OPENROUTER_API_KEY.");
10185
+ outputInfo("For deployment, add OPENROUTER_API_KEY to your hosting provider environment.");
10186
+ outputInfo(`Do not rename it to ${pc7.bold("NEXT_PUBLIC_")}, ${pc7.bold("VITE_")}, or ${pc7.bold("PUBLIC_")}.`);
10187
+ clack17.outro("Done.");
10188
+ }
10189
+ return {
10190
+ envFile: envLabel,
10191
+ added: update.added,
10192
+ skipped: update.skipped,
10193
+ mismatched: update.mismatched.map((m) => m.key),
10194
+ gitignoreUpdated,
10195
+ maskedKey: key.maskedKey
10196
+ };
10197
+ }
10198
+ function displayPath(path6) {
10199
+ const rel = relative4(process.cwd(), path6);
10200
+ if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
10201
+ return path6;
10202
+ }
10203
+ return rel;
10204
+ }
10205
+ function isLocalEnvFile(envFile) {
10206
+ const normalized = envFile.replace(/\\/g, "/");
10207
+ const basename8 = normalized.split("/").pop() ?? normalized;
10208
+ return basename8 === ".env.local" || /^\.env\..+\.local$/.test(basename8);
10209
+ }
10210
+ function ensureLocalEnvIgnored(cwd, envFile) {
10211
+ if (!isLocalEnvFile(envFile)) return false;
10212
+ const envPath = resolve8(cwd, envFile);
10213
+ const relEnvPath = relative4(cwd, envPath);
10214
+ if (!relEnvPath || relEnvPath.startsWith("..") || isAbsolute(relEnvPath)) {
10215
+ return false;
10216
+ }
10217
+ const gitignorePath = join17(cwd, ".gitignore");
10218
+ const existing = existsSync15(gitignorePath) ? readFileSync13(gitignorePath, "utf-8") : "";
10219
+ const lines = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
10220
+ const envBasename = envFile.replace(/\\/g, "/").split("/").pop() ?? envFile;
10221
+ if (lines.has(".env*") || lines.has(".env.*") || lines.has(".env*.local") || lines.has(".env.local") && envBasename === ".env.local") {
10222
+ return false;
10223
+ }
10224
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
10225
+ const spacer = existing.length > 0 ? "\n" : "";
10226
+ appendFileSync2(gitignorePath, `${prefix}${spacer}# Local environment secrets
10227
+ .env*.local
10228
+ `);
10229
+ return true;
10230
+ }
10231
+
10232
+ // src/commands/ai/index.ts
10233
+ function registerAiCommands(aiCmd2) {
10234
+ registerAiSetupCommand(aiCmd2);
10235
+ }
10236
+
9414
10237
  // src/index.ts
9415
10238
  var __dirname = dirname3(fileURLToPath(import.meta.url));
9416
- var pkg = JSON.parse(readFileSync13(join17(__dirname, "../package.json"), "utf-8"));
10239
+ var pkg = JSON.parse(readFileSync14(join18(__dirname, "../package.json"), "utf-8"));
9417
10240
  var INSFORGE_LOGO = `
9418
10241
  \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
9419
10242
  \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
@@ -9497,6 +10320,8 @@ registerComputeStopCommand(computeCmd);
9497
10320
  registerComputeEventsCommand(computeCmd);
9498
10321
  var posthogCmd = program.command("posthog").description("Manage PostHog product analytics integration");
9499
10322
  registerPosthogSetupCommand(posthogCmd);
10323
+ var aiCmd = program.command("ai").description("Manage AI model gateway setup");
10324
+ registerAiCommands(aiCmd);
9500
10325
  var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
9501
10326
  registerSchedulesListCommand(schedulesCmd);
9502
10327
  registerSchedulesGetCommand(schedulesCmd);
@@ -9522,7 +10347,7 @@ async function showInteractiveMenu() {
9522
10347
  } catch {
9523
10348
  }
9524
10349
  console.log(INSFORGE_LOGO);
9525
- clack17.intro(`InsForge CLI v${pkg.version}`);
10350
+ clack18.intro(`InsForge CLI v${pkg.version}`);
9526
10351
  const options = [];
9527
10352
  if (!isLoggedIn) {
9528
10353
  options.push({ value: "login", label: "Log in to InsForge" });
@@ -9543,7 +10368,7 @@ async function showInteractiveMenu() {
9543
10368
  options
9544
10369
  });
9545
10370
  if (isCancel2(action)) {
9546
- clack17.cancel("Bye!");
10371
+ clack18.cancel("Bye!");
9547
10372
  process.exit(0);
9548
10373
  }
9549
10374
  switch (action) {