@insforge/cli 0.1.74 → 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 readFileSync11 } 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((resolve5) => {
45
- this.waiter = resolve5;
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((resolve5, reject) => {
428
- resolveResult = resolve5;
427
+ const resultPromise = new Promise((resolve9, reject) => {
428
+ resolveResult = resolve9;
429
429
  rejectResult = reject;
430
430
  });
431
431
  const server = createServer((req, res) => {
@@ -1158,7 +1158,7 @@ function registerProjectsCommands(projectsCmd2) {
1158
1158
  }
1159
1159
  outputTable(
1160
1160
  ["ID", "Name", "Region", "Status", "AppKey"],
1161
- projects.map((p) => [p.id, p.name, p.region, p.status, p.appkey])
1161
+ projects.map((p3) => [p3.id, p3.name, p3.region, p3.status, p3.appkey])
1162
1162
  );
1163
1163
  }
1164
1164
  } catch (err) {
@@ -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));
@@ -1888,18 +1888,34 @@ async function getJwtSecret() {
1888
1888
  function spliceDatabasePassword(maskedUrl, password3) {
1889
1889
  return maskedUrl.replace(/^(postgresql:\/\/[^:]+:)[^@]+(@)/, `$1${password3}$2`);
1890
1890
  }
1891
+ function isMaskedDatabasePassword(value) {
1892
+ return /^\*+$/.test(value);
1893
+ }
1894
+ async function fetchDatabasePasswordOnce() {
1895
+ try {
1896
+ const res = await ossFetch("/api/metadata/database-password");
1897
+ const body = await res.json();
1898
+ const pw = body.databasePassword;
1899
+ if (typeof pw !== "string" || !pw || isMaskedDatabasePassword(pw)) return null;
1900
+ return pw;
1901
+ } catch {
1902
+ return null;
1903
+ }
1904
+ }
1891
1905
  async function getDatabaseConnectionString() {
1892
1906
  try {
1893
- const [urlRes, pwRes] = await Promise.all([
1894
- ossFetch("/api/metadata/database-connection-string"),
1895
- ossFetch("/api/metadata/database-password")
1896
- ]);
1907
+ const urlRes = await ossFetch("/api/metadata/database-connection-string");
1897
1908
  const urlBody = await urlRes.json();
1898
- const pwBody = await pwRes.json();
1899
1909
  const masked = urlBody.connectionURL;
1900
- const password3 = pwBody.databasePassword;
1901
1910
  if (typeof masked !== "string" || !masked) return null;
1902
- if (typeof password3 !== "string" || !password3) return null;
1911
+ let password3 = await fetchDatabasePasswordOnce();
1912
+ const POLL_ATTEMPTS = 9;
1913
+ const POLL_DELAY_MS = 2e3;
1914
+ for (let attempt = 0; password3 === null && attempt < POLL_ATTEMPTS; attempt++) {
1915
+ await new Promise((r) => setTimeout(r, POLL_DELAY_MS));
1916
+ password3 = await fetchDatabasePasswordOnce();
1917
+ }
1918
+ if (password3 === null) return null;
1903
1919
  return spliceDatabasePassword(masked, password3);
1904
1920
  } catch {
1905
1921
  return null;
@@ -1930,6 +1946,9 @@ ${err.nextActions}`;
1930
1946
  if (res.status === 404 && isRouteLevel404 && path6 === "/api/database/migrations") {
1931
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.";
1932
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
+ }
1933
1952
  throw new CLIError(message);
1934
1953
  }
1935
1954
  return res;
@@ -1938,8 +1957,8 @@ ${err.nextActions}`;
1938
1957
  // src/auth-providers/apply.ts
1939
1958
  var execFileAsync = promisify2(execFile);
1940
1959
  var VALID_AUTH_PROVIDERS = ["better-auth"];
1941
- function pathExists(p) {
1942
- return fs.stat(p).then(() => true, () => false);
1960
+ function pathExists(p3) {
1961
+ return fs.stat(p3).then(() => true, () => false);
1943
1962
  }
1944
1963
  function deepMergeKeepBase(base, patch) {
1945
1964
  const out = { ...base };
@@ -2330,11 +2349,11 @@ async function collectDeploymentFiles(sourceDir) {
2330
2349
  return files;
2331
2350
  }
2332
2351
  async function createZipBuffer(sourceDir) {
2333
- return new Promise((resolve5, reject) => {
2352
+ return new Promise((resolve9, reject) => {
2334
2353
  const archive = archiver("zip", { zlib: { level: 9 } });
2335
2354
  const chunks = [];
2336
2355
  archive.on("data", (chunk) => chunks.push(chunk));
2337
- archive.on("end", () => resolve5(Buffer.concat(chunks)));
2356
+ archive.on("end", () => resolve9(Buffer.concat(chunks)));
2338
2357
  archive.on("error", (err) => reject(err));
2339
2358
  archive.directory(sourceDir, false, (entry) => {
2340
2359
  if (shouldExclude(entry.name)) return false;
@@ -2411,12 +2430,12 @@ async function startDirectDeployment(deploymentId, startBody) {
2411
2430
  });
2412
2431
  await response.json();
2413
2432
  }
2414
- async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
2415
- spinner10?.message("Building and deploying...");
2433
+ async function pollDeployment(deploymentId, spinner11, syncBeforeRead) {
2434
+ spinner11?.message("Building and deploying...");
2416
2435
  const startTime = Date.now();
2417
2436
  let deployment = null;
2418
2437
  while (Date.now() - startTime < POLL_TIMEOUT_MS3) {
2419
- await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS3));
2438
+ await new Promise((resolve9) => setTimeout(resolve9, POLL_INTERVAL_MS3));
2420
2439
  try {
2421
2440
  if (syncBeforeRead) {
2422
2441
  await ossFetch(`/api/deployments/${deploymentId}/sync`, { method: "POST" });
@@ -2428,13 +2447,13 @@ async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
2428
2447
  break;
2429
2448
  }
2430
2449
  if (status === "ERROR" || status === "CANCELED") {
2431
- spinner10?.stop("Deployment failed");
2450
+ spinner11?.stop("Deployment failed");
2432
2451
  throw new CLIError(
2433
2452
  getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`
2434
2453
  );
2435
2454
  }
2436
2455
  const elapsed = Math.round((Date.now() - startTime) / 1e3);
2437
- spinner10?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
2456
+ spinner11?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
2438
2457
  } catch (err) {
2439
2458
  if (err instanceof CLIError) throw err;
2440
2459
  }
@@ -2444,20 +2463,20 @@ async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
2444
2463
  return { deploymentId, deployment, isReady, liveUrl };
2445
2464
  }
2446
2465
  async function deployProjectDirect(opts, config) {
2447
- const { sourceDir, startBody = {}, spinner: spinner10 } = opts;
2448
- spinner10?.start("Scanning source files...");
2466
+ const { sourceDir, startBody = {}, spinner: spinner11 } = opts;
2467
+ spinner11?.start("Scanning source files...");
2449
2468
  const localFiles = await collectDeploymentFiles(sourceDir);
2450
2469
  if (localFiles.length === 0) {
2451
2470
  throw new CLIError("No deployable files found in the source directory.");
2452
2471
  }
2453
- spinner10?.message("Creating deployment...");
2472
+ spinner11?.message("Creating deployment...");
2454
2473
  const createResult = await createDirectDeploymentSession(
2455
2474
  config,
2456
2475
  localFiles.map(({ path: relativePath, sha, size }) => ({ path: relativePath, sha, size }))
2457
2476
  );
2458
2477
  const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
2459
2478
  const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
2460
- spinner10?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
2479
+ spinner11?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
2461
2480
  await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
2462
2481
  const localFile = localFileByPath.get(manifestFile.path);
2463
2482
  if (!localFile) {
@@ -2468,18 +2487,18 @@ async function deployProjectDirect(opts, config) {
2468
2487
  }
2469
2488
  await uploadDirectDeploymentFile(createResult.id, manifestFile, localFile);
2470
2489
  });
2471
- spinner10?.message("Starting deployment...");
2490
+ spinner11?.message("Starting deployment...");
2472
2491
  await startDirectDeployment(createResult.id, startBody);
2473
- return await pollDeployment(createResult.id, spinner10, !isInsforgeCloudOssHost(config.oss_host));
2492
+ return await pollDeployment(createResult.id, spinner11, !isInsforgeCloudOssHost(config.oss_host));
2474
2493
  }
2475
2494
  async function deployProjectLegacy(opts) {
2476
- const { sourceDir, startBody = {}, spinner: spinner10 } = opts;
2477
- spinner10?.message("Creating deployment...");
2495
+ const { sourceDir, startBody = {}, spinner: spinner11 } = opts;
2496
+ spinner11?.message("Creating deployment...");
2478
2497
  const createRes = await ossFetch("/api/deployments", { method: "POST" });
2479
2498
  const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
2480
- spinner10?.message("Compressing source files...");
2499
+ spinner11?.message("Compressing source files...");
2481
2500
  const zipBuffer = await createZipBuffer(sourceDir);
2482
- spinner10?.message("Uploading...");
2501
+ spinner11?.message("Uploading...");
2483
2502
  const formData = new FormData();
2484
2503
  for (const [key, value] of Object.entries(uploadFields)) {
2485
2504
  formData.append(key, value);
@@ -2490,13 +2509,13 @@ async function deployProjectLegacy(opts) {
2490
2509
  const uploadErr = await uploadRes.text();
2491
2510
  throw new CLIError(`Failed to upload: ${uploadErr}`);
2492
2511
  }
2493
- spinner10?.message("Starting deployment...");
2512
+ spinner11?.message("Starting deployment...");
2494
2513
  const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
2495
2514
  method: "POST",
2496
2515
  body: JSON.stringify(startBody)
2497
2516
  });
2498
2517
  await startRes.json();
2499
- return await pollDeployment(deploymentId, spinner10, false);
2518
+ return await pollDeployment(deploymentId, spinner11, false);
2500
2519
  }
2501
2520
  async function deployProject(opts) {
2502
2521
  const config = getProjectConfig();
@@ -2531,7 +2550,7 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
2531
2550
  `"${dirName}" is an excluded directory and cannot be used as a deploy source. Please specify your project root or output directory instead.`
2532
2551
  );
2533
2552
  }
2534
- const spinner10 = !json ? clack11.spinner() : null;
2553
+ const spinner11 = !json ? clack11.spinner() : null;
2535
2554
  const startBody = {};
2536
2555
  if (opts.env) {
2537
2556
  try {
@@ -2557,9 +2576,9 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
2557
2576
  throw new CLIError("Invalid --meta JSON.");
2558
2577
  }
2559
2578
  }
2560
- const result = await deployProject({ sourceDir, startBody, spinner: spinner10 });
2579
+ const result = await deployProject({ sourceDir, startBody, spinner: spinner11 });
2561
2580
  if (result.isReady) {
2562
- spinner10?.stop("Deployment complete");
2581
+ spinner11?.stop("Deployment complete");
2563
2582
  if (json) {
2564
2583
  outputJson(result.deployment);
2565
2584
  } else {
@@ -2569,7 +2588,7 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
2569
2588
  clack11.log.info(`Deployment ID: ${result.deploymentId}`);
2570
2589
  }
2571
2590
  } else {
2572
- spinner10?.stop("Deployment is still building");
2591
+ spinner11?.stop("Deployment is still building");
2573
2592
  if (json) {
2574
2593
  outputJson({
2575
2594
  id: result.deploymentId,
@@ -2982,7 +3001,7 @@ function registerCreateCommand(program2) {
2982
3001
  clack12.note(
2983
3002
  `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
2984
3003
 
2985
- ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
3004
+ ${prompts.map((p3) => `\u2022 "${p3}"`).join("\n")}`,
2986
3005
  "Start building"
2987
3006
  );
2988
3007
  }
@@ -3135,13 +3154,13 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
3135
3154
  // src/commands/projects/link.ts
3136
3155
  var execAsync3 = promisify4(exec3);
3137
3156
  async function runNpmInstall(startMessage = "Installing dependencies...") {
3138
- const spinner10 = clack13.spinner();
3139
- spinner10.start(startMessage);
3157
+ const spinner11 = clack13.spinner();
3158
+ spinner11.start(startMessage);
3140
3159
  try {
3141
3160
  await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
3142
- spinner10.stop("Dependencies installed");
3161
+ spinner11.stop("Dependencies installed");
3143
3162
  } catch (err) {
3144
- spinner10.stop("Failed to install dependencies");
3163
+ spinner11.stop("Failed to install dependencies");
3145
3164
  clack13.log.warn(`npm install failed: ${err.message}`);
3146
3165
  clack13.log.info("Run `npm install` manually to install dependencies.");
3147
3166
  }
@@ -3155,13 +3174,13 @@ async function runNpmSetupIfPresent() {
3155
3174
  } catch {
3156
3175
  }
3157
3176
  if (!hasSetup) return;
3158
- const spinner10 = clack13.spinner();
3159
- spinner10.start("Running setup (schema + migrations)...");
3177
+ const spinner11 = clack13.spinner();
3178
+ spinner11.start("Running setup (schema + migrations)...");
3160
3179
  try {
3161
3180
  await execAsync3("npm run setup", { cwd: process.cwd(), maxBuffer: 20 * 1024 * 1024 });
3162
- spinner10.stop("Setup complete");
3181
+ spinner11.stop("Setup complete");
3163
3182
  } catch (err) {
3164
- spinner10.stop("Setup failed");
3183
+ spinner11.stop("Setup failed");
3165
3184
  clack13.log.warn(`npm run setup failed: ${err.message.split("\n")[0]}`);
3166
3185
  clack13.log.info("Inspect the error, fix DATABASE_URL or network access, then run `npm run setup` manually.");
3167
3186
  }
@@ -3187,7 +3206,7 @@ function registerProjectLinkCommand(program2) {
3187
3206
  outputJson({ success: true, skills_only: true });
3188
3207
  } else {
3189
3208
  clack13.note(
3190
- `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.`,
3191
3210
  "What's next"
3192
3211
  );
3193
3212
  }
@@ -3379,9 +3398,9 @@ function registerProjectLinkCommand(program2) {
3379
3398
  }
3380
3399
  const selected = await select2({
3381
3400
  message: "Select a project to link:",
3382
- options: projects.map((p) => ({
3383
- value: p.id,
3384
- label: `${p.name} (${p.region}, ${p.status})`
3401
+ options: projects.map((p3) => ({
3402
+ value: p3.id,
3403
+ label: `${p3.name} (${p3.region}, ${p3.status})`
3385
3404
  }))
3386
3405
  });
3387
3406
  if (isCancel2(selected)) process.exit(0);
@@ -3520,7 +3539,7 @@ function registerProjectLinkCommand(program2) {
3520
3539
  clack13.note(
3521
3540
  `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
3522
3541
 
3523
- ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
3542
+ ${prompts.map((p3) => `\u2022 "${p3}"`).join("\n")}`,
3524
3543
  "Start building"
3525
3544
  );
3526
3545
  }
@@ -3677,13 +3696,13 @@ function registerDbPoliciesCommand(dbCmd2) {
3677
3696
  }
3678
3697
  outputTable(
3679
3698
  ["Table", "Policy Name", "Command", "Roles", "Qual", "With Check"],
3680
- policies.map((p) => [
3681
- String(p.tableName ?? "-"),
3682
- String(p.policyName ?? "-"),
3683
- String(p.cmd ?? "-"),
3684
- Array.isArray(p.roles) ? p.roles.join(", ") : String(p.roles ?? "-"),
3685
- String(p.qual ?? "-"),
3686
- String(p.withCheck ?? "-")
3699
+ policies.map((p3) => [
3700
+ String(p3.tableName ?? "-"),
3701
+ String(p3.policyName ?? "-"),
3702
+ String(p3.cmd ?? "-"),
3703
+ Array.isArray(p3.roles) ? p3.roles.join(", ") : String(p3.roles ?? "-"),
3704
+ String(p3.qual ?? "-"),
3705
+ String(p3.withCheck ?? "-")
3687
3706
  ])
3688
3707
  );
3689
3708
  }
@@ -4829,10 +4848,10 @@ function registerStorageDeleteBucketCommand(storageCmd2) {
4829
4848
  try {
4830
4849
  await requireAuth();
4831
4850
  if (!yes && !json) {
4832
- const confirm6 = await confirm2({
4851
+ const confirm8 = await confirm2({
4833
4852
  message: `Delete bucket "${name}" and all its objects? This cannot be undone.`
4834
4853
  });
4835
- if (isCancel2(confirm6) || !confirm6) {
4854
+ if (isCancel2(confirm8) || !confirm8) {
4836
4855
  process.exit(0);
4837
4856
  }
4838
4857
  }
@@ -4979,12 +4998,12 @@ function registerListCommand(program2) {
4979
4998
  id: org.id,
4980
4999
  name: org.name,
4981
5000
  type: org.type ?? null,
4982
- projects: projects.map((p) => ({
4983
- id: p.id,
4984
- name: p.name,
4985
- region: p.region,
4986
- status: p.status,
4987
- appkey: p.appkey
5001
+ projects: projects.map((p3) => ({
5002
+ id: p3.id,
5003
+ name: p3.name,
5004
+ region: p3.region,
5005
+ status: p3.status,
5006
+ appkey: p3.appkey
4988
5007
  }))
4989
5008
  }))
4990
5009
  );
@@ -4996,13 +5015,13 @@ function registerListCommand(program2) {
4996
5015
  rows.push([org.name, "-", "-", "-", "-"]);
4997
5016
  } else {
4998
5017
  for (let i = 0; i < projects.length; i++) {
4999
- const p = projects[i];
5018
+ const p3 = projects[i];
5000
5019
  rows.push([
5001
5020
  i === 0 ? org.name : "",
5002
- p.name,
5003
- p.region,
5004
- p.status,
5005
- p.appkey
5021
+ p3.name,
5022
+ p3.region,
5023
+ p3.status,
5024
+ p3.appkey
5006
5025
  ]);
5007
5026
  }
5008
5027
  }
@@ -5383,10 +5402,10 @@ function registerSecretsDeleteCommand(secretsCmd2) {
5383
5402
  try {
5384
5403
  await requireAuth();
5385
5404
  if (!yes && !json) {
5386
- const confirm6 = await confirm2({
5405
+ const confirm8 = await confirm2({
5387
5406
  message: `Delete secret "${key}"? This cannot be undone.`
5388
5407
  });
5389
- if (isCancel2(confirm6) || !confirm6) {
5408
+ if (isCancel2(confirm8) || !confirm8) {
5390
5409
  process.exit(0);
5391
5410
  }
5392
5411
  }
@@ -5574,10 +5593,10 @@ function registerSchedulesDeleteCommand(schedulesCmd2) {
5574
5593
  try {
5575
5594
  await requireAuth();
5576
5595
  if (!yes && !json) {
5577
- const confirm6 = await confirm2({
5596
+ const confirm8 = await confirm2({
5578
5597
  message: `Delete schedule "${id}"? This cannot be undone.`
5579
5598
  });
5580
- if (isCancel2(confirm6) || !confirm6) {
5599
+ if (isCancel2(confirm8) || !confirm8) {
5581
5600
  process.exit(0);
5582
5601
  }
5583
5602
  }
@@ -5799,6 +5818,8 @@ function registerComputeUpdateCommand(computeCmd2) {
5799
5818
  outputJson(service);
5800
5819
  } else {
5801
5820
  outputSuccess(`Service "${service.name}" updated [${service.status}]`);
5821
+ if (service.endpointUrl) console.log(` Endpoint: ${service.endpointUrl}`);
5822
+ if (service.port !== void 0) console.log(` Port: ${service.port} (container must listen on this port)`);
5802
5823
  }
5803
5824
  await reportCliUsage("cli.compute.update", true);
5804
5825
  } catch (err) {
@@ -5999,7 +6020,7 @@ primary_region = "${opts.region}"
5999
6020
  };
6000
6021
  }
6001
6022
  function flyctlBuildAndPush(opts) {
6002
- return new Promise((resolve5, reject) => {
6023
+ return new Promise((resolve9, reject) => {
6003
6024
  const cleanupStub = ensureFlyTomlStub({
6004
6025
  dir: opts.dir,
6005
6026
  appId: opts.appId,
@@ -6055,7 +6076,7 @@ function flyctlBuildAndPush(opts) {
6055
6076
  )
6056
6077
  );
6057
6078
  }
6058
- resolve5({ imageRef: `registry.fly.io/${opts.appId}@${m[1]}` });
6079
+ resolve9({ imageRef: `registry.fly.io/${opts.appId}@${m[1]}` });
6059
6080
  });
6060
6081
  });
6061
6082
  }
@@ -6154,6 +6175,7 @@ function registerComputeDeployCommand(computeCmd2) {
6154
6175
  const verb = existing2 ? "updated" : "deployed";
6155
6176
  outputSuccess(`Service "${service2.name}" ${verb} [${service2.status}]`);
6156
6177
  if (service2.endpointUrl) console.log(` Endpoint: ${service2.endpointUrl}`);
6178
+ if (service2.port !== void 0) console.log(` Port: ${service2.port} (container must listen on this port)`);
6157
6179
  }
6158
6180
  await reportCliUsage("cli.compute.deploy", true);
6159
6181
  return;
@@ -6249,6 +6271,7 @@ function registerComputeDeployCommand(computeCmd2) {
6249
6271
  const verb = existing ? "updated" : "deployed";
6250
6272
  outputSuccess(`Service "${service.name}" ${verb} [${service.status}]`);
6251
6273
  if (service.endpointUrl) console.log(` Endpoint: ${service.endpointUrl}`);
6274
+ if (service.port !== void 0) console.log(` Port: ${service.port} (container must listen on this port)`);
6252
6275
  console.log(` Image: ${imageRef} (built remotely; no local image to clean up)`);
6253
6276
  }
6254
6277
  await reportCliUsage("cli.compute.deploy", true);
@@ -6938,7 +6961,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
6938
6961
  const s = !json ? clack15.spinner() : null;
6939
6962
  s?.start("Collecting diagnostic data...");
6940
6963
  const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
6941
- const cliVersion = "0.1.74";
6964
+ const cliVersion = "0.1.78";
6942
6965
  s?.stop("Data collected");
6943
6966
  if (!json) {
6944
6967
  console.log(`
@@ -7572,10 +7595,10 @@ function registerPaymentsConfigCommand(paymentsCmd2) {
7572
7595
  throw new CLIError("Use --yes with --json to remove a Stripe key non-interactively.");
7573
7596
  }
7574
7597
  if (!yes) {
7575
- const confirm6 = await confirm2({
7598
+ const confirm8 = await confirm2({
7576
7599
  message: `Remove Stripe ${environment} key? Payment sync and mutations for this environment will stop.`
7577
7600
  });
7578
- if (isCancel2(confirm6) || !confirm6) process.exit(0);
7601
+ if (isCancel2(confirm8) || !confirm8) process.exit(0);
7579
7602
  }
7580
7603
  const data = await removeStripeSecretKey(environment);
7581
7604
  if (json) {
@@ -8098,10 +8121,10 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
8098
8121
  );
8099
8122
  }
8100
8123
  if (!yes) {
8101
- const confirm6 = await confirm2({
8124
+ const confirm8 = await confirm2({
8102
8125
  message: `Delete Stripe ${environment} product "${productId}"?`
8103
8126
  });
8104
- if (isCancel2(confirm6) || !confirm6) process.exit(0);
8127
+ if (isCancel2(confirm8) || !confirm8) process.exit(0);
8105
8128
  }
8106
8129
  const data = await deletePaymentProduct(environment, productId);
8107
8130
  if (json) {
@@ -8452,14 +8475,14 @@ async function startPosthogCliFlow(projectId, jwt, apiUrl) {
8452
8475
  throw new CLIError("PostHog cli-start returned an unexpected response shape.");
8453
8476
  }
8454
8477
  function sleep(ms, signal) {
8455
- return new Promise((resolve5, reject) => {
8478
+ return new Promise((resolve9, reject) => {
8456
8479
  if (signal?.aborted) {
8457
8480
  reject(new CLIError("Connection wait cancelled."));
8458
8481
  return;
8459
8482
  }
8460
8483
  const timer = setTimeout(() => {
8461
8484
  signal?.removeEventListener("abort", onAbort);
8462
- resolve5();
8485
+ resolve9();
8463
8486
  }, ms);
8464
8487
  const onAbort = () => {
8465
8488
  clearTimeout(timer);
@@ -8762,8 +8785,8 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8762
8785
  } catch {
8763
8786
  }
8764
8787
  }
8765
- const spinner10 = !opts.json && isInteractive ? clack16.spinner() : null;
8766
- 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)");
8767
8790
  try {
8768
8791
  const conn = await pollPosthogConnection(
8769
8792
  projectId,
@@ -8773,20 +8796,20 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8773
8796
  timeoutMs: POLL_TIMEOUT_MS4,
8774
8797
  maxTransientRetries: MAX_TRANSIENT_RETRIES,
8775
8798
  onTick: (elapsed) => {
8776
- if (spinner10) {
8799
+ if (spinner11) {
8777
8800
  const secs = Math.floor(elapsed / 1e3);
8778
8801
  const mins = Math.floor(secs / 60);
8779
8802
  const remaining = `${mins}m ${secs % 60}s elapsed`;
8780
- spinner10.message(`Waiting for connection... (${remaining})`);
8803
+ spinner11.message(`Waiting for connection... (${remaining})`);
8781
8804
  }
8782
8805
  }
8783
8806
  },
8784
8807
  opts.apiUrl
8785
8808
  );
8786
- spinner10?.stop("Connection received from PostHog.");
8809
+ spinner11?.stop("Connection received from PostHog.");
8787
8810
  return conn;
8788
8811
  } catch (err) {
8789
- spinner10?.stop("Connection wait failed.");
8812
+ spinner11?.stop("Connection wait failed.");
8790
8813
  throw err;
8791
8814
  }
8792
8815
  }
@@ -8804,14 +8827,14 @@ function resolveFramework(opts) {
8804
8827
  }
8805
8828
  async function installSdk(pm, cwd, opts) {
8806
8829
  const cmd = installCommand(pm, "posthog-js");
8807
- const spinner10 = !opts.json && isInteractive ? clack16.spinner() : null;
8808
- spinner10?.start(`Installing posthog-js (${cmd})...`);
8830
+ const spinner11 = !opts.json && isInteractive ? clack16.spinner() : null;
8831
+ spinner11?.start(`Installing posthog-js (${cmd})...`);
8809
8832
  try {
8810
8833
  await runInstall(pm, "posthog-js", cwd);
8811
- spinner10?.stop("Installed posthog-js.");
8834
+ spinner11?.stop("Installed posthog-js.");
8812
8835
  return true;
8813
8836
  } catch (err) {
8814
- spinner10?.stop("Install failed.");
8837
+ spinner11?.stop("Install failed.");
8815
8838
  if (!opts.json) {
8816
8839
  clack16.log.warn(
8817
8840
  `Could not run \`${cmd}\` automatically: ${err.message}
@@ -9027,13 +9050,907 @@ function frameworkLabel(framework) {
9027
9050
  return "Astro";
9028
9051
  }
9029
9052
  }
9030
- function relative3(p) {
9031
- return p.replace(process.cwd() + "/", "");
9053
+ function relative3(p3) {
9054
+ return p3.replace(process.cwd() + "/", "");
9055
+ }
9056
+
9057
+ // src/commands/config/export.ts
9058
+ import { writeFileSync as writeFileSync9, existsSync as existsSync14 } from "fs";
9059
+ import { resolve as resolve5 } from "path";
9060
+ import * as p from "@clack/prompts";
9061
+ import pc4 from "picocolors";
9062
+
9063
+ // src/lib/config-toml.ts
9064
+ import * as smolToml from "smol-toml";
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
+
9135
+ // src/lib/config-schema.ts
9136
+ var ConfigValidationError = class extends Error {
9137
+ constructor(path6, message) {
9138
+ super(`config.${path6}: ${message}`);
9139
+ this.path = path6;
9140
+ this.name = "ConfigValidationError";
9141
+ }
9142
+ };
9143
+ function validateConfig(input) {
9144
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
9145
+ throw new ConfigValidationError("", "must be an object");
9146
+ }
9147
+ const obj = input;
9148
+ const out = {};
9149
+ if ("project_id" in obj) {
9150
+ if (typeof obj.project_id !== "string") {
9151
+ throw new ConfigValidationError("project_id", "must be a string");
9152
+ }
9153
+ out.project_id = obj.project_id;
9154
+ }
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
+ }
9175
+ return out;
9176
+ }
9177
+ function validateAuth(input) {
9178
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
9179
+ throw new ConfigValidationError("auth", "must be an object");
9180
+ }
9181
+ const obj = input;
9182
+ const out = {};
9183
+ if ("allowed_redirect_urls" in obj) {
9184
+ const v = obj.allowed_redirect_urls;
9185
+ if (!Array.isArray(v) || !v.every((u) => typeof u === "string")) {
9186
+ throw new ConfigValidationError(
9187
+ "auth.allowed_redirect_urls",
9188
+ "must be an array of strings"
9189
+ );
9190
+ }
9191
+ out.allowed_redirect_urls = v;
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
+ }
9257
+ return out;
9258
+ }
9259
+
9260
+ // src/lib/config-toml.ts
9261
+ function parseConfigToml(input) {
9262
+ let parsed;
9263
+ try {
9264
+ parsed = smolToml.parse(input);
9265
+ } catch (err) {
9266
+ throw new Error(`TOML parse error: ${err.message}`, { cause: err });
9267
+ }
9268
+ return validateConfig(parsed);
9269
+ }
9270
+ function stringifyConfigToml(config) {
9271
+ const lines = [];
9272
+ if (config.project_id !== void 0) {
9273
+ lines.push(`project_id = ${JSON.stringify(config.project_id)}`);
9274
+ lines.push("");
9275
+ }
9276
+ if (config.auth) {
9277
+ lines.push("[auth]");
9278
+ if (config.auth.allowed_redirect_urls !== void 0) {
9279
+ const urls = config.auth.allowed_redirect_urls.map((u) => JSON.stringify(u)).join(", ");
9280
+ lines.push(`allowed_redirect_urls = [${urls}]`);
9281
+ }
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
+ }
9295
+ }
9296
+ return lines.join("\n").replace(/\n+$/, "\n");
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
+ }
9320
+
9321
+ // src/commands/config/export.ts
9322
+ function registerConfigExportCommand(cfg) {
9323
+ cfg.command("export").description("Pull live project config and write insforge.toml").option("--out <path>", "output path", "insforge.toml").option("--force", "overwrite without confirmation").action(async (opts, cmd) => {
9324
+ const { json } = getRootOpts(cmd);
9325
+ try {
9326
+ await requireAuth();
9327
+ const target = resolve5(process.cwd(), opts.out);
9328
+ if (existsSync14(target) && !opts.force) {
9329
+ if (json) {
9330
+ throw new CLIError(
9331
+ `${opts.out} exists. Re-run with --force to overwrite.`,
9332
+ 1,
9333
+ "OUTPUT_EXISTS"
9334
+ );
9335
+ }
9336
+ const ok = await p.confirm({
9337
+ message: `${opts.out} exists. Overwrite?`,
9338
+ initialValue: false
9339
+ });
9340
+ if (!ok || p.isCancel(ok)) {
9341
+ console.log("Aborted.");
9342
+ return;
9343
+ }
9344
+ }
9345
+ const res = await ossFetch("/api/metadata");
9346
+ const raw = await res.json();
9347
+ const config = {};
9348
+ const skipped = [];
9349
+ const authSlice = raw?.auth;
9350
+ if (authSlice && typeof authSlice === "object" && "allowedRedirectUrls" in authSlice) {
9351
+ config.auth = config.auth ?? {};
9352
+ config.auth.allowed_redirect_urls = authSlice.allowedRedirectUrls ?? [];
9353
+ } else {
9354
+ skipped.push("auth.allowed_redirect_urls");
9355
+ }
9356
+ if (authSlice && typeof authSlice === "object" && "smtpConfig" in authSlice && authSlice.smtpConfig) {
9357
+ const s = authSlice.smtpConfig;
9358
+ config.auth = config.auth ?? {};
9359
+ config.auth.smtp = {
9360
+ enabled: s.enabled ?? false,
9361
+ host: s.host ?? "",
9362
+ port: s.port ?? 587,
9363
+ username: s.username ?? "",
9364
+ // When backend has a password set, emit a deterministic env()
9365
+ // placeholder so the user knows which secret to define. We do
9366
+ // NOT round-trip the value (it never leaves the backend).
9367
+ // Re-applying this TOML force-resends from the secrets store
9368
+ // — see config-diff.ts for the force-resend rationale.
9369
+ ...s.hasPassword ? { password: "env(SMTP_PASSWORD)" } : {},
9370
+ sender_email: s.senderEmail ?? "",
9371
+ sender_name: s.senderName ?? "",
9372
+ min_interval_seconds: s.minIntervalSeconds ?? 60
9373
+ };
9374
+ } else {
9375
+ skipped.push("auth.smtp");
9376
+ }
9377
+ const deploymentsSlice = raw?.deployments;
9378
+ if (deploymentsSlice && typeof deploymentsSlice === "object") {
9379
+ if (typeof deploymentsSlice.customSlug === "string" && deploymentsSlice.customSlug) {
9380
+ config.deployments = { subdomain: deploymentsSlice.customSlug };
9381
+ }
9382
+ } else {
9383
+ skipped.push("deployments.subdomain");
9384
+ }
9385
+ const toml = stringifyConfigToml(config);
9386
+ writeFileSync9(target, toml, "utf8");
9387
+ if (json) {
9388
+ console.log(JSON.stringify({ written: target, config, skipped }, null, 2));
9389
+ } else {
9390
+ console.log(`${pc4.green("\u2713")} Wrote ${target}`);
9391
+ if (skipped.length) {
9392
+ console.warn(
9393
+ pc4.yellow(
9394
+ `\u26A0 Skipped ${skipped.length} section(s) not supported by this backend:`
9395
+ ) + "\n" + skipped.map((k) => ` - ${k}`).join("\n")
9396
+ );
9397
+ }
9398
+ }
9399
+ await reportCliUsage("cli.config.export", true);
9400
+ } catch (err) {
9401
+ await reportCliUsage("cli.config.export", false);
9402
+ handleError(err, json);
9403
+ }
9404
+ });
9405
+ }
9406
+
9407
+ // src/commands/config/plan.ts
9408
+ import { readFileSync as readFileSync11 } from "fs";
9409
+ import { resolve as resolve6 } from "path";
9410
+ import pc5 from "picocolors";
9411
+
9412
+ // src/lib/config-diff.ts
9413
+ function diffConfig({ live, file }) {
9414
+ const changes = [];
9415
+ const fileAuth = file.auth;
9416
+ const liveAuth = live.auth ?? {};
9417
+ if (fileAuth && "allowed_redirect_urls" in fileAuth) {
9418
+ const fromV = normalizeUrlList(liveAuth.allowed_redirect_urls);
9419
+ const toV = normalizeUrlList(fileAuth.allowed_redirect_urls);
9420
+ if (!arrayEquals(fromV, toV)) {
9421
+ changes.push({
9422
+ section: "auth",
9423
+ op: "modify",
9424
+ key: "allowed_redirect_urls",
9425
+ from: fromV,
9426
+ to: toV
9427
+ });
9428
+ }
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
+ }
9450
+ return { changes, summary: summarize(changes) };
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
+ };
9510
+ function summarize(changes) {
9511
+ const s = { add: 0, modify: 0, remove: 0, kept: 0 };
9512
+ for (const c of changes) {
9513
+ if (c.op === "modify") s.modify++;
9514
+ }
9515
+ return s;
9516
+ }
9517
+ function normalizeUrlList(input) {
9518
+ return Array.from(new Set(input ?? [])).sort();
9519
+ }
9520
+ function arrayEquals(a, b) {
9521
+ if (a.length !== b.length) return false;
9522
+ return a.every((v, i) => v === b[i]);
9523
+ }
9524
+
9525
+ // src/lib/config-format.ts
9526
+ function formatPlan(result) {
9527
+ if (result.changes.length === 0) {
9528
+ return "No changes. Live state matches insforge.toml.";
9529
+ }
9530
+ const bySection = /* @__PURE__ */ new Map();
9531
+ for (const c of result.changes) {
9532
+ const arr = bySection.get(c.section) ?? [];
9533
+ arr.push(c);
9534
+ bySection.set(c.section, arr);
9535
+ }
9536
+ const lines = [];
9537
+ for (const [section, changes] of bySection) {
9538
+ lines.push(` ${section}:`);
9539
+ for (const c of changes) {
9540
+ lines.push(` ${formatChange(c)}`);
9541
+ }
9542
+ lines.push("");
9543
+ }
9544
+ const s = result.summary;
9545
+ lines.push(
9546
+ `${s.add} add, ${s.modify} modify, ${s.remove} remove, ${s.kept} untracked kept.`
9547
+ );
9548
+ return lines.join("\n");
9549
+ }
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
+ }
9579
+ return `~ ${c.key}: ${JSON.stringify(c.from)} \u2192 ${JSON.stringify(c.to)}`;
9580
+ }
9581
+
9582
+ // src/lib/config-capabilities.ts
9583
+ function metadataSupports(raw, change) {
9584
+ if (change.section === "auth" && change.key === "allowed_redirect_urls") {
9585
+ return raw?.auth !== void 0 && raw.auth !== null && typeof raw.auth === "object" && "allowedRedirectUrls" in raw.auth;
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;
9595
+ return false;
9596
+ }
9597
+ function changePath(change) {
9598
+ if (change.section === "auth.smtp") return "auth.smtp";
9599
+ return `${change.section}.${change.key}`;
9600
+ }
9601
+
9602
+ // src/commands/config/plan.ts
9603
+ function registerConfigPlanCommand(cfg) {
9604
+ cfg.command("plan").description("Show diff between insforge.toml and live project state").option("--file <path>", "path to insforge.toml", "insforge.toml").action(async (opts, cmd) => {
9605
+ const { json } = getRootOpts(cmd);
9606
+ try {
9607
+ await requireAuth();
9608
+ const tomlPath = resolve6(process.cwd(), opts.file);
9609
+ const tomlSource = readFileSync11(tomlPath, "utf8");
9610
+ const file = parseConfigToml(tomlSource);
9611
+ const res = await ossFetch("/api/metadata");
9612
+ const raw = await res.json();
9613
+ const live = {
9614
+ auth: { allowed_redirect_urls: raw.auth?.allowedRedirectUrls ?? [] }
9615
+ };
9616
+ const result = diffConfig({ live, file });
9617
+ const skipped = result.changes.filter((c) => !metadataSupports(raw, c)).map((c) => changePath(c));
9618
+ if (json) {
9619
+ console.log(JSON.stringify({ ...result, skipped }, null, 2));
9620
+ } else {
9621
+ console.log(`Plan for insforge.toml (file: ${opts.file}):
9622
+ `);
9623
+ console.log(formatPlan(result));
9624
+ if (skipped.length) {
9625
+ console.warn(
9626
+ "\n" + pc5.yellow(`\u26A0 Apply will skip ${skipped.length} section(s) \u2014 backend doesn't support them yet:`) + "\n" + skipped.map((k) => ` - ${k}`).join("\n")
9627
+ );
9628
+ }
9629
+ }
9630
+ await reportCliUsage("cli.config.plan", true);
9631
+ } catch (err) {
9632
+ await reportCliUsage("cli.config.plan", false);
9633
+ handleError(err, json);
9634
+ }
9635
+ });
9636
+ }
9637
+
9638
+ // src/commands/config/apply.ts
9639
+ import { readFileSync as readFileSync12 } from "fs";
9640
+ import { resolve as resolve7 } from "path";
9641
+ import * as p2 from "@clack/prompts";
9642
+ import pc6 from "picocolors";
9643
+ function registerConfigApplyCommand(cfg) {
9644
+ cfg.command("apply").description("Apply insforge.toml to the live project").option("--file <path>", "path to insforge.toml", "insforge.toml").option("--dry-run", "show plan, do not apply").option("--auto-approve", "skip confirmation prompt").action(async (opts, cmd) => {
9645
+ const { json, yes } = getRootOpts(cmd);
9646
+ try {
9647
+ await requireAuth();
9648
+ const tomlPath = resolve7(process.cwd(), opts.file);
9649
+ const tomlSource = readFileSync12(tomlPath, "utf8");
9650
+ const file = parseConfigToml(tomlSource);
9651
+ const res = await ossFetch("/api/metadata");
9652
+ const raw = await res.json();
9653
+ const live = liveFromMetadata(raw);
9654
+ const result = diffConfig({ live, file });
9655
+ const approved = opts.autoApprove || yes;
9656
+ if (!json) {
9657
+ console.log(formatPlan(result));
9658
+ }
9659
+ if (result.changes.length === 0 || opts.dryRun) {
9660
+ if (json) {
9661
+ console.log(
9662
+ JSON.stringify({ plan: result, applied: false, dryRun: !!opts.dryRun }, null, 2)
9663
+ );
9664
+ }
9665
+ await reportCliUsage("cli.config.apply", true);
9666
+ return;
9667
+ }
9668
+ if (!approved) {
9669
+ if (json) {
9670
+ throw new CLIError(
9671
+ "Refusing to apply in --json mode without --auto-approve or --yes.",
9672
+ 1,
9673
+ "CONFIRMATION_REQUIRED"
9674
+ );
9675
+ }
9676
+ const ok = await p2.confirm({
9677
+ message: "Apply these changes?",
9678
+ initialValue: false
9679
+ });
9680
+ if (!ok || p2.isCancel(ok)) {
9681
+ console.log("Aborted.");
9682
+ await reportCliUsage("cli.config.apply", true);
9683
+ return;
9684
+ }
9685
+ }
9686
+ const applied = [];
9687
+ const skipped = [];
9688
+ for (const change of result.changes) {
9689
+ const path6 = changePath(change);
9690
+ if (!metadataSupports(raw, change)) {
9691
+ skipped.push({
9692
+ key: path6,
9693
+ reason: `your backend doesn't expose ${path6} \u2014 upgrade the project to apply this section`
9694
+ });
9695
+ continue;
9696
+ }
9697
+ await applyChange(change);
9698
+ applied.push(change);
9699
+ }
9700
+ if (json) {
9701
+ console.log(
9702
+ JSON.stringify({ plan: result, applied, skipped }, null, 2)
9703
+ );
9704
+ } else {
9705
+ if (skipped.length) {
9706
+ console.warn(
9707
+ pc6.yellow(`\u26A0 Skipped ${skipped.length} section(s):`) + "\n" + skipped.map((s) => ` - ${s.key}: ${s.reason}`).join("\n")
9708
+ );
9709
+ }
9710
+ if (applied.length) {
9711
+ console.log(
9712
+ `${pc6.green("\u2713")} Applied ${applied.length} of ${result.changes.length} change(s).`
9713
+ );
9714
+ } else {
9715
+ console.log("Nothing applied.");
9716
+ }
9717
+ }
9718
+ await reportCliUsage("cli.config.apply", true);
9719
+ } catch (err) {
9720
+ await reportCliUsage("cli.config.apply", false);
9721
+ handleError(err, json);
9722
+ }
9723
+ });
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
+ }
9748
+ async function applyChange(change) {
9749
+ if (change.section === "auth" && change.key === "allowed_redirect_urls") {
9750
+ await ossFetch("/api/auth/config", {
9751
+ method: "PUT",
9752
+ body: JSON.stringify({ allowedRedirectUrls: change.to })
9753
+ });
9754
+ return;
9755
+ }
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)}`);
9789
+ }
9790
+
9791
+ // src/commands/config/index.ts
9792
+ function registerConfigCommand(program2) {
9793
+ const cfg = program2.command("config").description("Manage insforge.toml (declarative project configuration)");
9794
+ registerConfigExportCommand(cfg);
9795
+ registerConfigPlanCommand(cfg);
9796
+ registerConfigApplyCommand(cfg);
9797
+ }
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);
9032
9949
  }
9033
9950
 
9034
9951
  // src/index.ts
9035
9952
  var __dirname = dirname3(fileURLToPath(import.meta.url));
9036
- var pkg = JSON.parse(readFileSync11(join17(__dirname, "../package.json"), "utf-8"));
9953
+ var pkg = JSON.parse(readFileSync14(join18(__dirname, "../package.json"), "utf-8"));
9037
9954
  var INSFORGE_LOGO = `
9038
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
9039
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
@@ -9117,6 +10034,8 @@ registerComputeStopCommand(computeCmd);
9117
10034
  registerComputeEventsCommand(computeCmd);
9118
10035
  var posthogCmd = program.command("posthog").description("Manage PostHog product analytics integration");
9119
10036
  registerPosthogSetupCommand(posthogCmd);
10037
+ var aiCmd = program.command("ai").description("Manage AI model gateway setup");
10038
+ registerAiCommands(aiCmd);
9120
10039
  var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
9121
10040
  registerSchedulesListCommand(schedulesCmd);
9122
10041
  registerSchedulesGetCommand(schedulesCmd);
@@ -9124,6 +10043,7 @@ registerSchedulesCreateCommand(schedulesCmd);
9124
10043
  registerSchedulesUpdateCommand(schedulesCmd);
9125
10044
  registerSchedulesDeleteCommand(schedulesCmd);
9126
10045
  registerSchedulesLogsCommand(schedulesCmd);
10046
+ registerConfigCommand(program);
9127
10047
  if (process.argv.length <= 2 && process.stdout.isTTY) {
9128
10048
  await showInteractiveMenu();
9129
10049
  } else {
@@ -9141,7 +10061,7 @@ async function showInteractiveMenu() {
9141
10061
  } catch {
9142
10062
  }
9143
10063
  console.log(INSFORGE_LOGO);
9144
- clack17.intro(`InsForge CLI v${pkg.version}`);
10064
+ clack18.intro(`InsForge CLI v${pkg.version}`);
9145
10065
  const options = [];
9146
10066
  if (!isLoggedIn) {
9147
10067
  options.push({ value: "login", label: "Log in to InsForge" });
@@ -9162,7 +10082,7 @@ async function showInteractiveMenu() {
9162
10082
  options
9163
10083
  });
9164
10084
  if (isCancel2(action)) {
9165
- clack17.cancel("Bye!");
10085
+ clack18.cancel("Bye!");
9166
10086
  process.exit(0);
9167
10087
  }
9168
10088
  switch (action) {