@insforge/cli 0.1.76 → 0.1.78

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) => {
@@ -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 = "phc_ueV1ii62wdBTkH7E70ugyeqHIHu8dFDdjs0qq3TZhJz";
1175
+ var POSTHOG_API_KEY = "";
1176
1176
  var POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
1177
1177
  var client = null;
1178
1178
  function getClient() {
@@ -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.78";
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,70 @@ function validateAuth(input) {
9099
9190
  }
9100
9191
  out.allowed_redirect_urls = v;
9101
9192
  }
9193
+ if ("smtp" in obj) out.smtp = validateSmtp(obj.smtp);
9194
+ return out;
9195
+ }
9196
+ function validateSmtp(input) {
9197
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
9198
+ throw new ConfigValidationError("auth.smtp", "must be a table");
9199
+ }
9200
+ const obj = input;
9201
+ const out = {};
9202
+ if ("enabled" in obj) {
9203
+ if (typeof obj.enabled !== "boolean") {
9204
+ throw new ConfigValidationError("auth.smtp.enabled", "must be a boolean");
9205
+ }
9206
+ out.enabled = obj.enabled;
9207
+ }
9208
+ if ("host" in obj) {
9209
+ if (typeof obj.host !== "string") {
9210
+ throw new ConfigValidationError("auth.smtp.host", "must be a string");
9211
+ }
9212
+ out.host = obj.host;
9213
+ }
9214
+ if ("port" in obj) {
9215
+ if (typeof obj.port !== "number" || !Number.isInteger(obj.port) || obj.port < 1 || obj.port > 65535) {
9216
+ throw new ConfigValidationError(
9217
+ "auth.smtp.port",
9218
+ "must be an integer between 1 and 65535"
9219
+ );
9220
+ }
9221
+ out.port = obj.port;
9222
+ }
9223
+ if ("username" in obj) {
9224
+ if (typeof obj.username !== "string") {
9225
+ throw new ConfigValidationError("auth.smtp.username", "must be a string");
9226
+ }
9227
+ out.username = obj.username;
9228
+ }
9229
+ if ("password" in obj) {
9230
+ out.password = validateSensitiveString(
9231
+ "auth.smtp.password",
9232
+ obj.password,
9233
+ "SMTP_PASSWORD"
9234
+ );
9235
+ }
9236
+ if ("sender_email" in obj) {
9237
+ if (typeof obj.sender_email !== "string") {
9238
+ throw new ConfigValidationError("auth.smtp.sender_email", "must be a string");
9239
+ }
9240
+ out.sender_email = obj.sender_email;
9241
+ }
9242
+ if ("sender_name" in obj) {
9243
+ if (typeof obj.sender_name !== "string") {
9244
+ throw new ConfigValidationError("auth.smtp.sender_name", "must be a string");
9245
+ }
9246
+ out.sender_name = obj.sender_name;
9247
+ }
9248
+ if ("min_interval_seconds" in obj) {
9249
+ if (typeof obj.min_interval_seconds !== "number" || !Number.isInteger(obj.min_interval_seconds) || obj.min_interval_seconds < 0) {
9250
+ throw new ConfigValidationError(
9251
+ "auth.smtp.min_interval_seconds",
9252
+ "must be a non-negative integer"
9253
+ );
9254
+ }
9255
+ out.min_interval_seconds = obj.min_interval_seconds;
9256
+ }
9102
9257
  return out;
9103
9258
  }
9104
9259
 
@@ -9125,9 +9280,43 @@ function stringifyConfigToml(config) {
9125
9280
  lines.push(`allowed_redirect_urls = [${urls}]`);
9126
9281
  }
9127
9282
  lines.push("");
9283
+ if (config.auth.smtp !== void 0) {
9284
+ lines.push("[auth.smtp]");
9285
+ renderSmtpFields(config.auth.smtp, lines);
9286
+ lines.push("");
9287
+ }
9288
+ }
9289
+ if (config.deployments) {
9290
+ if (typeof config.deployments.subdomain === "string" && config.deployments.subdomain !== "") {
9291
+ lines.push("[deployments]");
9292
+ lines.push(`subdomain = ${JSON.stringify(config.deployments.subdomain)}`);
9293
+ lines.push("");
9294
+ }
9128
9295
  }
9129
9296
  return lines.join("\n").replace(/\n+$/, "\n");
9130
9297
  }
9298
+ function renderSmtpFields(smtp, lines) {
9299
+ if (smtp.enabled !== void 0) lines.push(`enabled = ${smtp.enabled}`);
9300
+ if (smtp.host !== void 0) lines.push(`host = ${JSON.stringify(smtp.host)}`);
9301
+ if (smtp.port !== void 0) lines.push(`port = ${smtp.port}`);
9302
+ if (smtp.username !== void 0) lines.push(`username = ${JSON.stringify(smtp.username)}`);
9303
+ if (smtp.password !== void 0) {
9304
+ const secretName = parseEnvRef(smtp.password) ?? "SMTP_PASSWORD";
9305
+ lines.push(
9306
+ `# password is managed via secrets \u2014 run \`insforge secrets add ${secretName} "<value>"\``
9307
+ );
9308
+ lines.push(`password = ${JSON.stringify(smtp.password)}`);
9309
+ }
9310
+ if (smtp.sender_email !== void 0) {
9311
+ lines.push(`sender_email = ${JSON.stringify(smtp.sender_email)}`);
9312
+ }
9313
+ if (smtp.sender_name !== void 0) {
9314
+ lines.push(`sender_name = ${JSON.stringify(smtp.sender_name)}`);
9315
+ }
9316
+ if (smtp.min_interval_seconds !== void 0) {
9317
+ lines.push(`min_interval_seconds = ${smtp.min_interval_seconds}`);
9318
+ }
9319
+ }
9131
9320
 
9132
9321
  // src/commands/config/export.ts
9133
9322
  function registerConfigExportCommand(cfg) {
@@ -9159,12 +9348,40 @@ function registerConfigExportCommand(cfg) {
9159
9348
  const skipped = [];
9160
9349
  const authSlice = raw?.auth;
9161
9350
  if (authSlice && typeof authSlice === "object" && "allowedRedirectUrls" in authSlice) {
9162
- config.auth = {
9163
- allowed_redirect_urls: authSlice.allowedRedirectUrls ?? []
9164
- };
9351
+ config.auth = config.auth ?? {};
9352
+ config.auth.allowed_redirect_urls = authSlice.allowedRedirectUrls ?? [];
9165
9353
  } else {
9166
9354
  skipped.push("auth.allowed_redirect_urls");
9167
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
+ }
9168
9385
  const toml = stringifyConfigToml(config);
9169
9386
  writeFileSync9(target, toml, "utf8");
9170
9387
  if (json) {
@@ -9210,8 +9427,86 @@ function diffConfig({ live, file }) {
9210
9427
  });
9211
9428
  }
9212
9429
  }
9430
+ if (fileAuth?.smtp !== void 0) {
9431
+ const smtpChange = diffSmtp(liveAuth.smtp, fileAuth.smtp);
9432
+ if (smtpChange) changes.push(smtpChange);
9433
+ }
9434
+ const fileDeployments = file.deployments;
9435
+ const liveDeployments = live.deployments ?? {};
9436
+ if (fileDeployments && "subdomain" in fileDeployments) {
9437
+ const fromV = liveDeployments.subdomain ?? null;
9438
+ const rawTo = fileDeployments.subdomain;
9439
+ const toV = rawTo === null || rawTo === "" ? null : rawTo;
9440
+ if (fromV !== toV) {
9441
+ changes.push({
9442
+ section: "deployments",
9443
+ op: "modify",
9444
+ key: "subdomain",
9445
+ from: fromV,
9446
+ to: toV
9447
+ });
9448
+ }
9449
+ }
9213
9450
  return { changes, summary: summarize(changes) };
9214
9451
  }
9452
+ function diffSmtp(live, fileSmtp) {
9453
+ const livedView = renderLiveSmtp(live);
9454
+ const tomlView = renderFileSmtp(fileSmtp);
9455
+ const envRef = fileSmtp.password ? parseEnvRef(fileSmtp.password) : null;
9456
+ 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;
9457
+ if (!nonPasswordFieldsChanged && envRef === null) {
9458
+ return null;
9459
+ }
9460
+ return {
9461
+ section: "auth.smtp",
9462
+ op: "modify",
9463
+ key: "config",
9464
+ from: livedView,
9465
+ to: tomlView,
9466
+ passwordEnvRef: envRef ?? void 0
9467
+ };
9468
+ }
9469
+ function renderLiveSmtp(live) {
9470
+ const empty = EMPTY_SMTP_VIEW;
9471
+ if (!live) return empty;
9472
+ return {
9473
+ enabled: live.enabled,
9474
+ host: live.host,
9475
+ port: live.port,
9476
+ username: live.username,
9477
+ password: live.hasPassword ? "(set)" : "(unset)",
9478
+ sender_email: live.sender_email,
9479
+ sender_name: live.sender_name,
9480
+ min_interval_seconds: live.min_interval_seconds
9481
+ };
9482
+ }
9483
+ function renderFileSmtp(file) {
9484
+ return {
9485
+ enabled: file.enabled ?? false,
9486
+ host: file.host ?? "",
9487
+ port: file.port ?? 587,
9488
+ username: file.username ?? "",
9489
+ password: renderFilePassword(file.password),
9490
+ sender_email: file.sender_email ?? "",
9491
+ sender_name: file.sender_name ?? "",
9492
+ min_interval_seconds: file.min_interval_seconds ?? 60
9493
+ };
9494
+ }
9495
+ function renderFilePassword(value) {
9496
+ if (value === void 0) return "(unchanged)";
9497
+ const ref = parseEnvRef(value);
9498
+ return ref ? `env(${ref})` : "(invalid)";
9499
+ }
9500
+ var EMPTY_SMTP_VIEW = {
9501
+ enabled: false,
9502
+ host: "",
9503
+ port: 587,
9504
+ username: "",
9505
+ password: "(unset)",
9506
+ sender_email: "",
9507
+ sender_name: "",
9508
+ min_interval_seconds: 60
9509
+ };
9215
9510
  function summarize(changes) {
9216
9511
  const s = { add: 0, modify: 0, remove: 0, kept: 0 };
9217
9512
  for (const c of changes) {
@@ -9253,6 +9548,34 @@ function formatPlan(result) {
9253
9548
  return lines.join("\n");
9254
9549
  }
9255
9550
  function formatChange(c) {
9551
+ if (c.section === "auth.smtp") {
9552
+ const lines = [`~ smtp config:`];
9553
+ const from = c.from;
9554
+ const to = c.to;
9555
+ for (const key of [
9556
+ "enabled",
9557
+ "host",
9558
+ "port",
9559
+ "username",
9560
+ "password",
9561
+ "sender_email",
9562
+ "sender_name",
9563
+ "min_interval_seconds"
9564
+ ]) {
9565
+ if (from[key] !== to[key]) {
9566
+ lines.push(` ${key}: ${JSON.stringify(from[key])} \u2192 ${JSON.stringify(to[key])}`);
9567
+ }
9568
+ }
9569
+ if (c.passwordEnvRef) {
9570
+ lines.push(` (password force-resent from env(${c.passwordEnvRef}))`);
9571
+ }
9572
+ return lines.join("\n ");
9573
+ }
9574
+ if (c.section === "deployments" && c.key === "subdomain") {
9575
+ const fromLabel = c.from === null ? "(unset)" : JSON.stringify(c.from);
9576
+ const toLabel = c.to === null ? "(unset)" : JSON.stringify(c.to);
9577
+ return `~ ${c.key}: ${fromLabel} \u2192 ${toLabel}`;
9578
+ }
9256
9579
  return `~ ${c.key}: ${JSON.stringify(c.from)} \u2192 ${JSON.stringify(c.to)}`;
9257
9580
  }
9258
9581
 
@@ -9261,9 +9584,18 @@ function metadataSupports(raw, change) {
9261
9584
  if (change.section === "auth" && change.key === "allowed_redirect_urls") {
9262
9585
  return raw?.auth !== void 0 && raw.auth !== null && typeof raw.auth === "object" && "allowedRedirectUrls" in raw.auth;
9263
9586
  }
9587
+ if (change.section === "auth.smtp") {
9588
+ return raw?.auth !== void 0 && raw.auth !== null && typeof raw.auth === "object" && "smtpConfig" in raw.auth;
9589
+ }
9590
+ if (change.section === "deployments" && change.key === "subdomain") {
9591
+ return raw?.deployments !== void 0 && raw.deployments !== null && typeof raw.deployments === "object";
9592
+ }
9593
+ const _exhaustive = change;
9594
+ void _exhaustive;
9264
9595
  return false;
9265
9596
  }
9266
9597
  function changePath(change) {
9598
+ if (change.section === "auth.smtp") return "auth.smtp";
9267
9599
  return `${change.section}.${change.key}`;
9268
9600
  }
9269
9601
 
@@ -9318,9 +9650,7 @@ function registerConfigApplyCommand(cfg) {
9318
9650
  const file = parseConfigToml(tomlSource);
9319
9651
  const res = await ossFetch("/api/metadata");
9320
9652
  const raw = await res.json();
9321
- const live = {
9322
- auth: { allowed_redirect_urls: raw.auth?.allowedRedirectUrls ?? [] }
9323
- };
9653
+ const live = liveFromMetadata(raw);
9324
9654
  const result = diffConfig({ live, file });
9325
9655
  const approved = opts.autoApprove || yes;
9326
9656
  if (!json) {
@@ -9392,6 +9722,29 @@ function registerConfigApplyCommand(cfg) {
9392
9722
  }
9393
9723
  });
9394
9724
  }
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
+ }
9395
9748
  async function applyChange(change) {
9396
9749
  if (change.section === "auth" && change.key === "allowed_redirect_urls") {
9397
9750
  await ossFetch("/api/auth/config", {
@@ -9400,7 +9753,39 @@ async function applyChange(change) {
9400
9753
  });
9401
9754
  return;
9402
9755
  }
9403
- throw new Error(`Unsupported change type: ${change.section}.${change.key}`);
9756
+ if (change.section === "auth.smtp") {
9757
+ const to = change.to;
9758
+ const body = {
9759
+ enabled: to.enabled,
9760
+ host: to.host,
9761
+ port: to.port,
9762
+ username: to.username,
9763
+ senderEmail: to.sender_email,
9764
+ senderName: to.sender_name,
9765
+ minIntervalSeconds: to.min_interval_seconds
9766
+ };
9767
+ if (change.passwordEnvRef) {
9768
+ const value = await resolveEnvRef(
9769
+ `env(${change.passwordEnvRef})`,
9770
+ "auth.smtp.password"
9771
+ );
9772
+ body.password = value;
9773
+ }
9774
+ await ossFetch("/api/auth/smtp-config", {
9775
+ method: "PUT",
9776
+ body: JSON.stringify(body)
9777
+ });
9778
+ return;
9779
+ }
9780
+ if (change.section === "deployments" && change.key === "subdomain") {
9781
+ await ossFetch("/api/deployments/slug", {
9782
+ method: "PUT",
9783
+ body: JSON.stringify({ slug: change.to })
9784
+ });
9785
+ return;
9786
+ }
9787
+ const _exhaustive = change;
9788
+ throw new Error(`Unsupported change: ${JSON.stringify(_exhaustive)}`);
9404
9789
  }
9405
9790
 
9406
9791
  // src/commands/config/index.ts
@@ -9411,9 +9796,161 @@ function registerConfigCommand(program2) {
9411
9796
  registerConfigApplyCommand(cfg);
9412
9797
  }
9413
9798
 
9799
+ // 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";
9802
+ import * as clack17 from "@clack/prompts";
9803
+ import pc7 from "picocolors";
9804
+
9805
+ // src/lib/api/ai.ts
9806
+ async function getOpenRouterApiKey() {
9807
+ const res = await ossFetch("/api/ai/openrouter/api-key");
9808
+ const data = await res.json();
9809
+ const apiKey = typeof data.apiKey === "string" ? data.apiKey.trim() : "";
9810
+ const maskedKey = typeof data.maskedKey === "string" ? data.maskedKey.trim() : void 0;
9811
+ if (apiKey.length === 0) {
9812
+ throw new CLIError(
9813
+ "AI gateway returned no OpenRouter API key. Open the InsForge dashboard AI page and verify Model Gateway is configured."
9814
+ );
9815
+ }
9816
+ return {
9817
+ apiKey,
9818
+ maskedKey
9819
+ };
9820
+ }
9821
+
9822
+ // src/commands/ai/setup.ts
9823
+ var DEFAULT_ENV_FILE = ".env.local";
9824
+ var OPENROUTER_ENV_KEY = "OPENROUTER_API_KEY";
9825
+ function registerAiSetupCommand(aiCmd2) {
9826
+ 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) => {
9827
+ const { json } = getRootOpts(cmd);
9828
+ try {
9829
+ const result = await runAiSetup({
9830
+ envFile: opts.envFile,
9831
+ json
9832
+ });
9833
+ if (json) {
9834
+ outputJson({ success: true, ...result });
9835
+ }
9836
+ } catch (err) {
9837
+ handleError(err, json);
9838
+ } finally {
9839
+ await shutdownAnalytics();
9840
+ }
9841
+ });
9842
+ }
9843
+ async function runAiSetup(opts) {
9844
+ const project = getProjectConfig();
9845
+ if (!project) {
9846
+ throw new ProjectNotLinkedError();
9847
+ }
9848
+ if (!opts.json) {
9849
+ clack17.intro("AI setup");
9850
+ outputSuccess(`Linked to InsForge project: ${project.project_name} (${project.project_id})`);
9851
+ }
9852
+ const spinner11 = !opts.json && isInteractive ? clack17.spinner() : null;
9853
+ spinner11?.start("Fetching OpenRouter key...");
9854
+ let key;
9855
+ try {
9856
+ key = await getOpenRouterApiKey();
9857
+ spinner11?.stop("Fetched OpenRouter key.");
9858
+ } catch (err) {
9859
+ spinner11?.stop("Could not fetch OpenRouter key.");
9860
+ throw err;
9861
+ }
9862
+ const envFile = opts.envFile ?? DEFAULT_ENV_FILE;
9863
+ const envPath = resolve8(process.cwd(), envFile);
9864
+ const envLabel = displayPath(envPath);
9865
+ const update = upsertEnvFile(envPath, { [OPENROUTER_ENV_KEY]: key.apiKey });
9866
+ const gitignoreUpdated = ensureLocalEnvIgnored(process.cwd(), envFile);
9867
+ captureEvent(project.project_id, "cli_ai_setup", {
9868
+ project_id: project.project_id,
9869
+ project_name: project.project_name,
9870
+ org_id: project.org_id,
9871
+ region: project.region,
9872
+ env_file: envLabel,
9873
+ added: update.added.includes(OPENROUTER_ENV_KEY),
9874
+ skipped: update.skipped.includes(OPENROUTER_ENV_KEY),
9875
+ mismatched: update.mismatched.some((m) => m.key === OPENROUTER_ENV_KEY)
9876
+ });
9877
+ if (!opts.json) {
9878
+ if (update.added.length > 0) {
9879
+ outputSuccess(`Wrote ${envLabel}: ${update.added.join(", ")}`);
9880
+ }
9881
+ if (update.skipped.length > 0) {
9882
+ outputInfo(pc7.dim(`${envLabel}: ${update.skipped.join(", ")} already set (matching) - left as-is.`));
9883
+ }
9884
+ for (const m of update.mismatched) {
9885
+ clack17.log.warn(
9886
+ `${envLabel} already has ${m.key}; left existing value untouched. Remove it or pass --env-file to write elsewhere.`
9887
+ );
9888
+ }
9889
+ if (gitignoreUpdated) {
9890
+ outputInfo(pc7.dim("Added .env*.local to .gitignore."));
9891
+ }
9892
+ if (!isLocalEnvFile(envFile)) {
9893
+ clack17.log.warn(
9894
+ `${envLabel} may be committed unless it is listed in .gitignore. Keep ${OPENROUTER_ENV_KEY} server-only.`
9895
+ );
9896
+ }
9897
+ outputInfo("");
9898
+ outputInfo("Use this key only from server-side code as process.env.OPENROUTER_API_KEY.");
9899
+ outputInfo("For deployment, add OPENROUTER_API_KEY to your hosting provider environment.");
9900
+ outputInfo(`Do not rename it to ${pc7.bold("NEXT_PUBLIC_")}, ${pc7.bold("VITE_")}, or ${pc7.bold("PUBLIC_")}.`);
9901
+ clack17.outro("Done.");
9902
+ }
9903
+ return {
9904
+ envFile: envLabel,
9905
+ added: update.added,
9906
+ skipped: update.skipped,
9907
+ mismatched: update.mismatched.map((m) => m.key),
9908
+ gitignoreUpdated,
9909
+ maskedKey: key.maskedKey
9910
+ };
9911
+ }
9912
+ function displayPath(path6) {
9913
+ const rel = relative4(process.cwd(), path6);
9914
+ if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
9915
+ return path6;
9916
+ }
9917
+ return rel;
9918
+ }
9919
+ function isLocalEnvFile(envFile) {
9920
+ const normalized = envFile.replace(/\\/g, "/");
9921
+ const basename8 = normalized.split("/").pop() ?? normalized;
9922
+ return basename8 === ".env.local" || /^\.env\..+\.local$/.test(basename8);
9923
+ }
9924
+ function ensureLocalEnvIgnored(cwd, envFile) {
9925
+ if (!isLocalEnvFile(envFile)) return false;
9926
+ const envPath = resolve8(cwd, envFile);
9927
+ const relEnvPath = relative4(cwd, envPath);
9928
+ if (!relEnvPath || relEnvPath.startsWith("..") || isAbsolute(relEnvPath)) {
9929
+ return false;
9930
+ }
9931
+ const gitignorePath = join17(cwd, ".gitignore");
9932
+ const existing = existsSync15(gitignorePath) ? readFileSync13(gitignorePath, "utf-8") : "";
9933
+ const lines = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
9934
+ const envBasename = envFile.replace(/\\/g, "/").split("/").pop() ?? envFile;
9935
+ if (lines.has(".env*") || lines.has(".env.*") || lines.has(".env*.local") || lines.has(".env.local") && envBasename === ".env.local") {
9936
+ return false;
9937
+ }
9938
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
9939
+ const spacer = existing.length > 0 ? "\n" : "";
9940
+ appendFileSync2(gitignorePath, `${prefix}${spacer}# Local environment secrets
9941
+ .env*.local
9942
+ `);
9943
+ return true;
9944
+ }
9945
+
9946
+ // src/commands/ai/index.ts
9947
+ function registerAiCommands(aiCmd2) {
9948
+ registerAiSetupCommand(aiCmd2);
9949
+ }
9950
+
9414
9951
  // src/index.ts
9415
9952
  var __dirname = dirname3(fileURLToPath(import.meta.url));
9416
- var pkg = JSON.parse(readFileSync13(join17(__dirname, "../package.json"), "utf-8"));
9953
+ var pkg = JSON.parse(readFileSync14(join18(__dirname, "../package.json"), "utf-8"));
9417
9954
  var INSFORGE_LOGO = `
9418
9955
  \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
9956
  \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 +10034,8 @@ registerComputeStopCommand(computeCmd);
9497
10034
  registerComputeEventsCommand(computeCmd);
9498
10035
  var posthogCmd = program.command("posthog").description("Manage PostHog product analytics integration");
9499
10036
  registerPosthogSetupCommand(posthogCmd);
10037
+ var aiCmd = program.command("ai").description("Manage AI model gateway setup");
10038
+ registerAiCommands(aiCmd);
9500
10039
  var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
9501
10040
  registerSchedulesListCommand(schedulesCmd);
9502
10041
  registerSchedulesGetCommand(schedulesCmd);
@@ -9522,7 +10061,7 @@ async function showInteractiveMenu() {
9522
10061
  } catch {
9523
10062
  }
9524
10063
  console.log(INSFORGE_LOGO);
9525
- clack17.intro(`InsForge CLI v${pkg.version}`);
10064
+ clack18.intro(`InsForge CLI v${pkg.version}`);
9526
10065
  const options = [];
9527
10066
  if (!isLoggedIn) {
9528
10067
  options.push({ value: "login", label: "Log in to InsForge" });
@@ -9543,7 +10082,7 @@ async function showInteractiveMenu() {
9543
10082
  options
9544
10083
  });
9545
10084
  if (isCancel2(action)) {
9546
- clack17.cancel("Bye!");
10085
+ clack18.cancel("Bye!");
9547
10086
  process.exit(0);
9548
10087
  }
9549
10088
  switch (action) {