@insforge/cli 0.1.61 → 0.1.63

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 readFileSync7 } from "fs";
5
- import { join as join13, dirname } from "path";
4
+ import { readFileSync as readFileSync8 } from "fs";
5
+ import { join as join14, dirname as dirname2 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { Command } from "commander";
8
- import * as clack11 from "@clack/prompts";
8
+ import * as clack15 from "@clack/prompts";
9
9
 
10
10
  // src/lib/prompts.ts
11
11
  import * as readline from "readline";
@@ -220,6 +220,12 @@ function getLocalConfigDir() {
220
220
  function getLocalConfigFile() {
221
221
  return join(getLocalConfigDir(), "project.json");
222
222
  }
223
+ function getParentBackupFile() {
224
+ return join(getLocalConfigDir(), "project.parent.json");
225
+ }
226
+ function getProjectConfigFile() {
227
+ return getLocalConfigFile();
228
+ }
223
229
  function getProjectConfig() {
224
230
  const file = getLocalConfigFile();
225
231
  if (!existsSync(file)) {
@@ -644,7 +650,8 @@ async function refreshAccessToken(apiUrl) {
644
650
  }
645
651
 
646
652
  // src/lib/api/platform.ts
647
- async function platformFetch(path5, options = {}, apiUrl) {
653
+ async function platformFetch(path6, options = {}, apiUrl) {
654
+ const { passThroughStatuses, ...fetchOptions } = options;
648
655
  const baseUrl = getPlatformApiUrl(apiUrl);
649
656
  const token = getAccessToken();
650
657
  if (!token) {
@@ -653,19 +660,19 @@ async function platformFetch(path5, options = {}, apiUrl) {
653
660
  const headers = {
654
661
  "Content-Type": "application/json",
655
662
  Authorization: `Bearer ${token}`,
656
- ...options.headers ?? {}
663
+ ...fetchOptions.headers ?? {}
657
664
  };
658
- const fullUrl = `${baseUrl}${path5}`;
665
+ const fullUrl = `${baseUrl}${path6}`;
659
666
  if (process.env.INSFORGE_DEBUG) {
660
- console.error(`[DEBUG] ${options.method ?? "GET"} ${fullUrl}`);
667
+ console.error(`[DEBUG] ${fetchOptions.method ?? "GET"} ${fullUrl}`);
661
668
  console.error(`[DEBUG] Headers: ${JSON.stringify(headers, null, 2)}`);
662
- if (options.body) {
663
- console.error(`[DEBUG] Body: ${typeof options.body === "string" ? options.body : JSON.stringify(options.body)}`);
669
+ if (fetchOptions.body) {
670
+ console.error(`[DEBUG] Body: ${typeof fetchOptions.body === "string" ? fetchOptions.body : JSON.stringify(fetchOptions.body)}`);
664
671
  }
665
672
  }
666
673
  let res;
667
674
  try {
668
- res = await fetch(fullUrl, { ...options, headers });
675
+ res = await fetch(fullUrl, { ...fetchOptions, headers });
669
676
  } catch (err) {
670
677
  throw new CLIError(formatFetchError(err, fullUrl));
671
678
  }
@@ -674,16 +681,22 @@ async function platformFetch(path5, options = {}, apiUrl) {
674
681
  headers.Authorization = `Bearer ${newToken}`;
675
682
  let retryRes;
676
683
  try {
677
- retryRes = await fetch(fullUrl, { ...options, headers });
684
+ retryRes = await fetch(fullUrl, { ...fetchOptions, headers });
678
685
  } catch (err) {
679
686
  throw new CLIError(formatFetchError(err, fullUrl));
680
687
  }
688
+ if (passThroughStatuses?.includes(retryRes.status)) {
689
+ return retryRes;
690
+ }
681
691
  if (!retryRes.ok) {
682
692
  const err = await retryRes.json().catch(() => ({}));
683
693
  throw new CLIError(err.error ?? `Request failed: ${retryRes.status}`, retryRes.status === 403 ? 5 : 1);
684
694
  }
685
695
  return retryRes;
686
696
  }
697
+ if (passThroughStatuses?.includes(res.status)) {
698
+ return res;
699
+ }
687
700
  if (!res.ok) {
688
701
  const err = await res.json().catch(() => ({}));
689
702
  const msg = err.message ? `${err.error ?? res.status}: ${err.message}` : err.error ?? `Request failed: ${res.status}`;
@@ -811,6 +824,55 @@ async function createProject(orgId, name, region, apiUrl) {
811
824
  const data = await res.json();
812
825
  return data.project ?? data;
813
826
  }
827
+ async function createBranchApi(parentProjectId, body, apiUrl) {
828
+ const res = await platformFetch(`/projects/v1/${parentProjectId}/branches`, {
829
+ method: "POST",
830
+ body: JSON.stringify(body)
831
+ }, apiUrl);
832
+ const data = await res.json();
833
+ return data.branch;
834
+ }
835
+ async function listBranchesApi(parentProjectId, apiUrl) {
836
+ const res = await platformFetch(`/projects/v1/${parentProjectId}/branches`, {}, apiUrl);
837
+ const data = await res.json();
838
+ return data.data ?? [];
839
+ }
840
+ async function getBranchApi(branchId, apiUrl) {
841
+ const res = await platformFetch(`/projects/v1/branches/${branchId}`, {}, apiUrl);
842
+ const data = await res.json();
843
+ return data.branch;
844
+ }
845
+ async function deleteBranchApi(branchId, apiUrl) {
846
+ await platformFetch(`/projects/v1/branches/${branchId}`, { method: "DELETE" }, apiUrl);
847
+ }
848
+ async function resetBranchApi(branchId, apiUrl) {
849
+ const res = await platformFetch(
850
+ `/projects/v1/branches/${branchId}/reset`,
851
+ { method: "POST" },
852
+ apiUrl
853
+ );
854
+ const data = await res.json();
855
+ return data.branch;
856
+ }
857
+ async function mergeBranchDryRunApi(branchId, apiUrl) {
858
+ const res = await platformFetch(
859
+ `/projects/v1/branches/${branchId}/merge?dryRun=true`,
860
+ { method: "POST" },
861
+ apiUrl
862
+ );
863
+ return await res.json();
864
+ }
865
+ async function mergeBranchExecuteApi(branchId, apiUrl) {
866
+ const res = await platformFetch(
867
+ `/projects/v1/branches/${branchId}/merge`,
868
+ { method: "POST", passThroughStatuses: [409] },
869
+ apiUrl
870
+ );
871
+ if (res.status === 409) {
872
+ return { ok: false, conflict: await res.json() };
873
+ }
874
+ return { ok: true, result: await res.json() };
875
+ }
814
876
 
815
877
  // src/commands/login.ts
816
878
  function registerLoginCommand(program2) {
@@ -1098,143 +1160,6 @@ function registerProjectsCommands(projectsCmd2) {
1098
1160
  });
1099
1161
  }
1100
1162
 
1101
- // src/commands/projects/link.ts
1102
- import { exec as exec3 } from "child_process";
1103
- import { promisify as promisify3 } from "util";
1104
- import * as fs4 from "fs/promises";
1105
- import * as path4 from "path";
1106
- import * as clack8 from "@clack/prompts";
1107
- import pc2 from "picocolors";
1108
-
1109
- // src/lib/skills.ts
1110
- import { exec } from "child_process";
1111
- import { existsSync as existsSync2, readFileSync as readFileSync2, appendFileSync } from "fs";
1112
- import { join as join2 } from "path";
1113
- import { promisify } from "util";
1114
- import * as clack5 from "@clack/prompts";
1115
- var execAsync = promisify(exec);
1116
- var SKILL_INSTALL_TIMEOUT_MS = 6e4;
1117
- function describeExecError(err) {
1118
- const e = err;
1119
- if (e.killed && (e.signal === "SIGTERM" || e.signal === "SIGKILL")) {
1120
- return `timed out after ${SKILL_INSTALL_TIMEOUT_MS / 1e3}s \u2014 the npm registry may be slow or blocked by your network`;
1121
- }
1122
- if (e.code === "ENOENT") {
1123
- return "`npx` is not on your PATH \u2014 install Node.js 18+ and reopen your shell";
1124
- }
1125
- const stderr = (typeof e.stderr === "string" ? e.stderr : e.stderr?.toString()) ?? "";
1126
- if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(stderr)) return "cannot reach the npm registry (DNS lookup failed) \u2014 check your internet connection";
1127
- if (/ECONNREFUSED/i.test(stderr)) return "connection to the npm registry was refused \u2014 a proxy or firewall is likely blocking it";
1128
- if (/ETIMEDOUT|ESOCKETTIMEDOUT|network timeout/i.test(stderr)) return "the npm registry timed out \u2014 check your VPN, proxy, or corporate network";
1129
- if (/CERT_HAS_EXPIRED|UNABLE_TO_VERIFY_LEAF_SIGNATURE|SELF_SIGNED_CERT/i.test(stderr)) return "TLS error reaching the npm registry \u2014 a corporate proxy may be intercepting HTTPS";
1130
- if (/\bE404\b|404 Not Found/i.test(stderr)) return "npm returned 404 \u2014 the `skills` package or a dependency could not be found (check your npm registry config)";
1131
- if (/EACCES|permission denied/i.test(stderr)) return "permission denied writing files \u2014 run from a directory you own, without sudo";
1132
- if (/ENOSPC|no space left/i.test(stderr)) return "no disk space left to install the package";
1133
- if (/\b401\b|EAUTH|authentication/i.test(stderr)) return "npm authentication failed \u2014 check ~/.npmrc";
1134
- if (typeof e.code === "number") return `npx exited with code ${e.code}`;
1135
- if (typeof e.code === "string") return e.code;
1136
- return e.message ?? "unknown error";
1137
- }
1138
- var GITIGNORE_ENTRIES = [
1139
- ".insforge",
1140
- ".agent",
1141
- ".agents",
1142
- ".augment",
1143
- ".claude",
1144
- ".cline",
1145
- ".github/copilot*",
1146
- ".kilocode",
1147
- ".qoder",
1148
- ".qwen",
1149
- ".roo",
1150
- ".trae",
1151
- ".windsurf"
1152
- ];
1153
- function updateGitignore() {
1154
- const gitignorePath = join2(process.cwd(), ".gitignore");
1155
- const existing = existsSync2(gitignorePath) ? readFileSync2(gitignorePath, "utf-8") : "";
1156
- const lines = new Set(existing.split("\n").map((l) => l.trim()));
1157
- const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.has(entry));
1158
- if (!missing.length) return;
1159
- const block = `
1160
- # InsForge & AI agent skills
1161
- ${missing.join("\n")}
1162
- `;
1163
- appendFileSync(gitignorePath, block);
1164
- }
1165
- async function installSkills(json) {
1166
- try {
1167
- if (!json) clack5.log.info("Installing InsForge agent skills (global)...");
1168
- await execAsync("npx skills add insforge/agent-skills -g -y -a antigravity -a augment -a claude-code -a cline -a codex -a cursor -a gemini-cli -a github-copilot -a kilo -a qoder -a qwen-code -a roo -a trae -a windsurf", {
1169
- cwd: process.cwd(),
1170
- timeout: SKILL_INSTALL_TIMEOUT_MS
1171
- });
1172
- if (!json) clack5.log.success("InsForge agent skills installed.");
1173
- } catch (err) {
1174
- if (!json) {
1175
- clack5.log.warn(`Could not install agent skills: ${describeExecError(err)}`);
1176
- clack5.log.info("Run `npx skills add insforge/agent-skills` once resolved to see the full output.");
1177
- }
1178
- }
1179
- try {
1180
- if (!json) clack5.log.info("Installing find-skills (global)...");
1181
- await execAsync("npx skills add https://github.com/vercel-labs/skills --skill find-skills -g -y", {
1182
- cwd: process.cwd(),
1183
- timeout: SKILL_INSTALL_TIMEOUT_MS
1184
- });
1185
- if (!json) clack5.log.success("find-skills installed.");
1186
- } catch (err) {
1187
- if (!json) {
1188
- clack5.log.warn(`Could not install find-skills: ${describeExecError(err)}`);
1189
- clack5.log.info("Run `npx skills add https://github.com/vercel-labs/skills --skill find-skills` once resolved.");
1190
- }
1191
- }
1192
- try {
1193
- updateGitignore();
1194
- } catch {
1195
- }
1196
- }
1197
- async function reportCliUsage(toolName, success, maxRetries = 1, explicitConfig) {
1198
- let config = explicitConfig;
1199
- if (!config) {
1200
- try {
1201
- config = getProjectConfig();
1202
- } catch {
1203
- return;
1204
- }
1205
- }
1206
- if (!config) return;
1207
- const payload = JSON.stringify({
1208
- tool_name: toolName,
1209
- success,
1210
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1211
- });
1212
- for (let attempt = 0; attempt < maxRetries; attempt++) {
1213
- try {
1214
- const controller = new AbortController();
1215
- const timer = setTimeout(() => controller.abort(), 3e3);
1216
- try {
1217
- const res = await fetch(`${config.oss_host}/api/usage/mcp`, {
1218
- method: "POST",
1219
- headers: {
1220
- "Content-Type": "application/json",
1221
- "x-api-key": config.api_key
1222
- },
1223
- body: payload,
1224
- signal: controller.signal
1225
- });
1226
- if (res.status < 500) return;
1227
- } finally {
1228
- clearTimeout(timer);
1229
- }
1230
- } catch {
1231
- }
1232
- if (attempt < maxRetries - 1) {
1233
- await new Promise((r) => setTimeout(r, 5e3));
1234
- }
1235
- }
1236
- }
1237
-
1238
1163
  // src/lib/analytics.ts
1239
1164
  import { PostHog } from "posthog-node";
1240
1165
  var POSTHOG_API_KEY = "phc_ueV1ii62wdBTkH7E70ugyeqHIHu8dFDdjs0qq3TZhJz";
@@ -1269,6 +1194,17 @@ function trackDiagnose(subcommand, config) {
1269
1194
  oss_mode: config.project_id === FAKE_PROJECT_ID
1270
1195
  });
1271
1196
  }
1197
+ function trackPayments(subcommand, config, properties) {
1198
+ captureEvent(config.project_id, "cli_payments_invoked", {
1199
+ subcommand,
1200
+ project_id: config.project_id,
1201
+ project_name: config.project_name,
1202
+ org_id: config.org_id,
1203
+ region: config.region,
1204
+ oss_mode: config.project_id === FAKE_PROJECT_ID,
1205
+ ...properties
1206
+ });
1207
+ }
1272
1208
  async function shutdownAnalytics() {
1273
1209
  try {
1274
1210
  if (client) await client.shutdown();
@@ -1276,172 +1212,982 @@ async function shutdownAnalytics() {
1276
1212
  }
1277
1213
  }
1278
1214
 
1279
- // src/commands/create.ts
1280
- import { exec as exec2 } from "child_process";
1281
- import { tmpdir } from "os";
1282
- import { promisify as promisify2 } from "util";
1283
- import * as fs3 from "fs/promises";
1284
- import * as path3 from "path";
1285
- import * as clack7 from "@clack/prompts";
1286
-
1287
- // src/lib/api/oss.ts
1288
- function requireProjectConfig() {
1289
- const config = getProjectConfig();
1290
- if (!config) {
1291
- throw new ProjectNotLinkedError();
1215
+ // src/commands/branch/switch.ts
1216
+ import { copyFileSync, existsSync as existsSync2, unlinkSync as unlinkSync2 } from "fs";
1217
+ async function runBranchSwitch(input) {
1218
+ await requireAuth(input.apiUrl);
1219
+ const current = getProjectConfig();
1220
+ if (!current) {
1221
+ throw new CLIError("No project linked. Run `insforge link` first.");
1222
+ }
1223
+ if (input.toParent && input.name) {
1224
+ throw new CLIError("Pass either a branch name or --parent, not both.");
1225
+ }
1226
+ const projectFile = getProjectConfigFile();
1227
+ const parentBackup = getParentBackupFile();
1228
+ if (input.toParent) {
1229
+ if (!existsSync2(parentBackup)) {
1230
+ throw new CLIError(
1231
+ "No parent backup found. Re-link the directory with `insforge link --project-id <parent>`."
1232
+ );
1233
+ }
1234
+ copyFileSync(parentBackup, projectFile);
1235
+ unlinkSync2(parentBackup);
1236
+ captureEvent(current.project_id, "cli_branch_switch", { direction: "to_parent" });
1237
+ if (!input.silent) {
1238
+ if (input.json) {
1239
+ outputJson({ switched: "parent" });
1240
+ } else {
1241
+ outputSuccess("Switched back to parent.");
1242
+ }
1243
+ }
1244
+ return;
1292
1245
  }
1293
- return config;
1294
- }
1295
- async function runRawSql(sql, unrestricted = false) {
1296
- const endpoint = unrestricted ? "/api/database/advance/rawsql/unrestricted" : "/api/database/advance/rawsql";
1297
- const res = await ossFetch(endpoint, {
1298
- method: "POST",
1299
- body: JSON.stringify({ query: sql })
1246
+ if (!input.name) {
1247
+ throw new CLIError("Branch name required (or pass --parent).");
1248
+ }
1249
+ const parentId = current.branched_from?.project_id ?? current.project_id;
1250
+ const branches = await listBranchesApi(parentId, input.apiUrl);
1251
+ const target = branches.find((b) => b.name === input.name);
1252
+ if (!target) {
1253
+ throw new CLIError(`Branch '${input.name}' not found.`);
1254
+ }
1255
+ if (target.branch_state !== "ready") {
1256
+ throw new CLIError(
1257
+ `Branch '${input.name}' is in state '${target.branch_state}', cannot switch.`
1258
+ );
1259
+ }
1260
+ if (!existsSync2(parentBackup)) {
1261
+ copyFileSync(projectFile, parentBackup);
1262
+ }
1263
+ const apiKey = await getProjectApiKey(target.id, input.apiUrl);
1264
+ const ossHost = `${target.appkey}.${target.region}.insforge.app`;
1265
+ const branched_from = current.branched_from ?? {
1266
+ project_id: current.project_id,
1267
+ project_name: current.project_name
1268
+ };
1269
+ saveProjectConfig({
1270
+ project_id: target.id,
1271
+ project_name: target.name,
1272
+ org_id: target.organization_id,
1273
+ appkey: target.appkey,
1274
+ region: target.region,
1275
+ api_key: apiKey,
1276
+ oss_host: ossHost,
1277
+ branched_from
1300
1278
  });
1301
- const raw = await res.json();
1302
- const rows = raw.rows ?? raw.data ?? [];
1303
- return { rows, raw };
1279
+ captureEvent(parentId, "cli_branch_switch", { direction: "to_branch" });
1280
+ if (!input.silent) {
1281
+ if (input.json) {
1282
+ outputJson({ switched: "branch", branch_id: target.id });
1283
+ } else {
1284
+ outputSuccess(`Switched to branch '${target.name}'.`);
1285
+ }
1286
+ }
1304
1287
  }
1305
- async function getAnonKey() {
1306
- const res = await ossFetch("/api/auth/tokens/anon", { method: "POST" });
1307
- const data = await res.json();
1308
- return data.accessToken;
1288
+ function registerBranchSwitchCommand(branch) {
1289
+ branch.command("switch [name]").description("Switch this directory's context to a branch (or back with --parent)").option("--parent", "Switch back to the parent project").action(async (name, opts, cmd) => {
1290
+ const { json, apiUrl } = getRootOpts(cmd);
1291
+ try {
1292
+ await runBranchSwitch({ name, toParent: opts.parent, apiUrl, json });
1293
+ } catch (err) {
1294
+ handleError(err, json);
1295
+ } finally {
1296
+ await shutdownAnalytics();
1297
+ }
1298
+ });
1309
1299
  }
1310
- async function ossFetch(path5, options = {}) {
1311
- const config = requireProjectConfig();
1312
- const headers = {
1313
- "Content-Type": "application/json",
1314
- Authorization: `Bearer ${config.api_key}`,
1315
- ...options.headers ?? {}
1316
- };
1317
- const res = await fetch(`${config.oss_host}${path5}`, { ...options, headers });
1318
- if (!res.ok) {
1319
- const err = await res.json().catch(() => ({}));
1320
- let message = err.message ?? err.error ?? `OSS request failed: ${res.status}`;
1321
- if (err.nextActions) {
1322
- message += `
1323
- ${err.nextActions}`;
1300
+
1301
+ // src/commands/branch/create.ts
1302
+ var POLL_INTERVAL_MS = 3e3;
1303
+ var POLL_TIMEOUT_MS = 5 * 60 * 1e3;
1304
+ function registerBranchCreateCommand(branch) {
1305
+ branch.command("create <name>").description("Create a branch from the currently linked project").option("--mode <mode>", "full | schema-only", "full").option("--no-switch", "Do not auto-switch context after creation").action(async (name, opts, cmd) => {
1306
+ const { json, apiUrl } = getRootOpts(cmd);
1307
+ try {
1308
+ await requireAuth(apiUrl);
1309
+ const project = getProjectConfig();
1310
+ if (!project) {
1311
+ throw new CLIError("No project linked. Run `insforge link` first.");
1312
+ }
1313
+ if (project.branched_from) {
1314
+ throw new CLIError(
1315
+ "This directory is currently switched to a branch. Run `insforge branch switch --parent` first, then create a new branch from the parent."
1316
+ );
1317
+ }
1318
+ if (opts.mode !== "full" && opts.mode !== "schema-only") {
1319
+ throw new CLIError(`Invalid --mode: ${opts.mode} (must be "full" or "schema-only")`);
1320
+ }
1321
+ const mode = opts.mode;
1322
+ const created = await createBranchApi(project.project_id, { mode, name }, apiUrl);
1323
+ captureEvent(project.project_id, "cli_branch_create", {
1324
+ mode,
1325
+ parent_project_id: project.project_id
1326
+ });
1327
+ if (!json) {
1328
+ outputSuccess(`Branch '${name}' created (appkey: ${created.appkey}). Provisioning\u2026`);
1329
+ }
1330
+ const ready = await pollUntilReady(created.id, apiUrl, !json);
1331
+ if (opts.switch && ready.branch_state === "ready") {
1332
+ await runBranchSwitch({ name, apiUrl, json, silent: json });
1333
+ }
1334
+ if (json) {
1335
+ outputJson({ branch: ready });
1336
+ } else if (ready.branch_state === "ready") {
1337
+ outputSuccess(`Branch '${name}' is ready.`);
1338
+ if (opts.switch) {
1339
+ outputInfo(
1340
+ "\u26A0 Re-source your dev server env (.env) to pick up the new INSFORGE_URL / ANON_KEY."
1341
+ );
1342
+ }
1343
+ } else {
1344
+ outputInfo(
1345
+ `Branch '${name}' is still in '${ready.branch_state}' state. Run \`insforge branch list\` to check.`
1346
+ );
1347
+ }
1348
+ } catch (err) {
1349
+ handleError(err, json);
1350
+ } finally {
1351
+ await shutdownAnalytics();
1324
1352
  }
1325
- const isRouteLevel404 = !err.error || err.error === "NOT_FOUND";
1326
- if (res.status === 404 && isRouteLevel404 && path5.startsWith("/api/compute")) {
1327
- message = "Compute services are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin to enable compute.";
1353
+ });
1354
+ }
1355
+ async function pollUntilReady(branchId, apiUrl, showProgress) {
1356
+ const start = Date.now();
1357
+ let lastState = "";
1358
+ while (Date.now() - start < POLL_TIMEOUT_MS) {
1359
+ const branch2 = await getBranchApi(branchId, apiUrl);
1360
+ if (branch2.branch_state === "ready") return branch2;
1361
+ if (branch2.branch_state === "deleted" || branch2.branch_state === "conflicted") {
1362
+ throw new CLIError(`Branch creation failed (state: ${branch2.branch_state})`);
1328
1363
  }
1329
- if (res.status === 404 && isRouteLevel404 && path5 === "/api/database/migrations") {
1330
- message = "Database migrations are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin about database migration support.";
1364
+ if (showProgress && branch2.branch_state !== lastState) {
1365
+ outputInfo(` state: ${branch2.branch_state}\u2026`);
1366
+ lastState = branch2.branch_state;
1331
1367
  }
1332
- throw new CLIError(message);
1368
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
1333
1369
  }
1334
- return res;
1370
+ const branch = await getBranchApi(branchId, apiUrl);
1371
+ if (branch.branch_state === "deleted" || branch.branch_state === "conflicted") {
1372
+ throw new CLIError(`Branch creation failed (state: ${branch.branch_state})`);
1373
+ }
1374
+ return branch;
1335
1375
  }
1336
1376
 
1337
- // src/lib/env.ts
1338
- import * as fs from "fs/promises";
1339
- import * as path from "path";
1340
- async function readEnvFile(cwd) {
1341
- const candidates = [".env.local", ".env.production", ".env"];
1342
- for (const name of candidates) {
1343
- const filePath = path.join(cwd, name);
1344
- const exists = await fs.stat(filePath).catch(() => null);
1345
- if (!exists) continue;
1346
- const content = await fs.readFile(filePath, "utf-8");
1347
- const vars = [];
1348
- for (const line of content.split("\n")) {
1349
- const trimmed = line.trim();
1350
- if (!trimmed || trimmed.startsWith("#")) continue;
1351
- const eqIndex = trimmed.indexOf("=");
1352
- if (eqIndex === -1) continue;
1353
- const key = trimmed.slice(0, eqIndex).trim();
1354
- let value = trimmed.slice(eqIndex + 1).trim();
1355
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1356
- value = value.slice(1, -1);
1377
+ // src/commands/branch/list.ts
1378
+ function registerBranchListCommand(branch) {
1379
+ branch.command("list").description("List branches of the currently linked project").action(async (_opts, cmd) => {
1380
+ const { json, apiUrl } = getRootOpts(cmd);
1381
+ try {
1382
+ await requireAuth(apiUrl);
1383
+ const project = getProjectConfig();
1384
+ if (!project) {
1385
+ throw new CLIError("No project linked. Run `insforge link` first.");
1357
1386
  }
1358
- if (key) vars.push({ key, value });
1387
+ const parentId = project.branched_from?.project_id ?? project.project_id;
1388
+ const branches = await listBranchesApi(parentId, apiUrl);
1389
+ captureEvent(parentId, "cli_branch_list", { count: branches.length });
1390
+ if (json) {
1391
+ outputJson({ data: branches });
1392
+ return;
1393
+ }
1394
+ if (branches.length === 0) {
1395
+ outputInfo("No branches.");
1396
+ return;
1397
+ }
1398
+ const currentBranchId = project.branched_from ? project.project_id : null;
1399
+ const rows = branches.map((b) => [
1400
+ b.id === currentBranchId ? "*" : " ",
1401
+ b.name,
1402
+ b.branch_state,
1403
+ b.branch_metadata?.mode ?? "?",
1404
+ new Date(b.branch_created_at).toLocaleString()
1405
+ ]);
1406
+ outputTable(["", "Name", "State", "Mode", "Created"], rows);
1407
+ } catch (err) {
1408
+ handleError(err, json);
1409
+ } finally {
1410
+ await shutdownAnalytics();
1359
1411
  }
1360
- return vars;
1361
- }
1362
- return [];
1412
+ });
1363
1413
  }
1364
1414
 
1365
- // src/commands/deployments/deploy.ts
1366
- import * as path2 from "path";
1367
- import * as fs2 from "fs/promises";
1368
- import { createReadStream } from "fs";
1369
- import { createHash as createHash2 } from "crypto";
1415
+ // src/commands/branch/merge.ts
1416
+ import { writeFileSync as writeFileSync2 } from "fs";
1417
+ import * as clack5 from "@clack/prompts";
1418
+ function registerBranchMergeCommand(branch) {
1419
+ branch.command("merge <name>").description("Merge a branch back to its parent project").option("--dry-run", "Compute the diff and print rendered SQL; do not apply").option("-y, --yes", "Skip confirmation prompt").option("--save-sql <path>", "Write rendered SQL preview to a file").action(async (name, opts, cmd) => {
1420
+ const { json, apiUrl } = getRootOpts(cmd);
1421
+ try {
1422
+ await requireAuth(apiUrl);
1423
+ const project = getProjectConfig();
1424
+ if (!project) throw new CLIError("No project linked. Run `insforge link` first.");
1425
+ const parentId = project.branched_from?.project_id ?? project.project_id;
1426
+ const branches = await listBranchesApi(parentId, apiUrl);
1427
+ const target = branches.find((b) => b.name === name);
1428
+ if (!target) throw new CLIError(`Branch '${name}' not found.`);
1429
+ const diff = await mergeBranchDryRunApi(target.id, apiUrl);
1430
+ if (opts.saveSql) {
1431
+ writeFileSync2(opts.saveSql, diff.rendered_sql);
1432
+ if (!json) outputInfo(`SQL preview saved to ${opts.saveSql}`);
1433
+ }
1434
+ if (!json) {
1435
+ console.log(diff.rendered_sql);
1436
+ console.log();
1437
+ outputInfo(
1438
+ `${diff.summary.added} added, ${diff.summary.modified} modified, ${diff.summary.conflicts} conflict(s).`
1439
+ );
1440
+ }
1441
+ if (diff.summary.conflicts > 0) {
1442
+ captureEvent(parentId, "cli_branch_merge_conflict", {
1443
+ conflicts: diff.summary.conflicts
1444
+ });
1445
+ if (json) {
1446
+ outputJson({
1447
+ diff,
1448
+ applied: false,
1449
+ dryRun: !!opts.dryRun,
1450
+ error: "merge_conflict"
1451
+ });
1452
+ } else {
1453
+ outputInfo("");
1454
+ outputInfo("Merge blocked: resolve conflicts before retrying.");
1455
+ for (const c of diff.conflicts) {
1456
+ outputInfo(` - ${c.schema}.${c.object} [${c.type}] \u2014 ${c.hint}`);
1457
+ }
1458
+ }
1459
+ process.exit(2);
1460
+ }
1461
+ if (opts.dryRun) {
1462
+ captureEvent(parentId, "cli_branch_merge", {
1463
+ dry_run: true,
1464
+ conflicts: 0,
1465
+ applied: false
1466
+ });
1467
+ if (json) {
1468
+ outputJson({ diff, applied: false, dryRun: true });
1469
+ }
1470
+ return;
1471
+ }
1472
+ if (!opts.yes && !json) {
1473
+ const parentLabel = project.branched_from?.project_name ?? project.project_name;
1474
+ const confirmed = await clack5.confirm({
1475
+ message: `Apply this merge to parent project '${parentLabel}'?`
1476
+ });
1477
+ if (clack5.isCancel(confirmed) || !confirmed) {
1478
+ outputInfo("Merge cancelled.");
1479
+ return;
1480
+ }
1481
+ }
1482
+ const result = await mergeBranchExecuteApi(target.id, apiUrl);
1483
+ if (!result.ok) {
1484
+ captureEvent(parentId, "cli_branch_merge_conflict", {
1485
+ conflicts: result.conflict.diff.summary.conflicts
1486
+ });
1487
+ if (json) {
1488
+ outputJson({
1489
+ diff: result.conflict.diff,
1490
+ applied: false,
1491
+ dryRun: false,
1492
+ error: "merge_conflict"
1493
+ });
1494
+ } else {
1495
+ outputInfo("Merge blocked by a conflict that appeared between dry-run and apply:");
1496
+ for (const c of result.conflict.diff.conflicts) {
1497
+ outputInfo(` - ${c.schema}.${c.object} [${c.type}] \u2014 ${c.hint}`);
1498
+ }
1499
+ }
1500
+ process.exit(2);
1501
+ }
1502
+ captureEvent(parentId, "cli_branch_merge", {
1503
+ dry_run: false,
1504
+ conflicts: 0,
1505
+ applied: true
1506
+ });
1507
+ if (json) {
1508
+ outputJson({ ...result.result, diff, applied: true, dryRun: false });
1509
+ } else {
1510
+ outputSuccess(`Merged. Branch '${name}' is now in 'merged' state.`);
1511
+ outputInfo("\u26A0 Reminder: redeploy edge functions, website, and compute as needed.");
1512
+ }
1513
+ } catch (err) {
1514
+ handleError(err, json);
1515
+ } finally {
1516
+ await shutdownAnalytics();
1517
+ }
1518
+ });
1519
+ }
1520
+
1521
+ // src/commands/branch/reset.ts
1370
1522
  import * as clack6 from "@clack/prompts";
1371
- import archiver from "archiver";
1372
- var POLL_INTERVAL_MS = 5e3;
1373
- var POLL_TIMEOUT_MS = 3e5;
1374
- var DIRECT_UPLOAD_CONCURRENCY = 8;
1375
- var EXCLUDE_PATTERNS = [
1376
- "node_modules",
1377
- ".git",
1378
- ".next",
1379
- ".env",
1380
- ".env.local",
1381
- "dist",
1382
- "build",
1383
- ".DS_Store",
1523
+ var POLL_INTERVAL_MS2 = 3e3;
1524
+ var POLL_TIMEOUT_MS2 = 5 * 60 * 1e3;
1525
+ function registerBranchResetCommand(branch) {
1526
+ branch.command("reset <name>").description("Reset a branch's database back to T0 (the parent snapshot at branch creation)").option("-y, --yes", "Skip confirmation").action(async (name, opts, cmd) => {
1527
+ const { json, apiUrl } = getRootOpts(cmd);
1528
+ try {
1529
+ await requireAuth(apiUrl);
1530
+ const project = getProjectConfig();
1531
+ if (!project) throw new CLIError("No project linked. Run `insforge link` first.");
1532
+ const parentId = project.branched_from?.project_id ?? project.project_id;
1533
+ const branches = await listBranchesApi(parentId, apiUrl);
1534
+ const target = branches.find((b) => b.name === name);
1535
+ if (!target) throw new CLIError(`Branch '${name}' not found.`);
1536
+ if (target.branch_state !== "ready" && target.branch_state !== "merged") {
1537
+ throw new CLIError(
1538
+ `Branch '${name}' is in '${target.branch_state}' state; reset requires 'ready' or 'merged'.`
1539
+ );
1540
+ }
1541
+ const entryState = target.branch_state;
1542
+ if (!opts.yes && !json) {
1543
+ const confirmed = await clack6.confirm({
1544
+ message: `Reset branch '${name}' back to T0? This wipes all schema/data/policy/function/migration changes made on the branch since creation.` + (entryState === "merged" ? " (Branch is currently merged \u2014 reset will reopen it for further work.)" : "")
1545
+ });
1546
+ if (clack6.isCancel(confirmed) || !confirmed) {
1547
+ outputInfo("Cancelled.");
1548
+ return;
1549
+ }
1550
+ }
1551
+ const initial = await resetBranchApi(target.id, apiUrl);
1552
+ captureEvent(parentId, "cli_branch_reset", {
1553
+ entry_state: entryState,
1554
+ mode: target.branch_metadata?.mode
1555
+ });
1556
+ if (!json) {
1557
+ outputSuccess(`Reset enqueued for branch '${name}'. Restoring T0\u2026`);
1558
+ }
1559
+ const final = await pollUntilReady2(target.id, apiUrl, !json, initial.branch_state);
1560
+ if (json) {
1561
+ outputJson({ branch: final });
1562
+ } else if (final.branch_state === "ready") {
1563
+ outputSuccess(`Branch '${name}' is back to T0 and ready.`);
1564
+ outputInfo("\u26A0 Reminder: edge functions, website, and compute aren\u2019t touched by reset; redeploy if needed.");
1565
+ } else {
1566
+ outputInfo(
1567
+ `Branch '${name}' is still in '${final.branch_state}' state. Run \`insforge branch list\` to check.`
1568
+ );
1569
+ }
1570
+ } catch (err) {
1571
+ handleError(err, json);
1572
+ } finally {
1573
+ await shutdownAnalytics();
1574
+ }
1575
+ });
1576
+ }
1577
+ async function pollUntilReady2(branchId, apiUrl, showProgress, startingState) {
1578
+ const start = Date.now();
1579
+ let lastState = startingState;
1580
+ if (showProgress) outputInfo(` state: ${startingState}\u2026`);
1581
+ while (Date.now() - start < POLL_TIMEOUT_MS2) {
1582
+ const branch2 = await getBranchApi(branchId, apiUrl);
1583
+ if (branch2.branch_state === "ready") return branch2;
1584
+ if (branch2.branch_state === "merged") return branch2;
1585
+ if (branch2.branch_state === "deleted" || branch2.branch_state === "conflicted") {
1586
+ throw new CLIError(`Branch reset failed (state: ${branch2.branch_state})`);
1587
+ }
1588
+ if (showProgress && branch2.branch_state !== lastState) {
1589
+ outputInfo(` state: ${branch2.branch_state}\u2026`);
1590
+ lastState = branch2.branch_state;
1591
+ }
1592
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
1593
+ }
1594
+ const branch = await getBranchApi(branchId, apiUrl);
1595
+ if (branch.branch_state === "deleted" || branch.branch_state === "conflicted") {
1596
+ throw new CLIError(`Branch reset failed (state: ${branch.branch_state})`);
1597
+ }
1598
+ return branch;
1599
+ }
1600
+
1601
+ // src/commands/branch/delete.ts
1602
+ import * as clack7 from "@clack/prompts";
1603
+ function registerBranchDeleteCommand(branch) {
1604
+ branch.command("delete <name>").description("Delete a branch").option("-y, --yes", "Skip confirmation").action(async (name, opts, cmd) => {
1605
+ const { json, apiUrl } = getRootOpts(cmd);
1606
+ try {
1607
+ await requireAuth(apiUrl);
1608
+ const project = getProjectConfig();
1609
+ if (!project) throw new CLIError("No project linked. Run `insforge link` first.");
1610
+ const parentId = project.branched_from?.project_id ?? project.project_id;
1611
+ const branches = await listBranchesApi(parentId, apiUrl);
1612
+ const target = branches.find((b) => b.name === name);
1613
+ if (!target) throw new CLIError(`Branch '${name}' not found.`);
1614
+ if (!opts.yes && !json) {
1615
+ const confirmed = await clack7.confirm({
1616
+ message: `Delete branch '${name}'? This terminates its EC2 instance.`
1617
+ });
1618
+ if (clack7.isCancel(confirmed) || !confirmed) {
1619
+ outputInfo("Cancelled.");
1620
+ return;
1621
+ }
1622
+ }
1623
+ await deleteBranchApi(target.id, apiUrl);
1624
+ captureEvent(parentId, "cli_branch_delete", {});
1625
+ const currentlyOnDeleted = project.project_id === target.id;
1626
+ if (currentlyOnDeleted) {
1627
+ try {
1628
+ await runBranchSwitch({ toParent: true, apiUrl, json, silent: json });
1629
+ } catch (err) {
1630
+ outputInfo(
1631
+ `Switched-to-parent failed (${err.message}). Run \`insforge branch switch --parent\` manually.`
1632
+ );
1633
+ }
1634
+ }
1635
+ if (json) {
1636
+ outputJson({ deleted: true, branch_id: target.id, switched_back: currentlyOnDeleted });
1637
+ } else {
1638
+ outputSuccess(`Branch '${name}' deletion enqueued.`);
1639
+ if (currentlyOnDeleted) outputInfo("Switched back to parent.");
1640
+ }
1641
+ } catch (err) {
1642
+ handleError(err, json);
1643
+ } finally {
1644
+ await shutdownAnalytics();
1645
+ }
1646
+ });
1647
+ }
1648
+
1649
+ // src/commands/branch/index.ts
1650
+ function registerBranchCommands(program2) {
1651
+ const branch = program2.command("branch").description("Manage backend branches");
1652
+ registerBranchCreateCommand(branch);
1653
+ registerBranchListCommand(branch);
1654
+ registerBranchSwitchCommand(branch);
1655
+ registerBranchMergeCommand(branch);
1656
+ registerBranchResetCommand(branch);
1657
+ registerBranchDeleteCommand(branch);
1658
+ }
1659
+
1660
+ // src/commands/projects/link.ts
1661
+ import { exec as exec3 } from "child_process";
1662
+ import { promisify as promisify4 } from "util";
1663
+ import * as fs5 from "fs/promises";
1664
+ import * as path5 from "path";
1665
+ import * as clack12 from "@clack/prompts";
1666
+ import pc2 from "picocolors";
1667
+
1668
+ // src/lib/skills.ts
1669
+ import { exec } from "child_process";
1670
+ import { existsSync as existsSync3, readFileSync as readFileSync2, appendFileSync } from "fs";
1671
+ import { join as join2 } from "path";
1672
+ import { promisify } from "util";
1673
+ import * as clack8 from "@clack/prompts";
1674
+ var execAsync = promisify(exec);
1675
+ var SKILL_INSTALL_TIMEOUT_MS = 6e4;
1676
+ function describeExecError(err) {
1677
+ const e = err;
1678
+ if (e.killed && (e.signal === "SIGTERM" || e.signal === "SIGKILL")) {
1679
+ return `timed out after ${SKILL_INSTALL_TIMEOUT_MS / 1e3}s \u2014 the npm registry may be slow or blocked by your network`;
1680
+ }
1681
+ if (e.code === "ENOENT") {
1682
+ return "`npx` is not on your PATH \u2014 install Node.js 18+ and reopen your shell";
1683
+ }
1684
+ const stderr = (typeof e.stderr === "string" ? e.stderr : e.stderr?.toString()) ?? "";
1685
+ if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(stderr)) return "cannot reach the npm registry (DNS lookup failed) \u2014 check your internet connection";
1686
+ if (/ECONNREFUSED/i.test(stderr)) return "connection to the npm registry was refused \u2014 a proxy or firewall is likely blocking it";
1687
+ if (/ETIMEDOUT|ESOCKETTIMEDOUT|network timeout/i.test(stderr)) return "the npm registry timed out \u2014 check your VPN, proxy, or corporate network";
1688
+ if (/CERT_HAS_EXPIRED|UNABLE_TO_VERIFY_LEAF_SIGNATURE|SELF_SIGNED_CERT/i.test(stderr)) return "TLS error reaching the npm registry \u2014 a corporate proxy may be intercepting HTTPS";
1689
+ if (/\bE404\b|404 Not Found/i.test(stderr)) return "npm returned 404 \u2014 the `skills` package or a dependency could not be found (check your npm registry config)";
1690
+ if (/EACCES|permission denied/i.test(stderr)) return "permission denied writing files \u2014 run from a directory you own, without sudo";
1691
+ if (/ENOSPC|no space left/i.test(stderr)) return "no disk space left to install the package";
1692
+ if (/\b401\b|EAUTH|authentication/i.test(stderr)) return "npm authentication failed \u2014 check ~/.npmrc";
1693
+ if (typeof e.code === "number") return `npx exited with code ${e.code}`;
1694
+ if (typeof e.code === "string") return e.code;
1695
+ return e.message ?? "unknown error";
1696
+ }
1697
+ var GITIGNORE_ENTRIES = [
1384
1698
  ".insforge",
1385
- // IDE and AI agent configs
1386
- ".claude",
1699
+ ".agent",
1387
1700
  ".agents",
1388
1701
  ".augment",
1702
+ ".claude",
1703
+ ".cline",
1704
+ ".github/copilot*",
1389
1705
  ".kilocode",
1390
- ".kiro",
1391
1706
  ".qoder",
1392
1707
  ".qwen",
1393
1708
  ".roo",
1394
1709
  ".trae",
1395
- ".windsurf",
1396
- ".vercel",
1397
- ".turbo",
1398
- ".cache",
1399
- "skills",
1400
- "coverage"
1710
+ ".windsurf"
1401
1711
  ];
1402
- var DirectDeploymentUnsupportedError = class extends Error {
1403
- constructor() {
1404
- super("Direct deployment endpoints are not available on this backend");
1405
- this.name = "DirectDeploymentUnsupportedError";
1712
+ function updateGitignore() {
1713
+ const gitignorePath = join2(process.cwd(), ".gitignore");
1714
+ const existing = existsSync3(gitignorePath) ? readFileSync2(gitignorePath, "utf-8") : "";
1715
+ const lines = new Set(existing.split("\n").map((l) => l.trim()));
1716
+ const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.has(entry));
1717
+ if (!missing.length) return;
1718
+ const block = `
1719
+ # InsForge & AI agent skills
1720
+ ${missing.join("\n")}
1721
+ `;
1722
+ appendFileSync(gitignorePath, block);
1723
+ }
1724
+ async function installSkills(json) {
1725
+ try {
1726
+ if (!json) clack8.log.info("Installing InsForge agent skills (global)...");
1727
+ await execAsync("npx skills add insforge/agent-skills -g -y -a antigravity -a augment -a claude-code -a cline -a codex -a cursor -a gemini-cli -a github-copilot -a kilo -a qoder -a qwen-code -a roo -a trae -a windsurf", {
1728
+ cwd: process.cwd(),
1729
+ timeout: SKILL_INSTALL_TIMEOUT_MS
1730
+ });
1731
+ if (!json) clack8.log.success("InsForge agent skills installed.");
1732
+ } catch (err) {
1733
+ if (!json) {
1734
+ clack8.log.warn(`Could not install agent skills: ${describeExecError(err)}`);
1735
+ clack8.log.info("Run `npx skills add insforge/agent-skills` once resolved to see the full output.");
1736
+ }
1406
1737
  }
1407
- };
1408
- function shouldExclude(name) {
1409
- const normalized = name.replace(/\\/g, "/");
1410
- for (const pattern of EXCLUDE_PATTERNS) {
1411
- if (normalized === pattern || normalized.startsWith(pattern + "/") || normalized.endsWith("/" + pattern) || normalized.includes("/" + pattern + "/")) {
1412
- return true;
1738
+ try {
1739
+ if (!json) clack8.log.info("Installing find-skills (global)...");
1740
+ await execAsync("npx skills add https://github.com/vercel-labs/skills --skill find-skills -g -y", {
1741
+ cwd: process.cwd(),
1742
+ timeout: SKILL_INSTALL_TIMEOUT_MS
1743
+ });
1744
+ if (!json) clack8.log.success("find-skills installed.");
1745
+ } catch (err) {
1746
+ if (!json) {
1747
+ clack8.log.warn(`Could not install find-skills: ${describeExecError(err)}`);
1748
+ clack8.log.info("Run `npx skills add https://github.com/vercel-labs/skills --skill find-skills` once resolved.");
1413
1749
  }
1414
1750
  }
1415
- if (normalized.endsWith(".log")) return true;
1416
- return false;
1417
- }
1418
- function isInsforgeCloudOssHost(ossHost) {
1419
1751
  try {
1420
- return new URL(ossHost).hostname.endsWith(".insforge.app");
1752
+ updateGitignore();
1421
1753
  } catch {
1422
- return false;
1423
- }
1424
- }
1425
- function normalizeRelativePath(sourceDir, absolutePath) {
1426
- return path2.relative(sourceDir, absolutePath).split(path2.sep).join("/").replace(/\\/g, "/");
1427
- }
1428
- async function hashFile(filePath) {
1429
- const hash = createHash2("sha1");
1430
- let size = 0;
1431
- for await (const chunk of createReadStream(filePath)) {
1432
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1433
- size += buffer.length;
1434
- hash.update(buffer);
1435
1754
  }
1436
- return { sha: hash.digest("hex"), size };
1755
+ }
1756
+ async function reportCliUsage(toolName, success, maxRetries = 1, explicitConfig) {
1757
+ let config = explicitConfig;
1758
+ if (!config) {
1759
+ try {
1760
+ config = getProjectConfig();
1761
+ } catch {
1762
+ return;
1763
+ }
1764
+ }
1765
+ if (!config) return;
1766
+ const payload = JSON.stringify({
1767
+ tool_name: toolName,
1768
+ success,
1769
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1770
+ });
1771
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
1772
+ try {
1773
+ const controller = new AbortController();
1774
+ const timer = setTimeout(() => controller.abort(), 3e3);
1775
+ try {
1776
+ const res = await fetch(`${config.oss_host}/api/usage/mcp`, {
1777
+ method: "POST",
1778
+ headers: {
1779
+ "Content-Type": "application/json",
1780
+ "x-api-key": config.api_key
1781
+ },
1782
+ body: payload,
1783
+ signal: controller.signal
1784
+ });
1785
+ if (res.status < 500) return;
1786
+ } finally {
1787
+ clearTimeout(timer);
1788
+ }
1789
+ } catch {
1790
+ }
1791
+ if (attempt < maxRetries - 1) {
1792
+ await new Promise((r) => setTimeout(r, 5e3));
1793
+ }
1794
+ }
1795
+ }
1796
+
1797
+ // src/auth-providers/apply.ts
1798
+ import { promises as fs } from "fs";
1799
+ import * as path from "path";
1800
+ import { tmpdir } from "os";
1801
+ import { execFile } from "child_process";
1802
+ import { promisify as promisify2 } from "util";
1803
+ import { randomBytes as randomBytes2 } from "crypto";
1804
+ import * as clack9 from "@clack/prompts";
1805
+
1806
+ // src/lib/api/oss.ts
1807
+ function requireProjectConfig() {
1808
+ const config = getProjectConfig();
1809
+ if (!config) {
1810
+ throw new ProjectNotLinkedError();
1811
+ }
1812
+ return config;
1813
+ }
1814
+ async function runRawSql(sql, unrestricted = false) {
1815
+ const endpoint = unrestricted ? "/api/database/advance/rawsql/unrestricted" : "/api/database/advance/rawsql";
1816
+ const res = await ossFetch(endpoint, {
1817
+ method: "POST",
1818
+ body: JSON.stringify({ query: sql })
1819
+ });
1820
+ const raw = await res.json();
1821
+ const rows = raw.rows ?? raw.data ?? [];
1822
+ return { rows, raw };
1823
+ }
1824
+ async function getAnonKey() {
1825
+ const res = await ossFetch("/api/auth/tokens/anon", { method: "POST" });
1826
+ const data = await res.json();
1827
+ return data.accessToken;
1828
+ }
1829
+ async function getJwtSecret() {
1830
+ try {
1831
+ const res = await ossFetch("/api/secrets/JWT_SECRET");
1832
+ const data = await res.json();
1833
+ return typeof data.value === "string" && data.value.length > 0 ? data.value : null;
1834
+ } catch {
1835
+ return null;
1836
+ }
1837
+ }
1838
+ async function ossFetch(path6, options = {}) {
1839
+ const config = requireProjectConfig();
1840
+ const headers = {
1841
+ "Content-Type": "application/json",
1842
+ Authorization: `Bearer ${config.api_key}`,
1843
+ ...options.headers ?? {}
1844
+ };
1845
+ const res = await fetch(`${config.oss_host}${path6}`, { ...options, headers });
1846
+ if (!res.ok) {
1847
+ const err = await res.json().catch(() => ({}));
1848
+ let message = err.message ?? err.error ?? `OSS request failed: ${res.status}`;
1849
+ if (err.nextActions) {
1850
+ message += `
1851
+ ${err.nextActions}`;
1852
+ }
1853
+ const isRouteLevel404 = !err.error || err.error === "NOT_FOUND";
1854
+ if (res.status === 404 && isRouteLevel404 && path6.startsWith("/api/compute")) {
1855
+ message = "Compute services are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin to enable compute.";
1856
+ }
1857
+ if (res.status === 404 && isRouteLevel404 && path6.startsWith("/api/payments")) {
1858
+ message = "Payments are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud/private preview: contact your InsForge admin to enable payments.";
1859
+ }
1860
+ if (res.status === 404 && isRouteLevel404 && path6 === "/api/database/migrations") {
1861
+ message = "Database migrations are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin about database migration support.";
1862
+ }
1863
+ throw new CLIError(message);
1864
+ }
1865
+ return res;
1866
+ }
1867
+
1868
+ // src/auth-providers/apply.ts
1869
+ var execFileAsync = promisify2(execFile);
1870
+ var VALID_AUTH_PROVIDERS = ["better-auth"];
1871
+ function pathExists(p) {
1872
+ return fs.stat(p).then(() => true, () => false);
1873
+ }
1874
+ function deepMergeKeepBase(base, patch) {
1875
+ const out = { ...base };
1876
+ for (const [k, v] of Object.entries(patch)) {
1877
+ if (v && typeof v === "object" && !Array.isArray(v) && out[k] && typeof out[k] === "object" && !Array.isArray(out[k])) {
1878
+ out[k] = deepMergeKeepBase(out[k], v);
1879
+ } else if (out[k] === void 0) {
1880
+ out[k] = v;
1881
+ }
1882
+ }
1883
+ return out;
1884
+ }
1885
+ function extractEnvKeys(content) {
1886
+ const keys = /* @__PURE__ */ new Set();
1887
+ for (const line of content.split("\n")) {
1888
+ const trimmed = line.replace(/^\s*export\s+/, "").trimStart();
1889
+ if (!trimmed || trimmed.startsWith("#")) continue;
1890
+ const m = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
1891
+ if (m) keys.add(m[1]);
1892
+ }
1893
+ return keys;
1894
+ }
1895
+ function filterCollidingEnvLines(append, existingKeys) {
1896
+ const dropped = [];
1897
+ const out = [];
1898
+ for (const line of append.split("\n")) {
1899
+ const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
1900
+ if (m && existingKeys.has(m[1])) {
1901
+ dropped.push(m[1]);
1902
+ continue;
1903
+ }
1904
+ out.push(line);
1905
+ }
1906
+ return { filtered: out.join("\n"), dropped };
1907
+ }
1908
+ async function walkFiles(dir, base = dir) {
1909
+ const out = [];
1910
+ for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
1911
+ const full = path.join(dir, entry.name);
1912
+ if (entry.isDirectory()) out.push(...await walkFiles(full, base));
1913
+ else out.push(path.relative(base, full));
1914
+ }
1915
+ return out;
1916
+ }
1917
+ var PROVIDER_META_FILES = /* @__PURE__ */ new Set(["manifest.json", "README.md"]);
1918
+ var SAFE_REPO_PATTERN = /^(https?:\/\/|git@)[A-Za-z0-9._:/@~+-]+(\.git)?$/;
1919
+ var SAFE_BRANCH_PATTERN = /^[A-Za-z0-9._/-]+$/;
1920
+ async function fetchProviderTree(provider) {
1921
+ const tempDir = path.join(tmpdir(), `insforge-auth-${provider}-${Date.now()}`);
1922
+ await fs.mkdir(tempDir, { recursive: true });
1923
+ const cleanup = () => fs.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
1924
+ try {
1925
+ const repo = process.env.INSFORGE_TEMPLATES_REPO ?? "https://github.com/InsForge/insforge-templates.git";
1926
+ if (!SAFE_REPO_PATTERN.test(repo)) {
1927
+ throw new Error(`INSFORGE_TEMPLATES_REPO has unsupported characters: ${repo}`);
1928
+ }
1929
+ const branch = process.env.INSFORGE_TEMPLATES_BRANCH;
1930
+ if (branch !== void 0 && !SAFE_BRANCH_PATTERN.test(branch)) {
1931
+ throw new Error(`INSFORGE_TEMPLATES_BRANCH has unsupported characters: ${branch}`);
1932
+ }
1933
+ const args = ["clone", "--depth", "1"];
1934
+ if (branch) args.push("-b", branch);
1935
+ args.push("--", repo, ".");
1936
+ await execFileAsync("git", args, {
1937
+ cwd: tempDir,
1938
+ maxBuffer: 10 * 1024 * 1024,
1939
+ timeout: 6e4
1940
+ });
1941
+ const providerDir = path.join(tempDir, "auth-providers", provider);
1942
+ if (!await pathExists(providerDir)) {
1943
+ await cleanup();
1944
+ throw new Error(
1945
+ `Auth provider "${provider}" not found in templates repo. Looked for auth-providers/${provider}/ in ${repo}${branch ? ` (branch: ${branch})` : ""}.`
1946
+ );
1947
+ }
1948
+ return { dir: providerDir, cleanup };
1949
+ } catch (err) {
1950
+ await cleanup();
1951
+ throw err;
1952
+ }
1953
+ }
1954
+ async function loadManifest(providerDir) {
1955
+ const manifestPath = path.join(providerDir, "manifest.json");
1956
+ if (!await pathExists(manifestPath)) {
1957
+ throw new Error(`Missing manifest.json in ${providerDir}`);
1958
+ }
1959
+ const parsed = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
1960
+ const missing = [];
1961
+ if (typeof parsed.name !== "string") missing.push("name");
1962
+ if (!Array.isArray(parsed.files)) missing.push("files (array)");
1963
+ if (!parsed.packageJsonPatch || typeof parsed.packageJsonPatch !== "object") missing.push("packageJsonPatch (object)");
1964
+ if (typeof parsed.envExampleAppend !== "string") missing.push("envExampleAppend (string)");
1965
+ if (typeof parsed.nextSteps !== "string") missing.push("nextSteps (string)");
1966
+ if (missing.length > 0) {
1967
+ throw new Error(`Malformed manifest.json in ${providerDir} \u2014 missing or wrong-typed fields: ${missing.join(", ")}`);
1968
+ }
1969
+ return parsed;
1970
+ }
1971
+ async function applyAuthProvider(provider, cwd, projectConfig, json) {
1972
+ if (!VALID_AUTH_PROVIDERS.includes(provider)) {
1973
+ throw new Error(`Unknown auth provider: ${provider}`);
1974
+ }
1975
+ const fetchSpinner = !json ? clack9.spinner() : null;
1976
+ fetchSpinner?.start(`Fetching ${provider} scaffold from templates repo...`);
1977
+ const { dir: providerDir, cleanup } = await fetchProviderTree(provider);
1978
+ fetchSpinner?.stop(`${provider} scaffold ready`);
1979
+ try {
1980
+ const manifest = await loadManifest(providerDir);
1981
+ const result = {
1982
+ written: [],
1983
+ skipped: [],
1984
+ overwritten: [],
1985
+ packageJsonPatched: false,
1986
+ envExampleAppended: false,
1987
+ envLocalWritten: false,
1988
+ envKeysSkipped: [],
1989
+ nextSteps: manifest.nextSteps
1990
+ };
1991
+ const allFiles = (await walkFiles(providerDir)).filter((rel) => !PROVIDER_META_FILES.has(rel));
1992
+ for (const rel of allFiles) {
1993
+ const src = path.join(providerDir, rel);
1994
+ const dest = path.join(cwd, rel);
1995
+ const existed = await pathExists(dest);
1996
+ await fs.mkdir(path.dirname(dest), { recursive: true });
1997
+ await fs.copyFile(src, dest);
1998
+ if (existed) result.overwritten.push(rel);
1999
+ else result.written.push(rel);
2000
+ }
2001
+ const pkgPath = path.join(cwd, "package.json");
2002
+ if (await pathExists(pkgPath)) {
2003
+ const existing = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
2004
+ const merged = deepMergeKeepBase(existing, manifest.packageJsonPatch);
2005
+ await fs.writeFile(pkgPath, JSON.stringify(merged, null, 2) + "\n");
2006
+ result.packageJsonPatched = true;
2007
+ } else {
2008
+ const fresh = {
2009
+ name: path.basename(cwd),
2010
+ version: "0.0.1",
2011
+ private: true,
2012
+ ...manifest.packageJsonPatch
2013
+ };
2014
+ await fs.writeFile(pkgPath, JSON.stringify(fresh, null, 2) + "\n");
2015
+ result.packageJsonPatched = true;
2016
+ }
2017
+ const envExamplePath = path.join(cwd, ".env.example");
2018
+ if (await pathExists(envExamplePath)) {
2019
+ const existing = await fs.readFile(envExamplePath, "utf-8");
2020
+ if (!existing.includes("# \u2500\u2500\u2500 Better Auth + InsForge bridge")) {
2021
+ const existingKeys = extractEnvKeys(existing);
2022
+ const { filtered, dropped } = filterCollidingEnvLines(manifest.envExampleAppend, existingKeys);
2023
+ result.envKeysSkipped = dropped;
2024
+ await fs.writeFile(envExamplePath, existing.replace(/\n*$/, "\n\n") + filtered + "\n");
2025
+ result.envExampleAppended = true;
2026
+ }
2027
+ } else {
2028
+ await fs.writeFile(envExamplePath, manifest.envExampleAppend + "\n");
2029
+ result.envExampleAppended = true;
2030
+ }
2031
+ const envLocalPath = path.join(cwd, ".env.local");
2032
+ const envLocalExists = await pathExists(envLocalPath);
2033
+ const existingLocal = envLocalExists ? await fs.readFile(envLocalPath, "utf-8") : "";
2034
+ const existingLocalKeys = envLocalExists ? extractEnvKeys(existingLocal) : /* @__PURE__ */ new Set();
2035
+ const anonKey = await getAnonKey();
2036
+ const jwtSecret = await getJwtSecret();
2037
+ const filled = manifest.envExampleAppend.replace(
2038
+ /^([A-Z][A-Z0-9_]*=)(.*)$/gm,
2039
+ (_, prefix, value) => {
2040
+ const key = prefix.slice(0, -1);
2041
+ if (/INSFORGE.*(URL|BASE_URL)$/.test(key)) return `${prefix}${projectConfig.oss_host}`;
2042
+ if (/INSFORGE.*ANON_KEY$/.test(key)) return `${prefix}${anonKey}`;
2043
+ if (key === "NEXT_PUBLIC_APP_URL") return `${prefix}https://${projectConfig.appkey}.insforge.site`;
2044
+ if (/JWT_SECRET$/.test(key)) return `${prefix}${jwtSecret ?? value}`;
2045
+ if (key === "BETTER_AUTH_SECRET") return `${prefix}${randomBytes2(32).toString("hex")}`;
2046
+ return `${prefix}${value}`;
2047
+ }
2048
+ );
2049
+ if (!envLocalExists) {
2050
+ await fs.writeFile(envLocalPath, filled + "\n");
2051
+ result.envLocalWritten = true;
2052
+ } else {
2053
+ const { filtered, dropped } = filterCollidingEnvLines(filled, existingLocalKeys);
2054
+ const hasNewKey = filtered.split("\n").some((l) => /^[A-Z][A-Z0-9_]*=/.test(l));
2055
+ if (hasNewKey) {
2056
+ await fs.writeFile(envLocalPath, existingLocal.replace(/\n*$/, "\n\n") + filtered + "\n");
2057
+ result.envLocalWritten = true;
2058
+ }
2059
+ result.envKeysSkipped = Array.from(/* @__PURE__ */ new Set([...result.envKeysSkipped, ...dropped]));
2060
+ }
2061
+ if (!jwtSecret && !json) {
2062
+ clack9.log.warn("Could not auto-fill JWT_SECRET \u2014 run `npx @insforge/cli secrets get JWT_SECRET` and paste it into .env.local.");
2063
+ }
2064
+ if (result.envKeysSkipped.length > 0 && !json) {
2065
+ clack9.log.warn(
2066
+ `Kept your existing values for: ${result.envKeysSkipped.join(", ")}. If any of these need the auth-provider's defaults, see .env.example for reference.`
2067
+ );
2068
+ }
2069
+ return result;
2070
+ } finally {
2071
+ await cleanup();
2072
+ }
2073
+ }
2074
+
2075
+ // src/commands/create.ts
2076
+ import { exec as exec2, execFile as execFile2 } from "child_process";
2077
+ import { tmpdir as tmpdir2 } from "os";
2078
+ import { promisify as promisify3 } from "util";
2079
+ import * as fs4 from "fs/promises";
2080
+ import * as path4 from "path";
2081
+ import * as clack11 from "@clack/prompts";
2082
+
2083
+ // src/lib/env.ts
2084
+ import * as fs2 from "fs/promises";
2085
+ import * as path2 from "path";
2086
+ async function readEnvFile(cwd) {
2087
+ const candidates = [".env.local", ".env.production", ".env"];
2088
+ for (const name of candidates) {
2089
+ const filePath = path2.join(cwd, name);
2090
+ const exists = await fs2.stat(filePath).catch(() => null);
2091
+ if (!exists) continue;
2092
+ const content = await fs2.readFile(filePath, "utf-8");
2093
+ const vars = [];
2094
+ for (const line of content.split("\n")) {
2095
+ const trimmed = line.trim();
2096
+ if (!trimmed || trimmed.startsWith("#")) continue;
2097
+ const eqIndex = trimmed.indexOf("=");
2098
+ if (eqIndex === -1) continue;
2099
+ const key = trimmed.slice(0, eqIndex).trim();
2100
+ let value = trimmed.slice(eqIndex + 1).trim();
2101
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2102
+ value = value.slice(1, -1);
2103
+ }
2104
+ if (key) vars.push({ key, value });
2105
+ }
2106
+ return vars;
2107
+ }
2108
+ return [];
2109
+ }
2110
+
2111
+ // src/commands/deployments/deploy.ts
2112
+ import * as path3 from "path";
2113
+ import * as fs3 from "fs/promises";
2114
+ import { createReadStream } from "fs";
2115
+ import { createHash as createHash2 } from "crypto";
2116
+ import * as clack10 from "@clack/prompts";
2117
+ import archiver from "archiver";
2118
+ var POLL_INTERVAL_MS3 = 5e3;
2119
+ var POLL_TIMEOUT_MS3 = 3e5;
2120
+ var DIRECT_UPLOAD_CONCURRENCY = 8;
2121
+ var EXCLUDE_PATTERNS = [
2122
+ "node_modules",
2123
+ ".git",
2124
+ ".next",
2125
+ ".env",
2126
+ ".env.local",
2127
+ "dist",
2128
+ "build",
2129
+ ".DS_Store",
2130
+ ".insforge",
2131
+ // IDE and AI agent configs
2132
+ ".claude",
2133
+ ".agents",
2134
+ ".augment",
2135
+ ".kilocode",
2136
+ ".kiro",
2137
+ ".qoder",
2138
+ ".qwen",
2139
+ ".roo",
2140
+ ".trae",
2141
+ ".windsurf",
2142
+ ".vercel",
2143
+ ".turbo",
2144
+ ".cache",
2145
+ "skills",
2146
+ "coverage"
2147
+ ];
2148
+ var DirectDeploymentUnsupportedError = class extends Error {
2149
+ constructor() {
2150
+ super("Direct deployment endpoints are not available on this backend");
2151
+ this.name = "DirectDeploymentUnsupportedError";
2152
+ }
2153
+ };
2154
+ function shouldExclude(name) {
2155
+ const normalized = name.replace(/\\/g, "/");
2156
+ for (const pattern of EXCLUDE_PATTERNS) {
2157
+ if (normalized === pattern || normalized.startsWith(pattern + "/") || normalized.endsWith("/" + pattern) || normalized.includes("/" + pattern + "/")) {
2158
+ return true;
2159
+ }
2160
+ }
2161
+ if (normalized.endsWith(".log")) return true;
2162
+ return false;
2163
+ }
2164
+ function isInsforgeCloudOssHost(ossHost) {
2165
+ try {
2166
+ return new URL(ossHost).hostname.endsWith(".insforge.app");
2167
+ } catch {
2168
+ return false;
2169
+ }
2170
+ }
2171
+ function normalizeRelativePath(sourceDir, absolutePath) {
2172
+ return path3.relative(sourceDir, absolutePath).split(path3.sep).join("/").replace(/\\/g, "/");
2173
+ }
2174
+ async function hashFile(filePath) {
2175
+ const hash = createHash2("sha1");
2176
+ let size = 0;
2177
+ for await (const chunk of createReadStream(filePath)) {
2178
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2179
+ size += buffer.length;
2180
+ hash.update(buffer);
2181
+ }
2182
+ return { sha: hash.digest("hex"), size };
1437
2183
  }
1438
2184
  async function collectDeploymentFiles(sourceDir) {
1439
2185
  const files = [];
1440
2186
  async function walk(currentDir) {
1441
- const entries = await fs2.readdir(currentDir, { withFileTypes: true });
2187
+ const entries = await fs3.readdir(currentDir, { withFileTypes: true });
1442
2188
  entries.sort((a, b) => a.name.localeCompare(b.name));
1443
2189
  for (const entry of entries) {
1444
- const absolutePath = path2.join(currentDir, entry.name);
2190
+ const absolutePath = path3.join(currentDir, entry.name);
1445
2191
  const normalizedPath = normalizeRelativePath(sourceDir, absolutePath);
1446
2192
  if (!normalizedPath || shouldExclude(normalizedPath)) {
1447
2193
  continue;
@@ -1547,12 +2293,12 @@ async function startDirectDeployment(deploymentId, startBody) {
1547
2293
  });
1548
2294
  await response.json();
1549
2295
  }
1550
- async function pollDeployment(deploymentId, spinner7, syncBeforeRead) {
1551
- spinner7?.message("Building and deploying...");
2296
+ async function pollDeployment(deploymentId, spinner8, syncBeforeRead) {
2297
+ spinner8?.message("Building and deploying...");
1552
2298
  const startTime = Date.now();
1553
2299
  let deployment = null;
1554
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1555
- await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS));
2300
+ while (Date.now() - startTime < POLL_TIMEOUT_MS3) {
2301
+ await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS3));
1556
2302
  try {
1557
2303
  if (syncBeforeRead) {
1558
2304
  await ossFetch(`/api/deployments/${deploymentId}/sync`, { method: "POST" });
@@ -1564,13 +2310,13 @@ async function pollDeployment(deploymentId, spinner7, syncBeforeRead) {
1564
2310
  break;
1565
2311
  }
1566
2312
  if (status === "ERROR" || status === "CANCELED") {
1567
- spinner7?.stop("Deployment failed");
2313
+ spinner8?.stop("Deployment failed");
1568
2314
  throw new CLIError(
1569
2315
  getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`
1570
2316
  );
1571
2317
  }
1572
2318
  const elapsed = Math.round((Date.now() - startTime) / 1e3);
1573
- spinner7?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
2319
+ spinner8?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
1574
2320
  } catch (err) {
1575
2321
  if (err instanceof CLIError) throw err;
1576
2322
  }
@@ -1580,20 +2326,20 @@ async function pollDeployment(deploymentId, spinner7, syncBeforeRead) {
1580
2326
  return { deploymentId, deployment, isReady, liveUrl };
1581
2327
  }
1582
2328
  async function deployProjectDirect(opts, config) {
1583
- const { sourceDir, startBody = {}, spinner: spinner7 } = opts;
1584
- spinner7?.start("Scanning source files...");
2329
+ const { sourceDir, startBody = {}, spinner: spinner8 } = opts;
2330
+ spinner8?.start("Scanning source files...");
1585
2331
  const localFiles = await collectDeploymentFiles(sourceDir);
1586
2332
  if (localFiles.length === 0) {
1587
2333
  throw new CLIError("No deployable files found in the source directory.");
1588
2334
  }
1589
- spinner7?.message("Creating deployment...");
2335
+ spinner8?.message("Creating deployment...");
1590
2336
  const createResult = await createDirectDeploymentSession(
1591
2337
  config,
1592
2338
  localFiles.map(({ path: relativePath, sha, size }) => ({ path: relativePath, sha, size }))
1593
2339
  );
1594
2340
  const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
1595
2341
  const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
1596
- spinner7?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
2342
+ spinner8?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
1597
2343
  await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
1598
2344
  const localFile = localFileByPath.get(manifestFile.path);
1599
2345
  if (!localFile) {
@@ -1604,18 +2350,18 @@ async function deployProjectDirect(opts, config) {
1604
2350
  }
1605
2351
  await uploadDirectDeploymentFile(createResult.id, manifestFile, localFile);
1606
2352
  });
1607
- spinner7?.message("Starting deployment...");
2353
+ spinner8?.message("Starting deployment...");
1608
2354
  await startDirectDeployment(createResult.id, startBody);
1609
- return await pollDeployment(createResult.id, spinner7, !isInsforgeCloudOssHost(config.oss_host));
2355
+ return await pollDeployment(createResult.id, spinner8, !isInsforgeCloudOssHost(config.oss_host));
1610
2356
  }
1611
2357
  async function deployProjectLegacy(opts) {
1612
- const { sourceDir, startBody = {}, spinner: spinner7 } = opts;
1613
- spinner7?.message("Creating deployment...");
2358
+ const { sourceDir, startBody = {}, spinner: spinner8 } = opts;
2359
+ spinner8?.message("Creating deployment...");
1614
2360
  const createRes = await ossFetch("/api/deployments", { method: "POST" });
1615
2361
  const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
1616
- spinner7?.message("Compressing source files...");
2362
+ spinner8?.message("Compressing source files...");
1617
2363
  const zipBuffer = await createZipBuffer(sourceDir);
1618
- spinner7?.message("Uploading...");
2364
+ spinner8?.message("Uploading...");
1619
2365
  const formData = new FormData();
1620
2366
  for (const [key, value] of Object.entries(uploadFields)) {
1621
2367
  formData.append(key, value);
@@ -1626,13 +2372,13 @@ async function deployProjectLegacy(opts) {
1626
2372
  const uploadErr = await uploadRes.text();
1627
2373
  throw new CLIError(`Failed to upload: ${uploadErr}`);
1628
2374
  }
1629
- spinner7?.message("Starting deployment...");
2375
+ spinner8?.message("Starting deployment...");
1630
2376
  const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
1631
2377
  method: "POST",
1632
2378
  body: JSON.stringify(startBody)
1633
2379
  });
1634
2380
  await startRes.json();
1635
- return await pollDeployment(deploymentId, spinner7, false);
2381
+ return await pollDeployment(deploymentId, spinner8, false);
1636
2382
  }
1637
2383
  async function deployProject(opts) {
1638
2384
  const config = getProjectConfig();
@@ -1656,18 +2402,18 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
1656
2402
  await requireAuth();
1657
2403
  const config = getProjectConfig();
1658
2404
  if (!config) throw new ProjectNotLinkedError();
1659
- const sourceDir = path2.resolve(directory ?? ".");
1660
- const stats = await fs2.stat(sourceDir).catch(() => null);
2405
+ const sourceDir = path3.resolve(directory ?? ".");
2406
+ const stats = await fs3.stat(sourceDir).catch(() => null);
1661
2407
  if (!stats?.isDirectory()) {
1662
2408
  throw new CLIError(`"${sourceDir}" is not a valid directory.`);
1663
2409
  }
1664
- const dirName = path2.basename(sourceDir);
2410
+ const dirName = path3.basename(sourceDir);
1665
2411
  if (EXCLUDE_PATTERNS.includes(dirName)) {
1666
2412
  throw new CLIError(
1667
2413
  `"${dirName}" is an excluded directory and cannot be used as a deploy source. Please specify your project root or output directory instead.`
1668
2414
  );
1669
2415
  }
1670
- const spinner7 = !json ? clack6.spinner() : null;
2416
+ const spinner8 = !json ? clack10.spinner() : null;
1671
2417
  const startBody = {};
1672
2418
  if (opts.env) {
1673
2419
  try {
@@ -1693,19 +2439,19 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
1693
2439
  throw new CLIError("Invalid --meta JSON.");
1694
2440
  }
1695
2441
  }
1696
- const result = await deployProject({ sourceDir, startBody, spinner: spinner7 });
2442
+ const result = await deployProject({ sourceDir, startBody, spinner: spinner8 });
1697
2443
  if (result.isReady) {
1698
- spinner7?.stop("Deployment complete");
2444
+ spinner8?.stop("Deployment complete");
1699
2445
  if (json) {
1700
2446
  outputJson(result.deployment);
1701
2447
  } else {
1702
2448
  if (result.liveUrl) {
1703
- clack6.log.success(`Live at: ${result.liveUrl}`);
2449
+ clack10.log.success(`Live at: ${result.liveUrl}`);
1704
2450
  }
1705
- clack6.log.info(`Deployment ID: ${result.deploymentId}`);
2451
+ clack10.log.info(`Deployment ID: ${result.deploymentId}`);
1706
2452
  }
1707
2453
  } else {
1708
- spinner7?.stop("Deployment is still building");
2454
+ spinner8?.stop("Deployment is still building");
1709
2455
  if (json) {
1710
2456
  outputJson({
1711
2457
  id: result.deploymentId,
@@ -1713,9 +2459,9 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
1713
2459
  timedOut: true
1714
2460
  });
1715
2461
  } else {
1716
- clack6.log.info(`Deployment ID: ${result.deploymentId}`);
1717
- clack6.log.warn("Deployment did not finish within 5 minutes.");
1718
- clack6.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
2462
+ clack10.log.info(`Deployment ID: ${result.deploymentId}`);
2463
+ clack10.log.warn("Deployment did not finish within 5 minutes.");
2464
+ clack10.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
1719
2465
  }
1720
2466
  }
1721
2467
  await reportCliUsage("cli.deployments.deploy", true);
@@ -1727,7 +2473,10 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
1727
2473
  }
1728
2474
 
1729
2475
  // src/commands/create.ts
1730
- var execAsync2 = promisify2(exec2);
2476
+ var execAsync2 = promisify3(exec2);
2477
+ var execFileAsync2 = promisify3(execFile2);
2478
+ var SAFE_REPO_PATTERN2 = /^(https?:\/\/|git@)[A-Za-z0-9._:/@~+-]+(\.git)?$/;
2479
+ var SAFE_BRANCH_PATTERN2 = /^[A-Za-z0-9._/-]+$/;
1731
2480
  function buildOssHost(appkey, region) {
1732
2481
  return `https://${appkey}.${region}.insforge.app`;
1733
2482
  }
@@ -1821,31 +2570,31 @@ async function animateBanner() {
1821
2570
  process.stderr.write("\n");
1822
2571
  }
1823
2572
  function getDefaultProjectName() {
1824
- const dirName = path3.basename(process.cwd());
2573
+ const dirName = path4.basename(process.cwd());
1825
2574
  const sanitized = dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1826
2575
  return sanitized.length >= 2 ? sanitized : "";
1827
2576
  }
1828
2577
  async function copyDir(src, dest) {
1829
- const entries = await fs3.readdir(src, { withFileTypes: true });
2578
+ const entries = await fs4.readdir(src, { withFileTypes: true });
1830
2579
  for (const entry of entries) {
1831
- const srcPath = path3.join(src, entry.name);
1832
- const destPath = path3.join(dest, entry.name);
2580
+ const srcPath = path4.join(src, entry.name);
2581
+ const destPath = path4.join(dest, entry.name);
1833
2582
  if (entry.isDirectory()) {
1834
- await fs3.mkdir(destPath, { recursive: true });
2583
+ await fs4.mkdir(destPath, { recursive: true });
1835
2584
  await copyDir(srcPath, destPath);
1836
2585
  } else {
1837
- await fs3.copyFile(srcPath, destPath);
2586
+ await fs4.copyFile(srcPath, destPath);
1838
2587
  }
1839
2588
  }
1840
2589
  }
1841
2590
  function registerCreateCommand(program2) {
1842
- program2.command("create").description("Create a new InsForge project").option("--name <name>", "Project name").option("--org-id <id>", "Organization ID").option("--region <region>", "Deployment region (us-east, us-west, eu-central, ap-southeast)").option("--template <template>", "Template to use: react, nextjs, chatbot, crm, e-commerce, todo, or empty").action(async (opts, cmd) => {
2591
+ program2.command("create").description("Create a new InsForge project").option("--name <name>", "Project name").option("--org-id <id>", "Organization ID").option("--region <region>", "Deployment region (us-east, us-west, eu-central, ap-southeast)").option("--template <template>", "Template to use: react, nextjs, chatbot, crm, e-commerce, todo, or empty").option("--auth <provider>", "Wire a third-party auth provider into the chosen template (currently: better-auth)").action(async (opts, cmd) => {
1843
2592
  const { json, apiUrl } = getRootOpts(cmd);
1844
2593
  try {
1845
2594
  await requireAuth(apiUrl, false);
1846
2595
  if (!json) {
1847
2596
  await animateBanner();
1848
- clack7.intro("Let's build something great");
2597
+ clack11.intro("Let's build something great");
1849
2598
  }
1850
2599
  let orgId = opts.orgId;
1851
2600
  if (!orgId) {
@@ -1855,7 +2604,7 @@ function registerCreateCommand(program2) {
1855
2604
  }
1856
2605
  if (orgs.length === 1) {
1857
2606
  orgId = orgs[0].id;
1858
- if (!json) clack7.log.info(`Using organization: ${orgs[0].name}`);
2607
+ if (!json) clack11.log.info(`Using organization: ${orgs[0].name}`);
1859
2608
  } else {
1860
2609
  if (json) {
1861
2610
  throw new CLIError("Multiple organizations found. Specify --org-id.");
@@ -1886,12 +2635,15 @@ function registerCreateCommand(program2) {
1886
2635
  if (isCancel2(name)) process.exit(0);
1887
2636
  projectName = name;
1888
2637
  }
1889
- projectName = path3.basename(projectName).replace(/[^a-zA-Z0-9._-]/g, "-").replace(/\.+/g, ".");
2638
+ projectName = path4.basename(projectName).replace(/[^a-zA-Z0-9._-]/g, "-").replace(/\.+/g, ".");
1890
2639
  if (projectName.length < 2 || projectName === "." || projectName === "..") {
1891
2640
  throw new CLIError("Project name must be at least 2 safe characters (letters, numbers, hyphens).");
1892
2641
  }
1893
2642
  const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo", "empty"];
1894
2643
  let template = opts.template;
2644
+ if (opts.auth && !VALID_AUTH_PROVIDERS.includes(opts.auth)) {
2645
+ throw new CLIError(`Invalid --auth "${opts.auth}". Valid: ${VALID_AUTH_PROVIDERS.join(", ")}`);
2646
+ }
1895
2647
  if (template && !validTemplates.includes(template)) {
1896
2648
  throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
1897
2649
  }
@@ -1945,27 +2697,27 @@ function registerCreateCommand(program2) {
1945
2697
  initialValue: projectName,
1946
2698
  validate: (v) => {
1947
2699
  if (v.length < 1) return "Directory name is required";
1948
- const normalized = path3.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
2700
+ const normalized = path4.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
1949
2701
  if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
1950
2702
  return void 0;
1951
2703
  }
1952
2704
  });
1953
2705
  if (isCancel2(inputDir)) process.exit(0);
1954
- dirName = path3.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
2706
+ dirName = path4.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
1955
2707
  }
1956
2708
  if (!dirName || dirName === "." || dirName === "..") {
1957
2709
  throw new CLIError("Invalid directory name.");
1958
2710
  }
1959
- projectDir = path3.resolve(originalCwd, dirName);
1960
- const dirExists = await fs3.stat(projectDir).catch(() => null);
2711
+ projectDir = path4.resolve(originalCwd, dirName);
2712
+ const dirExists = await fs4.stat(projectDir).catch(() => null);
1961
2713
  if (dirExists) {
1962
2714
  throw new CLIError(`Directory "${dirName}" already exists.`);
1963
2715
  }
1964
- await fs3.mkdir(projectDir);
2716
+ await fs4.mkdir(projectDir);
1965
2717
  process.chdir(projectDir);
1966
2718
  }
1967
2719
  let projectLinked = false;
1968
- const s = !json ? clack7.spinner() : null;
2720
+ const s = !json ? clack11.spinner() : null;
1969
2721
  try {
1970
2722
  s?.start("Creating project...");
1971
2723
  const project = await createProject(orgId, projectName, opts.region, apiUrl);
@@ -1993,37 +2745,49 @@ function registerCreateCommand(program2) {
1993
2745
  try {
1994
2746
  const anonKey = await getAnonKey();
1995
2747
  if (!anonKey) {
1996
- if (!json) clack7.log.warn("Could not retrieve anon key. You can add it to .env.local manually.");
2748
+ if (!json) clack11.log.warn("Could not retrieve anon key. You can add it to .env.local manually.");
1997
2749
  } else {
1998
- const envPath = path3.join(process.cwd(), ".env.local");
2750
+ const envPath = path4.join(process.cwd(), ".env.local");
1999
2751
  const envContent = [
2000
2752
  "# InsForge",
2001
2753
  `NEXT_PUBLIC_INSFORGE_URL=${projectConfig.oss_host}`,
2002
2754
  `NEXT_PUBLIC_INSFORGE_ANON_KEY=${anonKey}`,
2003
2755
  ""
2004
2756
  ].join("\n");
2005
- await fs3.writeFile(envPath, envContent, { flag: "wx" });
2757
+ await fs4.writeFile(envPath, envContent, { flag: "wx" });
2006
2758
  if (!json) {
2007
- clack7.log.success("Created .env.local with your InsForge credentials");
2759
+ clack11.log.success("Created .env.local with your InsForge credentials");
2008
2760
  }
2009
2761
  }
2010
2762
  } catch (err) {
2011
2763
  const error = err;
2012
2764
  if (!json) {
2013
2765
  if (error.code === "EEXIST") {
2014
- clack7.log.warn(".env.local already exists; skipping InsForge key seeding.");
2766
+ clack11.log.warn(".env.local already exists; skipping InsForge key seeding.");
2015
2767
  } else {
2016
- clack7.log.warn(`Failed to create .env.local: ${error.message}`);
2768
+ clack11.log.warn(`Failed to create .env.local: ${error.message}`);
2017
2769
  }
2018
2770
  }
2019
2771
  }
2020
2772
  }
2773
+ if (opts.auth) {
2774
+ try {
2775
+ const result = await applyAuthProvider(opts.auth, process.cwd(), projectConfig, json);
2776
+ if (!json) {
2777
+ clack11.log.success(`Wired in ${opts.auth}: ${result.written.length} new, ${result.overwritten.length} replaced`);
2778
+ }
2779
+ } catch (err) {
2780
+ const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
2781
+ if (json) console.error(JSON.stringify({ warning: msg }));
2782
+ else clack11.log.warn(msg);
2783
+ }
2784
+ }
2021
2785
  await installSkills(json);
2022
2786
  trackCommand("create", orgId);
2023
2787
  await reportCliUsage("cli.create", true, 6);
2024
- const templateDownloaded = hasTemplate ? await fs3.stat(path3.join(process.cwd(), "package.json")).catch(() => null) : null;
2788
+ const templateDownloaded = hasTemplate ? await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null) : null;
2025
2789
  if (templateDownloaded) {
2026
- const installSpinner = !json ? clack7.spinner() : null;
2790
+ const installSpinner = !json ? clack11.spinner() : null;
2027
2791
  installSpinner?.start("Installing dependencies...");
2028
2792
  try {
2029
2793
  await execAsync2("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
@@ -2031,8 +2795,8 @@ function registerCreateCommand(program2) {
2031
2795
  } catch (err) {
2032
2796
  installSpinner?.stop("Failed to install dependencies");
2033
2797
  if (!json) {
2034
- clack7.log.warn(`npm install failed: ${err.message}`);
2035
- clack7.log.info("Run `npm install` manually to install dependencies.");
2798
+ clack11.log.warn(`npm install failed: ${err.message}`);
2799
+ clack11.log.info("Run `npm install` manually to install dependencies.");
2036
2800
  }
2037
2801
  }
2038
2802
  }
@@ -2048,7 +2812,7 @@ function registerCreateCommand(program2) {
2048
2812
  if (envVars.length > 0) {
2049
2813
  startBody.envVars = envVars;
2050
2814
  }
2051
- const deploySpinner = clack7.spinner();
2815
+ const deploySpinner = clack11.spinner();
2052
2816
  const result = await deployProject({
2053
2817
  sourceDir: process.cwd(),
2054
2818
  startBody,
@@ -2059,12 +2823,12 @@ function registerCreateCommand(program2) {
2059
2823
  liveUrl = result.liveUrl;
2060
2824
  } else {
2061
2825
  deploySpinner.stop("Deployment is still building");
2062
- clack7.log.info(`Deployment ID: ${result.deploymentId}`);
2063
- clack7.log.warn("Deployment did not finish within 2 minutes.");
2064
- clack7.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
2826
+ clack11.log.info(`Deployment ID: ${result.deploymentId}`);
2827
+ clack11.log.warn("Deployment did not finish within 2 minutes.");
2828
+ clack11.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
2065
2829
  }
2066
2830
  } catch (err) {
2067
- clack7.log.warn(`Deploy failed: ${err.message}`);
2831
+ clack11.log.warn(`Deploy failed: ${err.message}`);
2068
2832
  }
2069
2833
  }
2070
2834
  }
@@ -2081,38 +2845,38 @@ function registerCreateCommand(program2) {
2081
2845
  }
2082
2846
  });
2083
2847
  } else {
2084
- clack7.log.step(`Dashboard: ${dashboardUrl}`);
2848
+ clack11.log.step(`Dashboard: ${dashboardUrl}`);
2085
2849
  if (liveUrl) {
2086
- clack7.log.success(`Live site: ${liveUrl}`);
2850
+ clack11.log.success(`Live site: ${liveUrl}`);
2087
2851
  }
2088
2852
  if (templateDownloaded) {
2089
2853
  const steps = [
2090
2854
  `cd ${dirName}`,
2091
2855
  "npm run dev"
2092
2856
  ];
2093
- clack7.note(steps.join("\n"), "Next steps");
2094
- clack7.note("Open your coding agent (Claude Code, Codex, Cursor, etc.) to add new features.", "Keep building");
2857
+ clack11.note(steps.join("\n"), "Next steps");
2858
+ clack11.note("Open your coding agent (Claude Code, Codex, Cursor, etc.) to add new features.", "Keep building");
2095
2859
  } else if (hasTemplate && !templateDownloaded) {
2096
- clack7.log.warn("Template download failed. You can retry or set up manually.");
2860
+ clack11.log.warn("Template download failed. You can retry or set up manually.");
2097
2861
  } else {
2098
2862
  const prompts = [
2099
2863
  "Build a todo app with Google OAuth sign-in",
2100
2864
  "Build an Instagram clone where users can upload photos, like, and comment",
2101
2865
  "Build an AI chatbot with conversation history"
2102
2866
  ];
2103
- clack7.note(
2867
+ clack11.note(
2104
2868
  `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
2105
2869
 
2106
2870
  ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
2107
2871
  "Start building"
2108
2872
  );
2109
2873
  }
2110
- clack7.outro("Done!");
2874
+ clack11.outro("Done!");
2111
2875
  }
2112
2876
  } catch (err) {
2113
2877
  if (!projectLinked && hasTemplate && projectDir !== originalCwd) {
2114
2878
  process.chdir(originalCwd);
2115
- await fs3.rm(projectDir, { recursive: true, force: true }).catch(() => {
2879
+ await fs4.rm(projectDir, { recursive: true, force: true }).catch(() => {
2116
2880
  });
2117
2881
  }
2118
2882
  throw err;
@@ -2126,18 +2890,18 @@ ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
2126
2890
  });
2127
2891
  }
2128
2892
  async function downloadTemplate(framework, projectConfig, projectName, json, _apiUrl) {
2129
- const s = !json ? clack7.spinner() : null;
2893
+ const s = !json ? clack11.spinner() : null;
2130
2894
  s?.start("Downloading template...");
2131
2895
  try {
2132
2896
  const anonKey = await getAnonKey();
2133
2897
  if (!anonKey) {
2134
2898
  throw new Error("Failed to retrieve anon key from backend");
2135
2899
  }
2136
- const tempDir = tmpdir();
2900
+ const tempDir = tmpdir2();
2137
2901
  const targetDir = projectName;
2138
- const templatePath = path3.join(tempDir, targetDir);
2902
+ const templatePath = path4.join(tempDir, targetDir);
2139
2903
  try {
2140
- await fs3.rm(templatePath, { recursive: true, force: true });
2904
+ await fs4.rm(templatePath, { recursive: true, force: true });
2141
2905
  } catch {
2142
2906
  }
2143
2907
  const frame = framework === "nextjs" ? "nextjs" : "react";
@@ -2151,41 +2915,53 @@ async function downloadTemplate(framework, projectConfig, projectName, json, _ap
2151
2915
  s?.message("Copying template files...");
2152
2916
  const cwd = process.cwd();
2153
2917
  await copyDir(templatePath, cwd);
2154
- await fs3.rm(templatePath, { recursive: true, force: true }).catch(() => {
2918
+ await fs4.rm(templatePath, { recursive: true, force: true }).catch(() => {
2155
2919
  });
2156
2920
  s?.stop("Template files downloaded");
2157
2921
  } catch (err) {
2158
2922
  s?.stop("Template download failed");
2159
2923
  if (!json) {
2160
- clack7.log.warn(`Failed to download template: ${err.message}`);
2161
- clack7.log.info("You can manually set up the template later.");
2924
+ clack11.log.warn(`Failed to download template: ${err.message}`);
2925
+ clack11.log.info("You can manually set up the template later.");
2162
2926
  }
2163
2927
  }
2164
2928
  }
2165
2929
  async function downloadGitHubTemplate(templateName, projectConfig, json) {
2166
- const s = !json ? clack7.spinner() : null;
2930
+ const s = !json ? clack11.spinner() : null;
2167
2931
  s?.start(`Downloading ${templateName} template...`);
2168
- const tempDir = path3.join(tmpdir(), `insforge-template-${Date.now()}`);
2932
+ const tempDir = path4.join(tmpdir2(), `insforge-template-${Date.now()}`);
2169
2933
  try {
2170
- await fs3.mkdir(tempDir, { recursive: true });
2171
- await execAsync2(
2172
- "git clone --depth 1 https://github.com/InsForge/insforge-templates.git .",
2173
- { cwd: tempDir, maxBuffer: 10 * 1024 * 1024, timeout: 6e4 }
2174
- );
2175
- const templateDir = path3.join(tempDir, templateName);
2176
- const stat5 = await fs3.stat(templateDir).catch(() => null);
2934
+ await fs4.mkdir(tempDir, { recursive: true });
2935
+ const templatesRepo = process.env.INSFORGE_TEMPLATES_REPO ?? "https://github.com/InsForge/insforge-templates.git";
2936
+ if (!SAFE_REPO_PATTERN2.test(templatesRepo)) {
2937
+ throw new Error(`INSFORGE_TEMPLATES_REPO has unsupported characters: ${templatesRepo}`);
2938
+ }
2939
+ const templatesBranch = process.env.INSFORGE_TEMPLATES_BRANCH;
2940
+ if (templatesBranch !== void 0 && !SAFE_BRANCH_PATTERN2.test(templatesBranch)) {
2941
+ throw new Error(`INSFORGE_TEMPLATES_BRANCH has unsupported characters: ${templatesBranch}`);
2942
+ }
2943
+ const cloneArgs = ["clone", "--depth", "1"];
2944
+ if (templatesBranch) cloneArgs.push("-b", templatesBranch);
2945
+ cloneArgs.push("--", templatesRepo, ".");
2946
+ await execFileAsync2("git", cloneArgs, {
2947
+ cwd: tempDir,
2948
+ maxBuffer: 10 * 1024 * 1024,
2949
+ timeout: 6e4
2950
+ });
2951
+ const templateDir = path4.join(tempDir, templateName);
2952
+ const stat5 = await fs4.stat(templateDir).catch(() => null);
2177
2953
  if (!stat5?.isDirectory()) {
2178
2954
  throw new Error(`Template "${templateName}" not found in repository`);
2179
2955
  }
2180
2956
  s?.message("Copying template files...");
2181
2957
  const cwd = process.cwd();
2182
2958
  await copyDir(templateDir, cwd);
2183
- const envExamplePath = path3.join(cwd, ".env.example");
2184
- const envExampleExists = await fs3.stat(envExamplePath).catch(() => null);
2959
+ const envExamplePath = path4.join(cwd, ".env.example");
2960
+ const envExampleExists = await fs4.stat(envExamplePath).catch(() => null);
2185
2961
  if (envExampleExists) {
2186
2962
  const anonKey = await getAnonKey();
2187
- const envExample = await fs3.readFile(envExamplePath, "utf-8");
2188
- const envContent = envExample.replace(
2963
+ const envExample = await fs4.readFile(envExamplePath, "utf-8");
2964
+ const envFinal = envExample.replace(
2189
2965
  /^([A-Z][A-Z0-9_]*=)(.*)$/gm,
2190
2966
  (_, prefix, _value) => {
2191
2967
  const key = prefix.slice(0, -1);
@@ -2195,32 +2971,32 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
2195
2971
  return `${prefix}${_value}`;
2196
2972
  }
2197
2973
  );
2198
- const envLocalPath = path3.join(cwd, ".env.local");
2974
+ const envLocalPath = path4.join(cwd, ".env.local");
2199
2975
  try {
2200
- await fs3.writeFile(envLocalPath, envContent, { flag: "wx" });
2976
+ await fs4.writeFile(envLocalPath, envFinal, { flag: "wx" });
2201
2977
  } catch (e) {
2202
2978
  if (e.code === "EEXIST") {
2203
- if (!json) clack7.log.warn(".env.local already exists; skipping env seeding.");
2979
+ if (!json) clack11.log.warn(".env.local already exists; skipping env seeding.");
2204
2980
  } else {
2205
2981
  throw e;
2206
2982
  }
2207
2983
  }
2208
2984
  }
2209
2985
  s?.stop(`${templateName} template downloaded`);
2210
- const migrationPath = path3.join(cwd, "migrations", "db_init.sql");
2211
- const migrationExists = await fs3.stat(migrationPath).catch(() => null);
2986
+ const migrationPath = path4.join(cwd, "migrations", "db_init.sql");
2987
+ const migrationExists = await fs4.stat(migrationPath).catch(() => null);
2212
2988
  if (migrationExists) {
2213
- const dbSpinner = !json ? clack7.spinner() : null;
2989
+ const dbSpinner = !json ? clack11.spinner() : null;
2214
2990
  dbSpinner?.start("Running database migrations...");
2215
2991
  try {
2216
- const sql = await fs3.readFile(migrationPath, "utf-8");
2992
+ const sql = await fs4.readFile(migrationPath, "utf-8");
2217
2993
  await runRawSql(sql, true);
2218
2994
  dbSpinner?.stop("Database migrations applied");
2219
2995
  } catch (err) {
2220
2996
  dbSpinner?.stop("Database migration failed");
2221
2997
  if (!json) {
2222
- clack7.log.warn(`Migration failed: ${err.message}`);
2223
- clack7.log.info('You can run the migration manually: npx @insforge/cli db query --unrestricted "$(cat migrations/db_init.sql)"');
2998
+ clack11.log.warn(`Migration failed: ${err.message}`);
2999
+ clack11.log.info('You can run the migration manually: npx @insforge/cli db query --unrestricted "$(cat migrations/db_init.sql)"');
2224
3000
  } else {
2225
3001
  throw err;
2226
3002
  }
@@ -2228,25 +3004,31 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
2228
3004
  }
2229
3005
  } catch (err) {
2230
3006
  s?.stop(`${templateName} template download failed`);
2231
- if (!json) {
2232
- clack7.log.warn(`Failed to download ${templateName} template: ${err.message}`);
2233
- clack7.log.info("You can manually clone from: https://github.com/InsForge/insforge-templates");
3007
+ const msg = `Failed to download ${templateName} template: ${err.message}`;
3008
+ if (json) {
3009
+ console.error(JSON.stringify({ warning: msg }));
3010
+ } else {
3011
+ clack11.log.warn(msg);
3012
+ clack11.log.info("You can manually clone from: https://github.com/InsForge/insforge-templates");
2234
3013
  }
2235
3014
  } finally {
2236
- await fs3.rm(tempDir, { recursive: true, force: true }).catch(() => {
3015
+ await fs4.rm(tempDir, { recursive: true, force: true }).catch(() => {
2237
3016
  });
2238
3017
  }
2239
3018
  }
2240
3019
 
2241
3020
  // src/commands/projects/link.ts
2242
- var execAsync3 = promisify3(exec3);
3021
+ var execAsync3 = promisify4(exec3);
2243
3022
  function buildOssHost2(appkey, region) {
2244
3023
  return `https://${appkey}.${region}.insforge.app`;
2245
3024
  }
2246
3025
  function registerProjectLinkCommand(program2) {
2247
- program2.command("link").description("Link current directory to an InsForge project").option("--project-id <id>", "Project ID to link").option("--org-id <id>", "Organization ID").option("--template <template>", "Download a template after linking: react, nextjs, chatbot, crm, e-commerce, todo").option("--api-base-url <url>", "API Base URL for direct linking (OSS/Self-hosted)").option("--api-key <key>", "API Key for direct linking (OSS/Self-hosted)").action(async (opts, cmd) => {
3026
+ program2.command("link").description("Link current directory to an InsForge project").option("--project-id <id>", "Project ID to link").option("--org-id <id>", "Organization ID").option("--template <template>", "Download a template after linking: react, nextjs, chatbot, crm, e-commerce, todo").option("--auth <provider>", "Wire a third-party auth provider into the chosen template (currently: better-auth)").option("--api-base-url <url>", "API Base URL for direct linking (OSS/Self-hosted)").option("--api-key <key>", "API Key for direct linking (OSS/Self-hosted)").action(async (opts, cmd) => {
2248
3027
  const { json, apiUrl } = getRootOpts(cmd);
2249
3028
  const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo"];
3029
+ if (opts.auth && !VALID_AUTH_PROVIDERS.includes(opts.auth)) {
3030
+ throw new CLIError(`Invalid --auth "${opts.auth}". Valid: ${VALID_AUTH_PROVIDERS.join(", ")}`);
3031
+ }
2250
3032
  try {
2251
3033
  if (opts.template && !validTemplates.includes(opts.template)) {
2252
3034
  throw new CLIError(`Invalid template "${opts.template}". Valid options: ${validTemplates.join(", ")}`);
@@ -2281,23 +3063,23 @@ function registerProjectLinkCommand(program2) {
2281
3063
  initialValue: defaultDir,
2282
3064
  validate: (v) => {
2283
3065
  if (v.length < 1) return "Directory name is required";
2284
- const normalized = path4.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
3066
+ const normalized = path5.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
2285
3067
  if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
2286
3068
  return void 0;
2287
3069
  }
2288
3070
  });
2289
3071
  if (isCancel2(inputDir)) process.exit(0);
2290
- dirName = path4.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
3072
+ dirName = path5.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
2291
3073
  }
2292
3074
  if (!dirName || dirName === "." || dirName === "..") {
2293
3075
  throw new CLIError("Invalid directory name.");
2294
3076
  }
2295
- const templateDir = path4.resolve(process.cwd(), dirName);
2296
- const dirExists = await fs4.stat(templateDir).catch(() => null);
3077
+ const templateDir = path5.resolve(process.cwd(), dirName);
3078
+ const dirExists = await fs5.stat(templateDir).catch(() => null);
2297
3079
  if (dirExists) {
2298
3080
  throw new CLIError(`Directory "${dirName}" already exists.`);
2299
3081
  }
2300
- await fs4.mkdir(templateDir);
3082
+ await fs5.mkdir(templateDir);
2301
3083
  process.chdir(templateDir);
2302
3084
  saveProjectConfig(projectConfig2);
2303
3085
  if (json) {
@@ -2312,17 +3094,27 @@ function registerProjectLinkCommand(program2) {
2312
3094
  }
2313
3095
  captureEvent(FAKE_ORG_ID, "template_selected", { template: template2, source: "link_direct" });
2314
3096
  await downloadGitHubTemplate(template2, projectConfig2, json);
2315
- const templateDownloaded = await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null);
2316
- if (templateDownloaded && !json) {
2317
- const installSpinner = clack8.spinner();
3097
+ const templateDownloaded = await fs5.stat(path5.join(process.cwd(), "package.json")).catch(() => null);
3098
+ if (opts.auth) {
3099
+ try {
3100
+ const result = await applyAuthProvider(opts.auth, process.cwd(), projectConfig2, json);
3101
+ if (!json) clack12.log.success(`Wired in ${opts.auth}: ${result.written.length} new, ${result.overwritten.length} replaced`);
3102
+ } catch (err) {
3103
+ const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
3104
+ if (json) console.error(JSON.stringify({ warning: msg }));
3105
+ else clack12.log.warn(msg);
3106
+ }
3107
+ }
3108
+ if (templateDownloaded && !json) {
3109
+ const installSpinner = clack12.spinner();
2318
3110
  installSpinner.start("Installing dependencies...");
2319
3111
  try {
2320
3112
  await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
2321
3113
  installSpinner.stop("Dependencies installed");
2322
3114
  } catch (err) {
2323
3115
  installSpinner.stop("Failed to install dependencies");
2324
- clack8.log.warn(`npm install failed: ${err.message}`);
2325
- clack8.log.info("Run `npm install` manually to install dependencies.");
3116
+ clack12.log.warn(`npm install failed: ${err.message}`);
3117
+ clack12.log.info("Run `npm install` manually to install dependencies.");
2326
3118
  }
2327
3119
  }
2328
3120
  await installSkills(json);
@@ -2342,9 +3134,9 @@ function registerProjectLinkCommand(program2) {
2342
3134
  `${pc2.bold("1.")} ${runCommand}`,
2343
3135
  `${pc2.bold("2.")} Open ${pc2.cyan("Claude Code")} or ${pc2.cyan("Cursor")} and prompt your agent to add more features`
2344
3136
  ];
2345
- clack8.note(steps.join("\n"), "What's next");
3137
+ clack12.note(steps.join("\n"), "What's next");
2346
3138
  } else {
2347
- clack8.log.warn("Template download failed. You can retry or set up manually.");
3139
+ clack12.log.warn("Template download failed. You can retry or set up manually.");
2348
3140
  }
2349
3141
  }
2350
3142
  return;
@@ -2355,6 +3147,31 @@ function registerProjectLinkCommand(program2) {
2355
3147
  } else {
2356
3148
  outputSuccess(`Linked to direct project at ${projectConfig2.oss_host}`);
2357
3149
  }
3150
+ if (opts.auth) {
3151
+ try {
3152
+ const result = await applyAuthProvider(opts.auth, process.cwd(), projectConfig2, json);
3153
+ if (!json) {
3154
+ clack12.log.success(`Wired in ${opts.auth}: ${result.written.length} new, ${result.overwritten.length} replaced`);
3155
+ }
3156
+ if (result.packageJsonPatched && !json) {
3157
+ const installSpinner = clack12.spinner();
3158
+ installSpinner.start("Installing new dependencies...");
3159
+ try {
3160
+ await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
3161
+ installSpinner.stop("Dependencies installed");
3162
+ } catch (err) {
3163
+ installSpinner.stop("Failed to install dependencies");
3164
+ clack12.log.warn(`npm install failed: ${err.message}`);
3165
+ clack12.log.info("Run `npm install` manually to install dependencies.");
3166
+ }
3167
+ }
3168
+ if (!json) clack12.note(result.nextSteps, "What's next");
3169
+ } catch (err) {
3170
+ const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
3171
+ if (json) console.error(JSON.stringify({ warning: msg }));
3172
+ else clack12.log.warn(msg);
3173
+ }
3174
+ }
2358
3175
  trackCommand("link", "oss-org", { direct: true });
2359
3176
  await installSkills(json);
2360
3177
  await reportCliUsage("cli.link_direct", true, 6, projectConfig2);
@@ -2382,7 +3199,7 @@ function registerProjectLinkCommand(program2) {
2382
3199
  }
2383
3200
  if (orgs.length === 1) {
2384
3201
  orgId = orgs[0].id;
2385
- if (!json) clack8.log.info(`Using organization: ${orgs[0].name}`);
3202
+ if (!json) clack12.log.info(`Using organization: ${orgs[0].name}`);
2386
3203
  } else {
2387
3204
  if (json) {
2388
3205
  throw new CLIError("Multiple organizations found. Specify --org-id.");
@@ -2468,68 +3285,91 @@ function registerProjectLinkCommand(program2) {
2468
3285
  initialValue: project.name,
2469
3286
  validate: (v) => {
2470
3287
  if (v.length < 1) return "Directory name is required";
2471
- const normalized = path4.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
3288
+ const normalized = path5.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
2472
3289
  if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
2473
3290
  return void 0;
2474
3291
  }
2475
3292
  });
2476
3293
  if (isCancel2(inputDir)) process.exit(0);
2477
- dirName = path4.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
3294
+ dirName = path5.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
2478
3295
  }
2479
3296
  if (!dirName || dirName === "." || dirName === "..") {
2480
3297
  throw new CLIError("Invalid directory name.");
2481
3298
  }
2482
- const templateDir = path4.resolve(process.cwd(), dirName);
2483
- const dirExists = await fs4.stat(templateDir).catch(() => null);
3299
+ const templateDir = path5.resolve(process.cwd(), dirName);
3300
+ const dirExists = await fs5.stat(templateDir).catch(() => null);
2484
3301
  if (dirExists) {
2485
3302
  throw new CLIError(`Directory "${dirName}" already exists.`);
2486
3303
  }
2487
- await fs4.mkdir(templateDir);
3304
+ await fs5.mkdir(templateDir);
2488
3305
  process.chdir(templateDir);
2489
3306
  saveProjectConfig(projectConfig);
2490
3307
  captureEvent(orgId ?? project.organization_id, "template_selected", { template, source: "link" });
2491
3308
  await downloadGitHubTemplate(template, projectConfig, json);
2492
- const templateDownloaded = await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null);
3309
+ const templateDownloaded = await fs5.stat(path5.join(process.cwd(), "package.json")).catch(() => null);
3310
+ if (opts.auth) {
3311
+ try {
3312
+ const result = await applyAuthProvider(opts.auth, process.cwd(), projectConfig, json);
3313
+ if (!json) clack12.log.success(`Wired in ${opts.auth}: ${result.written.length} new, ${result.overwritten.length} replaced`);
3314
+ } catch (err) {
3315
+ const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
3316
+ if (json) console.error(JSON.stringify({ warning: msg }));
3317
+ else clack12.log.warn(msg);
3318
+ }
3319
+ }
2493
3320
  if (templateDownloaded && !json) {
2494
- const installSpinner = clack8.spinner();
3321
+ const installSpinner = clack12.spinner();
2495
3322
  installSpinner.start("Installing dependencies...");
2496
3323
  try {
2497
3324
  await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
2498
3325
  installSpinner.stop("Dependencies installed");
2499
3326
  } catch (err) {
2500
3327
  installSpinner.stop("Failed to install dependencies");
2501
- clack8.log.warn(`npm install failed: ${err.message}`);
2502
- clack8.log.info("Run `npm install` manually to install dependencies.");
3328
+ clack12.log.warn(`npm install failed: ${err.message}`);
3329
+ clack12.log.info("Run `npm install` manually to install dependencies.");
2503
3330
  }
2504
3331
  }
2505
3332
  await installSkills(json);
2506
3333
  await reportCliUsage("cli.link", true, 6, projectConfig);
2507
3334
  if (!json) {
2508
3335
  const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
2509
- clack8.log.step(`Dashboard: ${pc2.underline(dashboardUrl)}`);
3336
+ clack12.log.step(`Dashboard: ${pc2.underline(dashboardUrl)}`);
2510
3337
  if (templateDownloaded) {
2511
3338
  const runCommand = `${pc2.cyan("cd")} ${pc2.green(dirName)} ${pc2.dim("&&")} ${pc2.cyan("npm run dev")}`;
2512
3339
  const steps = [
2513
3340
  `${pc2.bold("1.")} ${runCommand}`,
2514
3341
  `${pc2.bold("2.")} Open ${pc2.cyan("Claude Code")} or ${pc2.cyan("Cursor")} and prompt your agent to add more features`
2515
3342
  ];
2516
- clack8.note(steps.join("\n"), "What's next");
3343
+ clack12.note(steps.join("\n"), "What's next");
2517
3344
  } else {
2518
- clack8.log.warn("Template download failed. You can retry or set up manually.");
3345
+ clack12.log.warn("Template download failed. You can retry or set up manually.");
2519
3346
  }
2520
3347
  }
2521
3348
  } else {
3349
+ if (opts.auth) {
3350
+ try {
3351
+ const result = await applyAuthProvider(opts.auth, process.cwd(), projectConfig, json);
3352
+ if (!json) {
3353
+ clack12.log.success(`Wired in ${opts.auth}: ${result.written.length} new, ${result.overwritten.length} replaced`);
3354
+ clack12.note(result.nextSteps, "What's next");
3355
+ }
3356
+ } catch (err) {
3357
+ const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
3358
+ if (json) console.error(JSON.stringify({ warning: msg }));
3359
+ else clack12.log.warn(msg);
3360
+ }
3361
+ }
2522
3362
  await installSkills(json);
2523
3363
  await reportCliUsage("cli.link", true, 6, projectConfig);
2524
3364
  if (!json) {
2525
3365
  const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
2526
- clack8.log.step(`Dashboard: ${dashboardUrl}`);
3366
+ clack12.log.step(`Dashboard: ${dashboardUrl}`);
2527
3367
  const prompts = [
2528
3368
  "Build a todo app with Google OAuth sign-in",
2529
3369
  "Build an Instagram clone where users can upload photos, like, and comment",
2530
3370
  "Build an AI chatbot with conversation history and deploy it to a live URL"
2531
3371
  ];
2532
- clack8.note(
3372
+ clack12.note(
2533
3373
  `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
2534
3374
 
2535
3375
  ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
@@ -2769,7 +3609,7 @@ function registerDbRpcCommand(dbCmd2) {
2769
3609
  }
2770
3610
 
2771
3611
  // src/commands/db/export.ts
2772
- import { writeFileSync as writeFileSync2 } from "fs";
3612
+ import { writeFileSync as writeFileSync3 } from "fs";
2773
3613
  function registerDbExportCommand(dbCmd2) {
2774
3614
  dbCmd2.command("export").description("Export database schema and/or data").option("--format <format>", "Export format: sql or json", "sql").option("--tables <tables>", "Comma-separated list of tables to export (default: all)").option("--no-data", "Exclude table data (schema only)").option("--include-functions", "Include database functions").option("--include-sequences", "Include sequences").option("--include-views", "Include views").option("--row-limit <n>", "Maximum rows per table").option("-o, --output <file>", "Output file path (default: stdout)").action(async (opts, cmd) => {
2775
3615
  const { json } = getRootOpts(cmd);
@@ -2809,7 +3649,7 @@ function registerDbExportCommand(dbCmd2) {
2809
3649
  return;
2810
3650
  }
2811
3651
  if (opts.output) {
2812
- writeFileSync2(opts.output, content);
3652
+ writeFileSync3(opts.output, content);
2813
3653
  const tableCount = meta?.tables?.length;
2814
3654
  const suffix = tableCount ? ` (${tableCount} tables, format: ${meta?.format ?? opts.format})` : "";
2815
3655
  outputSuccess(`Exported to ${opts.output}${suffix}`);
@@ -2824,7 +3664,7 @@ function registerDbExportCommand(dbCmd2) {
2824
3664
 
2825
3665
  // src/commands/db/import.ts
2826
3666
  import { readFileSync as readFileSync3 } from "fs";
2827
- import { basename as basename4 } from "path";
3667
+ import { basename as basename5 } from "path";
2828
3668
  function registerDbImportCommand(dbCmd2) {
2829
3669
  dbCmd2.command("import <file>").description("Import database from a local SQL file").option("--truncate", "Truncate existing tables before import").action(async (file, opts, cmd) => {
2830
3670
  const { json } = getRootOpts(cmd);
@@ -2833,7 +3673,7 @@ function registerDbImportCommand(dbCmd2) {
2833
3673
  const config = getProjectConfig();
2834
3674
  if (!config) throw new ProjectNotLinkedError();
2835
3675
  const fileContent = readFileSync3(file);
2836
- const fileName = basename4(file);
3676
+ const fileName = basename5(file);
2837
3677
  const formData = new FormData();
2838
3678
  formData.append("file", new Blob([fileContent]), fileName);
2839
3679
  if (opts.truncate) {
@@ -2863,12 +3703,12 @@ function registerDbImportCommand(dbCmd2) {
2863
3703
  }
2864
3704
 
2865
3705
  // src/commands/db/migrations.ts
2866
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2867
- import { join as join8 } from "path";
3706
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
3707
+ import { join as join9 } from "path";
2868
3708
 
2869
3709
  // src/lib/migrations.ts
2870
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync } from "fs";
2871
- import { join as join7 } from "path";
3710
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync } from "fs";
3711
+ import { join as join8 } from "path";
2872
3712
  var MIGRATION_VERSION_REGEX = /^\d{1,64}$/u;
2873
3713
  var MIGRATION_FILENAME_REGEX = /^(\d{1,64})_([a-z0-9-]+)\.sql$/u;
2874
3714
  function assertValidMigrationVersion(version) {
@@ -2932,18 +3772,18 @@ function incrementMigrationVersion(version) {
2932
3772
  return formatMigrationVersion(new Date(nextTimestamp));
2933
3773
  }
2934
3774
  function getMigrationsDir(cwd = process.cwd()) {
2935
- return join7(cwd, "migrations");
3775
+ return join8(cwd, "migrations");
2936
3776
  }
2937
3777
  function ensureMigrationsDir(cwd = process.cwd()) {
2938
3778
  const migrationsDir = getMigrationsDir(cwd);
2939
- if (!existsSync3(migrationsDir)) {
3779
+ if (!existsSync4(migrationsDir)) {
2940
3780
  mkdirSync2(migrationsDir, { recursive: true });
2941
3781
  }
2942
3782
  return migrationsDir;
2943
3783
  }
2944
3784
  function listLocalMigrationFilenames(cwd = process.cwd()) {
2945
3785
  const migrationsDir = getMigrationsDir(cwd);
2946
- if (!existsSync3(migrationsDir)) {
3786
+ if (!existsSync4(migrationsDir)) {
2947
3787
  return [];
2948
3788
  }
2949
3789
  return readdirSync(migrationsDir).sort((left, right) => left.localeCompare(right));
@@ -3131,12 +3971,12 @@ function registerDbMigrationsCommand(dbCmd2) {
3131
3971
  migration.version,
3132
3972
  migration.name
3133
3973
  );
3134
- const filePath = join8(migrationsDir, filename);
3135
- if (existingLocalVersions.has(migration.version) || existsSync4(filePath)) {
3974
+ const filePath = join9(migrationsDir, filename);
3975
+ if (existingLocalVersions.has(migration.version) || existsSync5(filePath)) {
3136
3976
  skippedFiles.push(filename);
3137
3977
  continue;
3138
3978
  }
3139
- writeFileSync3(filePath, formatMigrationSql(migration.statements));
3979
+ writeFileSync4(filePath, formatMigrationSql(migration.statements));
3140
3980
  createdFiles.push(filename);
3141
3981
  existingLocalVersions.add(migration.version);
3142
3982
  }
@@ -3174,9 +4014,9 @@ function registerDbMigrationsCommand(dbCmd2) {
3174
4014
  );
3175
4015
  const filename = buildMigrationFilename(nextVersion, migrationName);
3176
4016
  const migrationsDir = ensureMigrationsDir();
3177
- const filePath = join8(migrationsDir, filename);
4017
+ const filePath = join9(migrationsDir, filename);
3178
4018
  try {
3179
- writeFileSync3(filePath, "", { flag: "wx" });
4019
+ writeFileSync4(filePath, "", { flag: "wx" });
3180
4020
  } catch (error) {
3181
4021
  if (error.code === "EEXIST") {
3182
4022
  throw new CLIError(`Migration file already exists: ${filename}`);
@@ -3232,8 +4072,8 @@ function registerDbMigrationsCommand(dbCmd2) {
3232
4072
  `Migration ${targetMigration.filename} is not the next pending local migration. Apply ${earlierPendingMigration.filename} first, or fix/delete it locally if it is invalid or no longer needed.`
3233
4073
  );
3234
4074
  }
3235
- const filePath = join8(getMigrationsDir(), targetMigration.filename);
3236
- if (!existsSync4(filePath)) {
4075
+ const filePath = join9(getMigrationsDir(), targetMigration.filename);
4076
+ if (!existsSync5(filePath)) {
3237
4077
  throw new CLIError(`Local migration file not found: ${targetMigration.filename}`);
3238
4078
  }
3239
4079
  const sql = readFileSync4(filePath, "utf-8");
@@ -3298,8 +4138,8 @@ function registerDbMigrationsCommand(dbCmd2) {
3298
4138
  }
3299
4139
  }
3300
4140
  for (const migration of migrationsToApply) {
3301
- const filePath = join8(getMigrationsDir(), migration.filename);
3302
- if (!existsSync4(filePath)) {
4141
+ const filePath = join9(getMigrationsDir(), migration.filename);
4142
+ if (!existsSync5(filePath)) {
3303
4143
  throw new CLIError(`Local migration file not found: ${migration.filename}`);
3304
4144
  }
3305
4145
  const sql = readFileSync4(filePath, "utf-8");
@@ -3338,8 +4178,8 @@ function registerRecordsCommands(recordsCmd2) {
3338
4178
  if (opts.limit) params.set("limit", String(opts.limit));
3339
4179
  if (opts.offset) params.set("offset", String(opts.offset));
3340
4180
  const query = params.toString();
3341
- const path5 = `/api/database/records/${encodeURIComponent(table)}${query ? `?${query}` : ""}`;
3342
- const res = await ossFetch(path5);
4181
+ const path6 = `/api/database/records/${encodeURIComponent(table)}${query ? `?${query}` : ""}`;
4182
+ const res = await ossFetch(path6);
3343
4183
  const data = await res.json();
3344
4184
  const records = data.data ?? [];
3345
4185
  if (json) {
@@ -3508,18 +4348,18 @@ function registerFunctionsCommands(functionsCmd2) {
3508
4348
  }
3509
4349
 
3510
4350
  // src/commands/functions/deploy.ts
3511
- import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
3512
- import { join as join9 } from "path";
4351
+ import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
4352
+ import { join as join10 } from "path";
3513
4353
  function registerFunctionsDeployCommand(functionsCmd2) {
3514
4354
  functionsCmd2.command("deploy <slug>").description("Deploy an edge function (create or update)").option("--file <path>", "Path to the function source file").option("--name <name>", "Function display name").option("--description <desc>", "Function description").action(async (slug, opts, cmd) => {
3515
4355
  const { json } = getRootOpts(cmd);
3516
4356
  try {
3517
4357
  await requireAuth();
3518
- const filePath = opts.file ?? join9(process.cwd(), "insforge", "functions", slug, "index.ts");
3519
- if (!existsSync5(filePath)) {
4358
+ const filePath = opts.file ?? join10(process.cwd(), "insforge", "functions", slug, "index.ts");
4359
+ if (!existsSync6(filePath)) {
3520
4360
  throw new CLIError(
3521
4361
  `Source file not found: ${filePath}
3522
- Specify --file <path> or create ${join9("insforge", "functions", slug, "index.ts")}`
4362
+ Specify --file <path> or create ${join10("insforge", "functions", slug, "index.ts")}`
3523
4363
  );
3524
4364
  }
3525
4365
  const code = readFileSync5(filePath, "utf-8");
@@ -3643,7 +4483,7 @@ function registerFunctionsCodeCommand(functionsCmd2) {
3643
4483
  }
3644
4484
 
3645
4485
  // src/commands/functions/delete.ts
3646
- import * as clack9 from "@clack/prompts";
4486
+ import * as clack13 from "@clack/prompts";
3647
4487
  function registerFunctionsDeleteCommand(functionsCmd2) {
3648
4488
  functionsCmd2.command("delete <slug>").description("Delete an edge function").action(async (slug, _opts, cmd) => {
3649
4489
  const { json, yes } = getRootOpts(cmd);
@@ -3654,7 +4494,7 @@ function registerFunctionsDeleteCommand(functionsCmd2) {
3654
4494
  message: `Delete function "${slug}"? This cannot be undone.`
3655
4495
  });
3656
4496
  if (isCancel2(confirmed) || !confirmed) {
3657
- clack9.log.info("Cancelled.");
4497
+ clack13.log.info("Cancelled.");
3658
4498
  return;
3659
4499
  }
3660
4500
  }
@@ -3709,8 +4549,8 @@ function registerStorageBucketsCommand(storageCmd2) {
3709
4549
  }
3710
4550
 
3711
4551
  // src/commands/storage/upload.ts
3712
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
3713
- import { basename as basename5 } from "path";
4552
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
4553
+ import { basename as basename6 } from "path";
3714
4554
  function registerStorageUploadCommand(storageCmd2) {
3715
4555
  storageCmd2.command("upload <file>").description("Upload a file to a storage bucket").requiredOption("--bucket <name>", "Target bucket name").option("--key <objectKey>", "Object key (defaults to filename)").action(async (file, opts, cmd) => {
3716
4556
  const { json } = getRootOpts(cmd);
@@ -3718,11 +4558,11 @@ function registerStorageUploadCommand(storageCmd2) {
3718
4558
  await requireAuth();
3719
4559
  const config = getProjectConfig();
3720
4560
  if (!config) throw new ProjectNotLinkedError();
3721
- if (!existsSync6(file)) {
4561
+ if (!existsSync7(file)) {
3722
4562
  throw new CLIError(`File not found: ${file}`);
3723
4563
  }
3724
4564
  const fileContent = readFileSync6(file);
3725
- const objectKey = opts.key ?? basename5(file);
4565
+ const objectKey = opts.key ?? basename6(file);
3726
4566
  const bucketName = opts.bucket;
3727
4567
  const formData = new FormData();
3728
4568
  const blob = new Blob([fileContent]);
@@ -3743,7 +4583,7 @@ function registerStorageUploadCommand(storageCmd2) {
3743
4583
  if (json) {
3744
4584
  outputJson(data);
3745
4585
  } else {
3746
- outputSuccess(`Uploaded "${basename5(file)}" to bucket "${bucketName}".`);
4586
+ outputSuccess(`Uploaded "${basename6(file)}" to bucket "${bucketName}".`);
3747
4587
  }
3748
4588
  } catch (err) {
3749
4589
  handleError(err, json);
@@ -3752,8 +4592,8 @@ function registerStorageUploadCommand(storageCmd2) {
3752
4592
  }
3753
4593
 
3754
4594
  // src/commands/storage/download.ts
3755
- import { writeFileSync as writeFileSync4 } from "fs";
3756
- import { join as join10, basename as basename6 } from "path";
4595
+ import { writeFileSync as writeFileSync5 } from "fs";
4596
+ import { join as join11, basename as basename7 } from "path";
3757
4597
  function registerStorageDownloadCommand(storageCmd2) {
3758
4598
  storageCmd2.command("download <objectKey>").description("Download a file from a storage bucket").requiredOption("--bucket <name>", "Source bucket name").option("--output <path>", "Output file path (defaults to current directory)").action(async (objectKey, opts, cmd) => {
3759
4599
  const { json } = getRootOpts(cmd);
@@ -3773,8 +4613,8 @@ function registerStorageDownloadCommand(storageCmd2) {
3773
4613
  throw new CLIError(err.error ?? `Download failed: ${res.status}`);
3774
4614
  }
3775
4615
  const buffer = Buffer.from(await res.arrayBuffer());
3776
- const outputPath = opts.output ?? join10(process.cwd(), basename6(objectKey));
3777
- writeFileSync4(outputPath, buffer);
4616
+ const outputPath = opts.output ?? join11(process.cwd(), basename7(objectKey));
4617
+ writeFileSync5(outputPath, buffer);
3778
4618
  if (json) {
3779
4619
  outputJson({ success: true, path: outputPath, size: buffer.length });
3780
4620
  } else {
@@ -3818,10 +4658,10 @@ function registerStorageDeleteBucketCommand(storageCmd2) {
3818
4658
  try {
3819
4659
  await requireAuth();
3820
4660
  if (!yes && !json) {
3821
- const confirm3 = await confirm2({
4661
+ const confirm6 = await confirm2({
3822
4662
  message: `Delete bucket "${name}" and all its objects? This cannot be undone.`
3823
4663
  });
3824
- if (isCancel2(confirm3) || !confirm3) {
4664
+ if (isCancel2(confirm6) || !confirm6) {
3825
4665
  process.exit(0);
3826
4666
  }
3827
4667
  }
@@ -4231,8 +5071,8 @@ async function listDocs(json) {
4231
5071
  );
4232
5072
  }
4233
5073
  }
4234
- async function fetchDoc(path5, label, json) {
4235
- const res = await ossFetch(path5);
5074
+ async function fetchDoc(path6, label, json) {
5075
+ const res = await ossFetch(path6);
4236
5076
  const data = await res.json();
4237
5077
  const doc = data.data ?? data;
4238
5078
  if (json) {
@@ -4372,10 +5212,10 @@ function registerSecretsDeleteCommand(secretsCmd2) {
4372
5212
  try {
4373
5213
  await requireAuth();
4374
5214
  if (!yes && !json) {
4375
- const confirm3 = await confirm2({
5215
+ const confirm6 = await confirm2({
4376
5216
  message: `Delete secret "${key}"? This cannot be undone.`
4377
5217
  });
4378
- if (isCancel2(confirm3) || !confirm3) {
5218
+ if (isCancel2(confirm6) || !confirm6) {
4379
5219
  process.exit(0);
4380
5220
  }
4381
5221
  }
@@ -4563,10 +5403,10 @@ function registerSchedulesDeleteCommand(schedulesCmd2) {
4563
5403
  try {
4564
5404
  await requireAuth();
4565
5405
  if (!yes && !json) {
4566
- const confirm3 = await confirm2({
5406
+ const confirm6 = await confirm2({
4567
5407
  message: `Delete schedule "${id}"? This cannot be undone.`
4568
5408
  });
4569
- if (isCancel2(confirm3) || !confirm3) {
5409
+ if (isCancel2(confirm6) || !confirm6) {
4570
5410
  process.exit(0);
4571
5411
  }
4572
5412
  }
@@ -4690,8 +5530,44 @@ function registerComputeGetCommand(computeCmd2) {
4690
5530
  }
4691
5531
 
4692
5532
  // src/commands/compute/update.ts
5533
+ var ENV_KEY_REGEX = /^[A-Z_][A-Z0-9_]*$/;
5534
+ function collect(value, previous) {
5535
+ return previous.concat([value]);
5536
+ }
5537
+ function parseKeyValue(raw) {
5538
+ const eq = raw.indexOf("=");
5539
+ if (eq <= 0) {
5540
+ throw new CLIError(
5541
+ `Invalid --env-set "${raw}": expected KEY=VALUE (key first, then '=', then value)`
5542
+ );
5543
+ }
5544
+ const key = raw.slice(0, eq);
5545
+ const value = raw.slice(eq + 1);
5546
+ if (!ENV_KEY_REGEX.test(key)) {
5547
+ throw new CLIError(`Invalid env var key "${key}": must match [A-Z_][A-Z0-9_]*`);
5548
+ }
5549
+ return [key, value];
5550
+ }
5551
+ function assertValidKey(key) {
5552
+ if (!ENV_KEY_REGEX.test(key)) {
5553
+ throw new CLIError(`Invalid env var key "${key}": must match [A-Z_][A-Z0-9_]*`);
5554
+ }
5555
+ }
4693
5556
  function registerComputeUpdateCommand(computeCmd2) {
4694
- computeCmd2.command("update <id>").description("Update a compute service").option("--image <image>", "Docker image URL").option("--port <port>", "Container port").option("--cpu <tier>", "CPU tier").option("--memory <mb>", "Memory in MB").option("--region <region>", "Fly.io region").option("--env <json>", "Environment variables as JSON object").action(async (id, opts, cmd) => {
5557
+ computeCmd2.command("update <id>").description("Update a compute service").option("--image <image>", "Docker image URL").option("--port <port>", "Container port").option("--cpu <tier>", "CPU tier").option("--memory <mb>", "Memory in MB").option("--region <region>", "Fly.io region").option(
5558
+ "--env <json>",
5559
+ "Replace ALL env vars with this JSON object. To rotate one secret without restating the others, use --env-set instead."
5560
+ ).option(
5561
+ "--env-set <KEY=VALUE>",
5562
+ "Set or update one env var (repeatable). Merges with existing \u2014 does not clear other vars.",
5563
+ collect,
5564
+ []
5565
+ ).option(
5566
+ "--env-unset <KEY>",
5567
+ "Remove one env var (repeatable). Merges with existing \u2014 leaves other vars in place.",
5568
+ collect,
5569
+ []
5570
+ ).action(async (id, opts, cmd) => {
4695
5571
  const { json } = getRootOpts(cmd);
4696
5572
  try {
4697
5573
  await requireAuth();
@@ -4711,6 +5587,14 @@ function registerComputeUpdateCommand(computeCmd2) {
4711
5587
  body.memory = Number(opts.memory);
4712
5588
  }
4713
5589
  if (opts.region) body.region = opts.region;
5590
+ const envSetArgs = opts.envSet;
5591
+ const envUnsetArgs = opts.envUnset;
5592
+ const hasPatch = envSetArgs.length > 0 || envUnsetArgs.length > 0;
5593
+ if (opts.env && hasPatch) {
5594
+ throw new CLIError(
5595
+ "--env (wholesale replace) and --env-set/--env-unset (partial merge) are mutually exclusive \u2014 pick one."
5596
+ );
5597
+ }
4714
5598
  if (opts.env) {
4715
5599
  try {
4716
5600
  body.envVars = JSON.parse(opts.env);
@@ -4718,8 +5602,22 @@ function registerComputeUpdateCommand(computeCmd2) {
4718
5602
  throw new CLIError("Invalid JSON for --env");
4719
5603
  }
4720
5604
  }
5605
+ if (hasPatch) {
5606
+ const setMap = {};
5607
+ for (const arg of envSetArgs) {
5608
+ const [k, v] = parseKeyValue(arg);
5609
+ setMap[k] = v;
5610
+ }
5611
+ for (const k of envUnsetArgs) assertValidKey(k);
5612
+ body.envVarsPatch = {
5613
+ ...envSetArgs.length > 0 && { set: setMap },
5614
+ ...envUnsetArgs.length > 0 && { unset: envUnsetArgs }
5615
+ };
5616
+ }
4721
5617
  if (Object.keys(body).length === 0) {
4722
- throw new CLIError("No update fields provided. Use --image, --port, --cpu, --memory, --region, or --env.");
5618
+ throw new CLIError(
5619
+ "No update fields provided. Use --image, --port, --cpu, --memory, --region, --env, --env-set, or --env-unset."
5620
+ );
4723
5621
  }
4724
5622
  const res = await ossFetch(`/api/compute/services/${encodeURIComponent(id)}`, {
4725
5623
  method: "PATCH",
@@ -4811,46 +5709,86 @@ function registerComputeStopCommand(computeCmd2) {
4811
5709
  });
4812
5710
  }
4813
5711
 
4814
- // src/commands/compute/logs.ts
4815
- function registerComputeLogsCommand(computeCmd2) {
4816
- computeCmd2.command("logs <id>").description("Get compute service logs (machine events)").option("--limit <n>", "Max number of log entries", "50").action(async (id, opts, cmd) => {
5712
+ // src/commands/compute/events.ts
5713
+ function registerComputeEventsCommand(computeCmd2) {
5714
+ computeCmd2.command("events <id>").description("Get compute service machine events (start/stop/exit/restart)").option("--limit <n>", "Max number of event entries", "50").action(async (id, opts, cmd) => {
4817
5715
  const { json } = getRootOpts(cmd);
4818
5716
  try {
4819
5717
  await requireAuth();
4820
5718
  const limit = Math.max(1, Math.min(Number(opts.limit) || 50, 1e3));
4821
5719
  const res = await ossFetch(
4822
- `/api/compute/services/${encodeURIComponent(id)}/logs?limit=${limit}`
5720
+ `/api/compute/services/${encodeURIComponent(id)}/events?limit=${limit}`
4823
5721
  );
4824
- const logs = await res.json();
5722
+ const events = await res.json();
4825
5723
  if (json) {
4826
- outputJson(logs);
5724
+ outputJson(events);
4827
5725
  } else {
4828
- if (!Array.isArray(logs) || logs.length === 0) {
4829
- console.log("No logs found.");
4830
- await reportCliUsage("cli.compute.logs", true);
5726
+ if (!Array.isArray(events) || events.length === 0) {
5727
+ console.log("No events found.");
5728
+ await reportCliUsage("cli.compute.events", true);
4831
5729
  return;
4832
5730
  }
4833
- for (const entry of logs) {
5731
+ for (const entry of events) {
4834
5732
  const ts = new Date(entry.timestamp).toISOString();
4835
5733
  console.log(`${ts} ${entry.message}`);
4836
5734
  }
4837
5735
  }
4838
- await reportCliUsage("cli.compute.logs", true);
5736
+ await reportCliUsage("cli.compute.events", true);
4839
5737
  } catch (err) {
4840
- await reportCliUsage("cli.compute.logs", false);
5738
+ await reportCliUsage("cli.compute.events", false);
4841
5739
  handleError(err, json);
4842
5740
  }
4843
5741
  });
4844
5742
  }
4845
5743
 
4846
5744
  // src/commands/compute/deploy.ts
4847
- import { existsSync as existsSync8 } from "fs";
4848
- import { join as join12, resolve as resolve4 } from "path";
5745
+ import { existsSync as existsSync9 } from "fs";
5746
+ import { join as join13, resolve as resolve4 } from "path";
5747
+
5748
+ // src/lib/env-file.ts
5749
+ import { readFileSync as readFileSync7 } from "fs";
5750
+ var ENV_KEY_REGEX2 = /^[A-Z_][A-Z0-9_]*$/;
5751
+ function parseEnvFile(path6) {
5752
+ let raw;
5753
+ try {
5754
+ raw = readFileSync7(path6, "utf-8");
5755
+ } catch (err) {
5756
+ const msg = err instanceof Error ? err.message : String(err);
5757
+ throw new CLIError(`Could not read --env-file at ${path6}: ${msg}`);
5758
+ }
5759
+ const result = {};
5760
+ const lines = raw.split(/\r?\n/);
5761
+ for (let i = 0; i < lines.length; i++) {
5762
+ const line = lines[i].trim();
5763
+ if (line === "" || line.startsWith("#")) continue;
5764
+ const eq = line.indexOf("=");
5765
+ if (eq <= 0) {
5766
+ throw new CLIError(
5767
+ `${path6}:${i + 1}: expected KEY=VALUE, got "${line}"`
5768
+ );
5769
+ }
5770
+ const key = line.slice(0, eq).trim();
5771
+ if (!ENV_KEY_REGEX2.test(key)) {
5772
+ throw new CLIError(
5773
+ `${path6}:${i + 1}: invalid env var key "${key}" (must match [A-Z_][A-Z0-9_]*)`
5774
+ );
5775
+ }
5776
+ let value = line.slice(eq + 1).trim();
5777
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2 || value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
5778
+ value = value.slice(1, -1);
5779
+ } else {
5780
+ const hash = value.indexOf(" #");
5781
+ if (hash >= 0) value = value.slice(0, hash).trimEnd();
5782
+ }
5783
+ result[key] = value;
5784
+ }
5785
+ return result;
5786
+ }
4849
5787
 
4850
5788
  // src/lib/flyctl.ts
4851
5789
  import { spawn, spawnSync } from "child_process";
4852
- import { existsSync as existsSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
4853
- import { join as join11 } from "path";
5790
+ import { existsSync as existsSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3 } from "fs";
5791
+ import { join as join12 } from "path";
4854
5792
  function ensureFlyctlAvailable() {
4855
5793
  const r = spawnSync("flyctl", ["version"], {
4856
5794
  encoding: "utf8",
@@ -4863,8 +5801,8 @@ function ensureFlyctlAvailable() {
4863
5801
  }
4864
5802
  }
4865
5803
  function ensureFlyTomlStub(opts) {
4866
- const path5 = join11(opts.dir, "fly.toml");
4867
- if (existsSync7(path5)) {
5804
+ const path6 = join12(opts.dir, "fly.toml");
5805
+ if (existsSync8(path6)) {
4868
5806
  return () => {
4869
5807
  };
4870
5808
  }
@@ -4881,10 +5819,10 @@ primary_region = "${opts.region}"
4881
5819
  auto_start_machines = true
4882
5820
  min_machines_running = 0
4883
5821
  `;
4884
- writeFileSync5(path5, stub, "utf8");
5822
+ writeFileSync6(path6, stub, "utf8");
4885
5823
  return () => {
4886
5824
  try {
4887
- unlinkSync2(path5);
5825
+ unlinkSync3(path6);
4888
5826
  } catch {
4889
5827
  }
4890
5828
  };
@@ -4959,7 +5897,10 @@ function registerComputeDeployCommand(computeCmd2) {
4959
5897
  "--cpu <tier>",
4960
5898
  "CPU tier in <kind>-<N>x format (e.g. shared-1x, performance-2x)",
4961
5899
  "shared-1x"
4962
- ).option("--memory <mb>", "Memory in MB", "512").option("--region <region>", "Fly.io region", "iad").option("--env <json>", "Env vars as JSON object").action(async (dir, opts, cmd) => {
5900
+ ).option("--memory <mb>", "Memory in MB", "512").option("--region <region>", "Fly.io region", "iad").option("--env <json>", "Env vars as JSON object").option(
5901
+ "--env-file <path>",
5902
+ "Path to a .env file (KEY=VALUE per line, #-comments + blank lines ok). Mutually exclusive with --env."
5903
+ ).action(async (dir, opts, cmd) => {
4963
5904
  const { json } = getRootOpts(cmd);
4964
5905
  try {
4965
5906
  await requireAuth();
@@ -4979,6 +5920,11 @@ function registerComputeDeployCommand(computeCmd2) {
4979
5920
  if (!Number.isInteger(memory) || memory <= 0) {
4980
5921
  throw new CLIError(`Invalid --memory: ${opts.memory}`);
4981
5922
  }
5923
+ if (opts.env && opts.envFile) {
5924
+ throw new CLIError(
5925
+ "--env and --env-file are mutually exclusive \u2014 pick one source for the env vars."
5926
+ );
5927
+ }
4982
5928
  let envVars;
4983
5929
  if (opts.env) {
4984
5930
  let parsed;
@@ -4998,6 +5944,8 @@ function registerComputeDeployCommand(computeCmd2) {
4998
5944
  }
4999
5945
  }
5000
5946
  envVars = parsed;
5947
+ } else if (opts.envFile) {
5948
+ envVars = parseEnvFile(resolve4(opts.envFile));
5001
5949
  }
5002
5950
  const baseBody = {
5003
5951
  name: opts.name,
@@ -5040,8 +5988,8 @@ function registerComputeDeployCommand(computeCmd2) {
5040
5988
  return;
5041
5989
  }
5042
5990
  const absDir = resolve4(dir);
5043
- const dockerfilePath = join12(absDir, "Dockerfile");
5044
- if (!existsSync8(dockerfilePath)) {
5991
+ const dockerfilePath = join13(absDir, "Dockerfile");
5992
+ if (!existsSync9(dockerfilePath)) {
5045
5993
  throw new CLIError(
5046
5994
  `No Dockerfile at ${dockerfilePath}.
5047
5995
  Either:
@@ -5275,7 +6223,7 @@ function formatSize2(gb) {
5275
6223
 
5276
6224
  // src/commands/diagnose/index.ts
5277
6225
  import * as os from "os";
5278
- import * as clack10 from "@clack/prompts";
6226
+ import * as clack14 from "@clack/prompts";
5279
6227
 
5280
6228
  // src/commands/diagnose/metrics.ts
5281
6229
  var METRIC_LABELS = {
@@ -5816,10 +6764,10 @@ function registerDiagnoseCommands(diagnoseCmd2) {
5816
6764
  if (question.length === 0 || question.length > 2e3) {
5817
6765
  throw new CLIError("Question must be between 1 and 2000 characters.");
5818
6766
  }
5819
- const s = !json ? clack10.spinner() : null;
6767
+ const s = !json ? clack14.spinner() : null;
5820
6768
  s?.start("Collecting diagnostic data...");
5821
6769
  const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
5822
- const cliVersion = "0.1.61";
6770
+ const cliVersion = "0.1.63";
5823
6771
  s?.stop("Data collected");
5824
6772
  if (!json) {
5825
6773
  console.log(`
@@ -5952,9 +6900,9 @@ function registerDiagnoseCommands(diagnoseCmd2) {
5952
6900
  void 0,
5953
6901
  apiUrl
5954
6902
  );
5955
- clack10.log.success("Thanks for your feedback!");
6903
+ clack14.log.success("Thanks for your feedback!");
5956
6904
  } catch {
5957
- clack10.log.warn("Failed to submit rating.");
6905
+ clack14.log.warn("Failed to submit rating.");
5958
6906
  }
5959
6907
  }
5960
6908
  }
@@ -6046,9 +6994,1145 @@ function formatBytesCompact(bytes) {
6046
6994
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
6047
6995
  }
6048
6996
 
6997
+ // src/lib/api/payments.ts
6998
+ function withQuery(path6, params) {
6999
+ const query = new URLSearchParams();
7000
+ for (const [key, value] of Object.entries(params)) {
7001
+ if (value !== void 0) query.set(key, String(value));
7002
+ }
7003
+ const suffix = query.toString();
7004
+ return suffix ? `${path6}?${suffix}` : path6;
7005
+ }
7006
+ async function readJson(res) {
7007
+ return await res.json();
7008
+ }
7009
+ function withEnvironmentPath(environment, suffix) {
7010
+ return `/api/payments/${encodeURIComponent(environment)}${suffix}`;
7011
+ }
7012
+ async function getPaymentsStatus() {
7013
+ return readJson(await ossFetch("/api/payments/status"));
7014
+ }
7015
+ async function getPaymentsConfig() {
7016
+ return readJson(await ossFetch("/api/payments/config"));
7017
+ }
7018
+ async function setStripeSecretKey(environment, secretKey) {
7019
+ return readJson(
7020
+ await ossFetch(withEnvironmentPath(environment, "/config"), {
7021
+ method: "PUT",
7022
+ body: JSON.stringify({ secretKey })
7023
+ })
7024
+ );
7025
+ }
7026
+ async function removeStripeSecretKey(environment) {
7027
+ return readJson(
7028
+ await ossFetch(withEnvironmentPath(environment, "/config"), {
7029
+ method: "DELETE"
7030
+ })
7031
+ );
7032
+ }
7033
+ async function syncPayments(environment = "all") {
7034
+ return readJson(
7035
+ await ossFetch(
7036
+ environment === "all" ? "/api/payments/sync" : withEnvironmentPath(environment, "/sync"),
7037
+ { method: "POST" }
7038
+ )
7039
+ );
7040
+ }
7041
+ async function configurePaymentWebhook(environment) {
7042
+ return readJson(
7043
+ await ossFetch(withEnvironmentPath(environment, "/webhook"), {
7044
+ method: "POST"
7045
+ })
7046
+ );
7047
+ }
7048
+ async function listPaymentCatalog(environment) {
7049
+ return readJson(await ossFetch(withEnvironmentPath(environment, "/catalog")));
7050
+ }
7051
+ async function listPaymentProducts(environment) {
7052
+ return readJson(
7053
+ await ossFetch(withEnvironmentPath(environment, "/catalog/products"))
7054
+ );
7055
+ }
7056
+ async function getPaymentProduct(environment, productId) {
7057
+ return readJson(
7058
+ await ossFetch(
7059
+ withEnvironmentPath(
7060
+ environment,
7061
+ `/catalog/products/${encodeURIComponent(productId)}`
7062
+ )
7063
+ )
7064
+ );
7065
+ }
7066
+ async function createPaymentProduct(environment, request) {
7067
+ return readJson(
7068
+ await ossFetch(withEnvironmentPath(environment, "/catalog/products"), {
7069
+ method: "POST",
7070
+ body: JSON.stringify(request)
7071
+ })
7072
+ );
7073
+ }
7074
+ async function updatePaymentProduct(environment, productId, request) {
7075
+ return readJson(
7076
+ await ossFetch(
7077
+ withEnvironmentPath(
7078
+ environment,
7079
+ `/catalog/products/${encodeURIComponent(productId)}`
7080
+ ),
7081
+ {
7082
+ method: "PATCH",
7083
+ body: JSON.stringify(request)
7084
+ }
7085
+ )
7086
+ );
7087
+ }
7088
+ async function deletePaymentProduct(environment, productId) {
7089
+ return readJson(
7090
+ await ossFetch(
7091
+ withEnvironmentPath(
7092
+ environment,
7093
+ `/catalog/products/${encodeURIComponent(productId)}`
7094
+ ),
7095
+ { method: "DELETE" }
7096
+ )
7097
+ );
7098
+ }
7099
+ async function listPaymentPrices(environment, stripeProductId) {
7100
+ return readJson(
7101
+ await ossFetch(
7102
+ withQuery(withEnvironmentPath(environment, "/catalog/prices"), {
7103
+ stripeProductId
7104
+ })
7105
+ )
7106
+ );
7107
+ }
7108
+ async function getPaymentPrice(environment, priceId) {
7109
+ return readJson(
7110
+ await ossFetch(
7111
+ withEnvironmentPath(
7112
+ environment,
7113
+ `/catalog/prices/${encodeURIComponent(priceId)}`
7114
+ )
7115
+ )
7116
+ );
7117
+ }
7118
+ async function createPaymentPrice(environment, request) {
7119
+ return readJson(
7120
+ await ossFetch(withEnvironmentPath(environment, "/catalog/prices"), {
7121
+ method: "POST",
7122
+ body: JSON.stringify(request)
7123
+ })
7124
+ );
7125
+ }
7126
+ async function updatePaymentPrice(environment, priceId, request) {
7127
+ return readJson(
7128
+ await ossFetch(
7129
+ withEnvironmentPath(
7130
+ environment,
7131
+ `/catalog/prices/${encodeURIComponent(priceId)}`
7132
+ ),
7133
+ {
7134
+ method: "PATCH",
7135
+ body: JSON.stringify(request)
7136
+ }
7137
+ )
7138
+ );
7139
+ }
7140
+ async function archivePaymentPrice(environment, priceId) {
7141
+ return readJson(
7142
+ await ossFetch(
7143
+ withEnvironmentPath(
7144
+ environment,
7145
+ `/catalog/prices/${encodeURIComponent(priceId)}`
7146
+ ),
7147
+ { method: "DELETE" }
7148
+ )
7149
+ );
7150
+ }
7151
+ async function listSubscriptions(environment, request) {
7152
+ return readJson(
7153
+ await ossFetch(
7154
+ withQuery(withEnvironmentPath(environment, "/subscriptions"), request)
7155
+ )
7156
+ );
7157
+ }
7158
+ async function listPaymentCustomers(environment, request = {}) {
7159
+ return readJson(
7160
+ await ossFetch(
7161
+ withQuery(withEnvironmentPath(environment, "/customers"), request)
7162
+ )
7163
+ );
7164
+ }
7165
+ async function listPaymentHistory(environment, request) {
7166
+ return readJson(
7167
+ await ossFetch(
7168
+ withQuery(withEnvironmentPath(environment, "/payment-history"), request)
7169
+ )
7170
+ );
7171
+ }
7172
+
7173
+ // src/commands/payments/utils.ts
7174
+ function parseEnvironment(value) {
7175
+ if (value === "test" || value === "live") return value;
7176
+ throw new CLIError('Environment must be "test" or "live".');
7177
+ }
7178
+ function parseEnvironmentOrAll(value) {
7179
+ if (value === "all") return value;
7180
+ return parseEnvironment(value);
7181
+ }
7182
+ function parseBooleanOption(value, flagName) {
7183
+ if (value === void 0) return void 0;
7184
+ const normalized = value.toLowerCase();
7185
+ if (normalized === "true") return true;
7186
+ if (normalized === "false") return false;
7187
+ throw new CLIError(`${flagName} must be "true" or "false".`);
7188
+ }
7189
+ function parseIntegerOption(value, flagName, options = {}) {
7190
+ if (value === void 0) return void 0;
7191
+ const parsed = Number.parseInt(value, 10);
7192
+ if (!Number.isInteger(parsed) || String(parsed) !== value.trim()) {
7193
+ throw new CLIError(`${flagName} must be an integer.`);
7194
+ }
7195
+ if (options.min !== void 0 && parsed < options.min) {
7196
+ throw new CLIError(`${flagName} must be at least ${options.min}.`);
7197
+ }
7198
+ if (options.max !== void 0 && parsed > options.max) {
7199
+ throw new CLIError(`${flagName} must be at most ${options.max}.`);
7200
+ }
7201
+ return parsed;
7202
+ }
7203
+ function parseMetadataOption(value) {
7204
+ if (value === void 0) return void 0;
7205
+ let parsed;
7206
+ try {
7207
+ parsed = JSON.parse(value);
7208
+ } catch {
7209
+ throw new CLIError("Invalid JSON for --metadata.");
7210
+ }
7211
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
7212
+ throw new CLIError("--metadata must be a JSON object.");
7213
+ }
7214
+ const metadata = {};
7215
+ for (const [key, raw] of Object.entries(parsed)) {
7216
+ if (typeof raw !== "string") {
7217
+ throw new CLIError(`Metadata value for "${key}" must be a string.`);
7218
+ }
7219
+ metadata[key] = raw;
7220
+ }
7221
+ return metadata;
7222
+ }
7223
+ function formatDate(value) {
7224
+ if (!value) return "-";
7225
+ const date = new Date(value);
7226
+ return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
7227
+ }
7228
+ function formatAmount(amount, currency) {
7229
+ if (amount === null || amount === void 0) return "-";
7230
+ const code = currency?.toUpperCase();
7231
+ let fractionDigits = 2;
7232
+ if (code) {
7233
+ try {
7234
+ fractionDigits = new Intl.NumberFormat(void 0, {
7235
+ style: "currency",
7236
+ currency: code
7237
+ }).resolvedOptions().maximumFractionDigits;
7238
+ } catch {
7239
+ fractionDigits = 2;
7240
+ }
7241
+ }
7242
+ const divisor = 10 ** fractionDigits;
7243
+ return `${(amount / divisor).toFixed(fractionDigits)} ${code ?? ""}`.trim();
7244
+ }
7245
+ function formatRecurring(interval, intervalCount) {
7246
+ if (!interval) return "one-time";
7247
+ return `${intervalCount && intervalCount > 1 ? `${intervalCount} ` : ""}${interval}`;
7248
+ }
7249
+ async function trackPaymentUsage(subcommand, success, properties = {}) {
7250
+ try {
7251
+ try {
7252
+ const config = getProjectConfig();
7253
+ if (config) {
7254
+ trackPayments(subcommand, config, {
7255
+ success,
7256
+ ...properties
7257
+ });
7258
+ }
7259
+ } catch {
7260
+ }
7261
+ } finally {
7262
+ await shutdownAnalytics();
7263
+ }
7264
+ }
7265
+
7266
+ // src/commands/payments/catalog.ts
7267
+ function registerPaymentsCatalogCommand(paymentsCmd2) {
7268
+ paymentsCmd2.command("catalog").description("List mirrored Stripe products and prices for one environment").requiredOption(
7269
+ "--environment <environment>",
7270
+ "Stripe environment: test or live"
7271
+ ).action(async (opts, cmd) => {
7272
+ const { json } = getRootOpts(cmd);
7273
+ try {
7274
+ const environment = parseEnvironment(opts.environment);
7275
+ await requireAuth();
7276
+ const data = await listPaymentCatalog(environment);
7277
+ if (json) {
7278
+ outputJson(data);
7279
+ } else {
7280
+ if (data.products.length === 0 && data.prices.length === 0) {
7281
+ console.log("No Stripe catalog records found.");
7282
+ await trackPaymentUsage("catalog", true, { environment });
7283
+ return;
7284
+ }
7285
+ if (data.products.length > 0) {
7286
+ console.log("Products");
7287
+ outputTable(
7288
+ ["Env", "Product ID", "Name", "Active", "Default Price"],
7289
+ data.products.map((product) => [
7290
+ product.environment,
7291
+ product.stripeProductId,
7292
+ product.name,
7293
+ product.active ? "Yes" : "No",
7294
+ product.defaultPriceId ?? "-"
7295
+ ])
7296
+ );
7297
+ }
7298
+ if (data.prices.length > 0) {
7299
+ console.log("Prices");
7300
+ outputTable(
7301
+ [
7302
+ "Env",
7303
+ "Price ID",
7304
+ "Product ID",
7305
+ "Amount",
7306
+ "Type",
7307
+ "Active",
7308
+ "Recurring"
7309
+ ],
7310
+ data.prices.map((price) => [
7311
+ price.environment,
7312
+ price.stripePriceId,
7313
+ price.stripeProductId ?? "-",
7314
+ formatAmount(price.unitAmount, price.currency),
7315
+ price.type,
7316
+ price.active ? "Yes" : "No",
7317
+ formatRecurring(
7318
+ price.recurringInterval,
7319
+ price.recurringIntervalCount
7320
+ )
7321
+ ])
7322
+ );
7323
+ }
7324
+ }
7325
+ await trackPaymentUsage("catalog", true, { environment });
7326
+ } catch (err) {
7327
+ await trackPaymentUsage("catalog", false, {
7328
+ environment: opts.environment
7329
+ });
7330
+ handleError(err, json);
7331
+ }
7332
+ });
7333
+ }
7334
+
7335
+ // src/commands/payments/config.ts
7336
+ function outputConfigTable(data) {
7337
+ if (data.keys.length === 0) {
7338
+ console.log("No Stripe keys configured.");
7339
+ return;
7340
+ }
7341
+ outputTable(
7342
+ ["Env", "Configured", "Key"],
7343
+ data.keys.map((key) => [
7344
+ key.environment,
7345
+ key.hasKey ? "Yes" : "No",
7346
+ key.maskedKey ?? "-"
7347
+ ])
7348
+ );
7349
+ }
7350
+ function registerPaymentsConfigCommand(paymentsCmd2) {
7351
+ const configCmd = paymentsCmd2.command("config").description("Manage Stripe API keys for payments").action(async (_opts, cmd) => {
7352
+ const { json } = getRootOpts(cmd);
7353
+ try {
7354
+ await requireAuth();
7355
+ const data = await getPaymentsConfig();
7356
+ if (json) {
7357
+ outputJson(data);
7358
+ } else {
7359
+ outputConfigTable(data);
7360
+ }
7361
+ await trackPaymentUsage("config", true);
7362
+ } catch (err) {
7363
+ await trackPaymentUsage("config", false);
7364
+ handleError(err, json);
7365
+ }
7366
+ });
7367
+ configCmd.command("set <environment> [secretKey]").description("Configure a Stripe secret key for test or live payments").action(async (environmentValue, secretKeyValue, _opts, cmd) => {
7368
+ const { json } = getRootOpts(cmd);
7369
+ try {
7370
+ const environment = parseEnvironment(environmentValue);
7371
+ await requireAuth();
7372
+ let secretKey = secretKeyValue;
7373
+ if (!secretKey) {
7374
+ if (json) {
7375
+ throw new CLIError("Provide secretKey when using --json.");
7376
+ }
7377
+ const input = await password2({
7378
+ message: `Stripe ${environment} secret key`
7379
+ });
7380
+ if (isCancel2(input)) process.exit(0);
7381
+ secretKey = input;
7382
+ }
7383
+ const data = await setStripeSecretKey(environment, secretKey);
7384
+ if (json) {
7385
+ outputJson(data);
7386
+ } else {
7387
+ outputSuccess(`Stripe ${environment} key configured.`);
7388
+ }
7389
+ await trackPaymentUsage("config.set", true, { environment });
7390
+ } catch (err) {
7391
+ await trackPaymentUsage("config.set", false, { environment: environmentValue });
7392
+ handleError(err, json);
7393
+ }
7394
+ });
7395
+ configCmd.command("remove <environment>").alias("delete").description("Remove a configured Stripe secret key").action(async (environmentValue, _opts, cmd) => {
7396
+ const { json, yes } = getRootOpts(cmd);
7397
+ try {
7398
+ const environment = parseEnvironment(environmentValue);
7399
+ await requireAuth();
7400
+ if (json && !yes) {
7401
+ throw new CLIError("Use --yes with --json to remove a Stripe key non-interactively.");
7402
+ }
7403
+ if (!yes) {
7404
+ const confirm6 = await confirm2({
7405
+ message: `Remove Stripe ${environment} key? Payment sync and mutations for this environment will stop.`
7406
+ });
7407
+ if (isCancel2(confirm6) || !confirm6) process.exit(0);
7408
+ }
7409
+ const data = await removeStripeSecretKey(environment);
7410
+ if (json) {
7411
+ outputJson(data);
7412
+ } else {
7413
+ outputSuccess(`Stripe ${environment} key removed.`);
7414
+ }
7415
+ await trackPaymentUsage("config.remove", true, { environment });
7416
+ } catch (err) {
7417
+ await trackPaymentUsage("config.remove", false, { environment: environmentValue });
7418
+ handleError(err, json);
7419
+ }
7420
+ });
7421
+ }
7422
+
7423
+ // src/commands/payments/customers.ts
7424
+ function formatPaymentMethod(customer) {
7425
+ if (customer.paymentMethodBrand && customer.paymentMethodLast4) {
7426
+ return `${customer.paymentMethodBrand} **** ${customer.paymentMethodLast4}`;
7427
+ }
7428
+ if (customer.paymentMethodBrand) {
7429
+ return customer.paymentMethodBrand;
7430
+ }
7431
+ if (customer.paymentMethodLast4) {
7432
+ return `**** ${customer.paymentMethodLast4}`;
7433
+ }
7434
+ return "-";
7435
+ }
7436
+ function registerPaymentsCustomersCommand(paymentsCmd2) {
7437
+ paymentsCmd2.command("customers").description("List mirrored Stripe customers").requiredOption(
7438
+ "--environment <environment>",
7439
+ "Stripe environment: test or live"
7440
+ ).option("--limit <limit>", "Maximum rows to return (1-100)", "50").action(async (opts, cmd) => {
7441
+ const { json } = getRootOpts(cmd);
7442
+ try {
7443
+ const environment = parseEnvironment(opts.environment);
7444
+ const limit = parseIntegerOption(opts.limit, "--limit", { min: 1, max: 100 }) ?? 50;
7445
+ await requireAuth();
7446
+ const data = await listPaymentCustomers(environment, { limit });
7447
+ if (json) {
7448
+ outputJson(data);
7449
+ } else if (data.customers.length === 0) {
7450
+ console.log("No Stripe customers found.");
7451
+ } else {
7452
+ outputTable(
7453
+ [
7454
+ "Customer ID",
7455
+ "Email",
7456
+ "Name",
7457
+ "Payments",
7458
+ "Total Spend",
7459
+ "Last Payment",
7460
+ "Method",
7461
+ "Country"
7462
+ ],
7463
+ data.customers.map((customer) => [
7464
+ customer.stripeCustomerId,
7465
+ customer.email ?? "-",
7466
+ customer.name ?? "-",
7467
+ String(customer.paymentsCount),
7468
+ formatAmount(customer.totalSpend, customer.totalSpendCurrency),
7469
+ formatDate(customer.lastPaymentAt),
7470
+ formatPaymentMethod(customer),
7471
+ customer.countryCode?.toUpperCase() ?? "-"
7472
+ ])
7473
+ );
7474
+ }
7475
+ await trackPaymentUsage("customers", true, { environment });
7476
+ } catch (err) {
7477
+ await trackPaymentUsage("customers", false, {
7478
+ environment: opts.environment
7479
+ });
7480
+ handleError(err, json);
7481
+ }
7482
+ });
7483
+ }
7484
+
7485
+ // src/commands/payments/history.ts
7486
+ function registerPaymentsHistoryCommand(paymentsCmd2) {
7487
+ paymentsCmd2.command("history").description("List mirrored Stripe payment history").requiredOption(
7488
+ "--environment <environment>",
7489
+ "Stripe environment: test or live"
7490
+ ).option("--subject-type <type>", "Filter by billing subject type").option("--subject-id <id>", "Filter by billing subject id").option("--limit <limit>", "Maximum rows to return (1-100)", "50").action(async (opts, cmd) => {
7491
+ const { json } = getRootOpts(cmd);
7492
+ try {
7493
+ const environment = parseEnvironment(opts.environment);
7494
+ const limit = parseIntegerOption(opts.limit, "--limit", { min: 1, max: 100 }) ?? 50;
7495
+ await requireAuth();
7496
+ const data = await listPaymentHistory(environment, {
7497
+ limit,
7498
+ ...opts.subjectType !== void 0 ? { subjectType: opts.subjectType } : {},
7499
+ ...opts.subjectId !== void 0 ? { subjectId: opts.subjectId } : {}
7500
+ });
7501
+ if (json) {
7502
+ outputJson(data);
7503
+ } else if (data.paymentHistory.length === 0) {
7504
+ console.log("No Stripe payment history found.");
7505
+ } else {
7506
+ outputTable(
7507
+ [
7508
+ "Type",
7509
+ "Status",
7510
+ "Subject",
7511
+ "Amount",
7512
+ "Customer",
7513
+ "Stripe Object",
7514
+ "When"
7515
+ ],
7516
+ data.paymentHistory.map((entry) => [
7517
+ entry.type,
7518
+ entry.status,
7519
+ entry.subjectType && entry.subjectId ? `${entry.subjectType}:${entry.subjectId}` : "-",
7520
+ formatAmount(entry.amount, entry.currency),
7521
+ entry.stripeCustomerId ?? "-",
7522
+ entry.stripeCheckoutSessionId ?? entry.stripeInvoiceId ?? entry.stripePaymentIntentId ?? entry.stripeRefundId ?? "-",
7523
+ formatDate(
7524
+ entry.paidAt ?? entry.failedAt ?? entry.refundedAt ?? entry.stripeCreatedAt
7525
+ )
7526
+ ])
7527
+ );
7528
+ }
7529
+ await trackPaymentUsage("history", true, { environment });
7530
+ } catch (err) {
7531
+ await trackPaymentUsage("history", false, {
7532
+ environment: opts.environment
7533
+ });
7534
+ handleError(err, json);
7535
+ }
7536
+ });
7537
+ }
7538
+
7539
+ // src/commands/payments/prices.ts
7540
+ function nullableString(value) {
7541
+ if (value === void 0) return void 0;
7542
+ return value === "null" ? null : value;
7543
+ }
7544
+ function parseRecurringInterval(value) {
7545
+ if (value === void 0) {
7546
+ return void 0;
7547
+ }
7548
+ if (value === "day" || value === "week" || value === "month" || value === "year") {
7549
+ return value;
7550
+ }
7551
+ throw new CLIError("--interval must be one of: day, week, month, year.");
7552
+ }
7553
+ function parseTaxBehavior(value) {
7554
+ if (value === void 0) {
7555
+ return void 0;
7556
+ }
7557
+ if (value === "exclusive" || value === "inclusive" || value === "unspecified") {
7558
+ return value;
7559
+ }
7560
+ throw new CLIError(
7561
+ "--tax-behavior must be one of: exclusive, inclusive, unspecified."
7562
+ );
7563
+ }
7564
+ function outputPricesTable(prices) {
7565
+ if (prices.length === 0) {
7566
+ console.log("No Stripe prices found.");
7567
+ return;
7568
+ }
7569
+ outputTable(
7570
+ [
7571
+ "Env",
7572
+ "Price ID",
7573
+ "Product ID",
7574
+ "Amount",
7575
+ "Type",
7576
+ "Active",
7577
+ "Recurring",
7578
+ "Synced At"
7579
+ ],
7580
+ prices.map((price) => [
7581
+ price.environment,
7582
+ price.stripePriceId,
7583
+ price.stripeProductId ?? "-",
7584
+ formatAmount(price.unitAmount, price.currency),
7585
+ price.type,
7586
+ price.active ? "Yes" : "No",
7587
+ formatRecurring(price.recurringInterval, price.recurringIntervalCount),
7588
+ formatDate(price.syncedAt)
7589
+ ])
7590
+ );
7591
+ }
7592
+ function registerPaymentsPricesCommand(paymentsCmd2) {
7593
+ const pricesCmd = paymentsCmd2.command("prices").description("Manage Stripe prices");
7594
+ pricesCmd.command("list").description("List mirrored Stripe prices").requiredOption(
7595
+ "--environment <environment>",
7596
+ "Stripe environment: test or live"
7597
+ ).option("--product <productId>", "Filter by Stripe product id").action(async (opts, cmd) => {
7598
+ const { json } = getRootOpts(cmd);
7599
+ try {
7600
+ const environment = parseEnvironment(opts.environment);
7601
+ await requireAuth();
7602
+ const data = await listPaymentPrices(environment, opts.product);
7603
+ if (json) {
7604
+ outputJson(data);
7605
+ } else {
7606
+ outputPricesTable(data.prices);
7607
+ }
7608
+ await trackPaymentUsage("prices.list", true, { environment });
7609
+ } catch (err) {
7610
+ await trackPaymentUsage("prices.list", false, {
7611
+ environment: opts.environment
7612
+ });
7613
+ handleError(err, json);
7614
+ }
7615
+ });
7616
+ pricesCmd.command("get <priceId>").description("Show one Stripe price").requiredOption(
7617
+ "--environment <environment>",
7618
+ "Stripe environment: test or live"
7619
+ ).action(async (priceId, opts, cmd) => {
7620
+ const { json } = getRootOpts(cmd);
7621
+ try {
7622
+ const environment = parseEnvironment(opts.environment);
7623
+ await requireAuth();
7624
+ const data = await getPaymentPrice(environment, priceId);
7625
+ if (json) {
7626
+ outputJson(data);
7627
+ } else {
7628
+ outputPricesTable([data.price]);
7629
+ }
7630
+ await trackPaymentUsage("prices.get", true, { environment });
7631
+ } catch (err) {
7632
+ await trackPaymentUsage("prices.get", false, {
7633
+ environment: opts.environment
7634
+ });
7635
+ handleError(err, json);
7636
+ }
7637
+ });
7638
+ pricesCmd.command("create").description("Create a Stripe one-time or recurring price").requiredOption(
7639
+ "--environment <environment>",
7640
+ "Stripe environment: test or live"
7641
+ ).requiredOption("--product <productId>", "Stripe product id").requiredOption(
7642
+ "--currency <currency>",
7643
+ "Three-letter currency code, e.g. usd"
7644
+ ).requiredOption(
7645
+ "--unit-amount <amount>",
7646
+ "Unit amount in the smallest currency unit, e.g. cents"
7647
+ ).option(
7648
+ "--interval <interval>",
7649
+ "Recurring interval: day, week, month, or year"
7650
+ ).option("--interval-count <count>", "Recurring interval count").option("--lookup-key <key>", 'Stripe lookup key, or "null"').option("--active <bool>", "Set active status (true/false)").option("--tax-behavior <behavior>", "exclusive, inclusive, or unspecified").option("--metadata <json>", "Metadata JSON object with string values").option("--idempotency-key <key>", "Caller-stable idempotency key").action(async (opts, cmd) => {
7651
+ const { json } = getRootOpts(cmd);
7652
+ try {
7653
+ const environment = parseEnvironment(opts.environment);
7654
+ await requireAuth();
7655
+ const interval = parseRecurringInterval(opts.interval);
7656
+ const intervalCount = parseIntegerOption(
7657
+ opts.intervalCount,
7658
+ "--interval-count",
7659
+ { min: 1 }
7660
+ );
7661
+ if (!interval && intervalCount !== void 0) {
7662
+ throw new CLIError("Provide --interval when using --interval-count.");
7663
+ }
7664
+ const request = {
7665
+ stripeProductId: opts.product,
7666
+ currency: opts.currency,
7667
+ unitAmount: parseIntegerOption(opts.unitAmount, "--unit-amount", { min: 0 }) ?? 0
7668
+ };
7669
+ const lookupKey = nullableString(opts.lookupKey);
7670
+ const active = parseBooleanOption(opts.active, "--active");
7671
+ const taxBehavior = parseTaxBehavior(opts.taxBehavior);
7672
+ const metadata = parseMetadataOption(opts.metadata);
7673
+ if (lookupKey !== void 0) request.lookupKey = lookupKey;
7674
+ if (active !== void 0) request.active = active;
7675
+ if (taxBehavior !== void 0) request.taxBehavior = taxBehavior;
7676
+ if (metadata !== void 0) request.metadata = metadata;
7677
+ if (opts.idempotencyKey !== void 0) {
7678
+ request.idempotencyKey = opts.idempotencyKey;
7679
+ }
7680
+ if (interval) {
7681
+ request.recurring = {
7682
+ interval,
7683
+ ...intervalCount !== void 0 ? { intervalCount } : {}
7684
+ };
7685
+ }
7686
+ const data = await createPaymentPrice(environment, request);
7687
+ if (json) {
7688
+ outputJson(data);
7689
+ } else {
7690
+ outputSuccess(`Stripe price created: ${data.price.stripePriceId}`);
7691
+ }
7692
+ await trackPaymentUsage("prices.create", true, { environment });
7693
+ } catch (err) {
7694
+ await trackPaymentUsage("prices.create", false, {
7695
+ environment: opts.environment
7696
+ });
7697
+ handleError(err, json);
7698
+ }
7699
+ });
7700
+ pricesCmd.command("update <priceId>").description("Update a Stripe price").requiredOption(
7701
+ "--environment <environment>",
7702
+ "Stripe environment: test or live"
7703
+ ).option("--active <bool>", "Set active status (true/false)").option("--lookup-key <key>", 'Stripe lookup key, or "null"').option("--tax-behavior <behavior>", "exclusive, inclusive, or unspecified").option("--metadata <json>", "Metadata JSON object with string values").action(async (priceId, opts, cmd) => {
7704
+ const { json } = getRootOpts(cmd);
7705
+ try {
7706
+ const environment = parseEnvironment(opts.environment);
7707
+ await requireAuth();
7708
+ const request = {};
7709
+ const active = parseBooleanOption(opts.active, "--active");
7710
+ const lookupKey = nullableString(opts.lookupKey);
7711
+ const taxBehavior = parseTaxBehavior(opts.taxBehavior);
7712
+ const metadata = parseMetadataOption(opts.metadata);
7713
+ if (active !== void 0) request.active = active;
7714
+ if (lookupKey !== void 0) request.lookupKey = lookupKey;
7715
+ if (taxBehavior !== void 0) request.taxBehavior = taxBehavior;
7716
+ if (metadata !== void 0) request.metadata = metadata;
7717
+ if (Object.keys(request).length === 0) {
7718
+ throw new CLIError(
7719
+ "Provide at least one option to update (--active, --lookup-key, --tax-behavior, --metadata)."
7720
+ );
7721
+ }
7722
+ const data = await updatePaymentPrice(environment, priceId, request);
7723
+ if (json) {
7724
+ outputJson(data);
7725
+ } else {
7726
+ outputSuccess(`Stripe price updated: ${data.price.stripePriceId}`);
7727
+ }
7728
+ await trackPaymentUsage("prices.update", true, { environment });
7729
+ } catch (err) {
7730
+ await trackPaymentUsage("prices.update", false, {
7731
+ environment: opts.environment
7732
+ });
7733
+ handleError(err, json);
7734
+ }
7735
+ });
7736
+ pricesCmd.command("archive <priceId>").alias("delete").description("Archive a Stripe price").requiredOption(
7737
+ "--environment <environment>",
7738
+ "Stripe environment: test or live"
7739
+ ).action(async (priceId, opts, cmd) => {
7740
+ const { json } = getRootOpts(cmd);
7741
+ try {
7742
+ const environment = parseEnvironment(opts.environment);
7743
+ await requireAuth();
7744
+ const data = await archivePaymentPrice(environment, priceId);
7745
+ if (json) {
7746
+ outputJson(data);
7747
+ } else {
7748
+ outputSuccess(`Stripe price archived: ${data.price.stripePriceId}`);
7749
+ }
7750
+ await trackPaymentUsage("prices.archive", true, { environment });
7751
+ } catch (err) {
7752
+ await trackPaymentUsage("prices.archive", false, {
7753
+ environment: opts.environment
7754
+ });
7755
+ handleError(err, json);
7756
+ }
7757
+ });
7758
+ }
7759
+
7760
+ // src/commands/payments/products.ts
7761
+ function nullableString2(value) {
7762
+ if (value === void 0) return void 0;
7763
+ return value === "null" ? null : value;
7764
+ }
7765
+ function outputProductsTable(products) {
7766
+ if (products.length === 0) {
7767
+ console.log("No Stripe products found.");
7768
+ return;
7769
+ }
7770
+ outputTable(
7771
+ ["Env", "Product ID", "Name", "Active", "Default Price", "Synced At"],
7772
+ products.map((product) => [
7773
+ product.environment,
7774
+ product.stripeProductId,
7775
+ product.name,
7776
+ product.active ? "Yes" : "No",
7777
+ product.defaultPriceId ?? "-",
7778
+ formatDate(product.syncedAt)
7779
+ ])
7780
+ );
7781
+ }
7782
+ function registerPaymentsProductsCommand(paymentsCmd2) {
7783
+ const productsCmd = paymentsCmd2.command("products").description("Manage Stripe products");
7784
+ productsCmd.command("list").description("List mirrored Stripe products").requiredOption(
7785
+ "--environment <environment>",
7786
+ "Stripe environment: test or live"
7787
+ ).action(async (opts, cmd) => {
7788
+ const { json } = getRootOpts(cmd);
7789
+ try {
7790
+ const environment = parseEnvironment(opts.environment);
7791
+ await requireAuth();
7792
+ const data = await listPaymentProducts(environment);
7793
+ if (json) {
7794
+ outputJson(data);
7795
+ } else {
7796
+ outputProductsTable(data.products);
7797
+ }
7798
+ await trackPaymentUsage("products.list", true, { environment });
7799
+ } catch (err) {
7800
+ await trackPaymentUsage("products.list", false, {
7801
+ environment: opts.environment
7802
+ });
7803
+ handleError(err, json);
7804
+ }
7805
+ });
7806
+ productsCmd.command("get <productId>").description("Show one Stripe product and its prices").requiredOption(
7807
+ "--environment <environment>",
7808
+ "Stripe environment: test or live"
7809
+ ).action(async (productId, opts, cmd) => {
7810
+ const { json } = getRootOpts(cmd);
7811
+ try {
7812
+ const environment = parseEnvironment(opts.environment);
7813
+ await requireAuth();
7814
+ const data = await getPaymentProduct(environment, productId);
7815
+ if (json) {
7816
+ outputJson(data);
7817
+ } else {
7818
+ outputProductsTable([data.product]);
7819
+ if (data.prices.length > 0) {
7820
+ console.log("Prices");
7821
+ outputTable(
7822
+ ["Price ID", "Amount", "Type", "Active", "Lookup Key"],
7823
+ data.prices.map((price) => [
7824
+ price.stripePriceId,
7825
+ formatAmount(price.unitAmount, price.currency),
7826
+ price.type,
7827
+ price.active ? "Yes" : "No",
7828
+ price.lookupKey ?? "-"
7829
+ ])
7830
+ );
7831
+ }
7832
+ }
7833
+ await trackPaymentUsage("products.get", true, { environment });
7834
+ } catch (err) {
7835
+ await trackPaymentUsage("products.get", false, {
7836
+ environment: opts.environment
7837
+ });
7838
+ handleError(err, json);
7839
+ }
7840
+ });
7841
+ productsCmd.command("create").description("Create a Stripe product").requiredOption(
7842
+ "--environment <environment>",
7843
+ "Stripe environment: test or live"
7844
+ ).requiredOption("--name <name>", "Product name").option("--description <description>", 'Product description, or "null"').option("--active <bool>", "Set active status (true/false)").option("--metadata <json>", "Metadata JSON object with string values").option("--idempotency-key <key>", "Caller-stable idempotency key").action(async (opts, cmd) => {
7845
+ const { json } = getRootOpts(cmd);
7846
+ try {
7847
+ const environment = parseEnvironment(opts.environment);
7848
+ await requireAuth();
7849
+ const request = { name: opts.name };
7850
+ const description = nullableString2(opts.description);
7851
+ const active = parseBooleanOption(opts.active, "--active");
7852
+ const metadata = parseMetadataOption(opts.metadata);
7853
+ if (description !== void 0) request.description = description;
7854
+ if (active !== void 0) request.active = active;
7855
+ if (metadata !== void 0) request.metadata = metadata;
7856
+ if (opts.idempotencyKey !== void 0) {
7857
+ request.idempotencyKey = opts.idempotencyKey;
7858
+ }
7859
+ const data = await createPaymentProduct(environment, request);
7860
+ if (json) {
7861
+ outputJson(data);
7862
+ } else {
7863
+ outputSuccess(
7864
+ `Stripe product created: ${data.product.stripeProductId}`
7865
+ );
7866
+ }
7867
+ await trackPaymentUsage("products.create", true, { environment });
7868
+ } catch (err) {
7869
+ await trackPaymentUsage("products.create", false, {
7870
+ environment: opts.environment
7871
+ });
7872
+ handleError(err, json);
7873
+ }
7874
+ });
7875
+ productsCmd.command("update <productId>").description("Update a Stripe product").requiredOption(
7876
+ "--environment <environment>",
7877
+ "Stripe environment: test or live"
7878
+ ).option("--name <name>", "Product name").option("--description <description>", 'Product description, or "null"').option("--active <bool>", "Set active status (true/false)").option("--metadata <json>", "Metadata JSON object with string values").action(async (productId, opts, cmd) => {
7879
+ const { json } = getRootOpts(cmd);
7880
+ try {
7881
+ const environment = parseEnvironment(opts.environment);
7882
+ await requireAuth();
7883
+ const request = {};
7884
+ const description = nullableString2(opts.description);
7885
+ const active = parseBooleanOption(opts.active, "--active");
7886
+ const metadata = parseMetadataOption(opts.metadata);
7887
+ if (opts.name !== void 0) request.name = opts.name;
7888
+ if (description !== void 0) request.description = description;
7889
+ if (active !== void 0) request.active = active;
7890
+ if (metadata !== void 0) request.metadata = metadata;
7891
+ if (Object.keys(request).length === 0) {
7892
+ throw new CLIError(
7893
+ "Provide at least one option to update (--name, --description, --active, --metadata)."
7894
+ );
7895
+ }
7896
+ const data = await updatePaymentProduct(
7897
+ environment,
7898
+ productId,
7899
+ request
7900
+ );
7901
+ if (json) {
7902
+ outputJson(data);
7903
+ } else {
7904
+ outputSuccess(
7905
+ `Stripe product updated: ${data.product.stripeProductId}`
7906
+ );
7907
+ }
7908
+ await trackPaymentUsage("products.update", true, { environment });
7909
+ } catch (err) {
7910
+ await trackPaymentUsage("products.update", false, {
7911
+ environment: opts.environment
7912
+ });
7913
+ handleError(err, json);
7914
+ }
7915
+ });
7916
+ productsCmd.command("delete <productId>").description("Delete a Stripe product that has no prices").requiredOption(
7917
+ "--environment <environment>",
7918
+ "Stripe environment: test or live"
7919
+ ).action(async (productId, opts, cmd) => {
7920
+ const { json, yes } = getRootOpts(cmd);
7921
+ try {
7922
+ const environment = parseEnvironment(opts.environment);
7923
+ await requireAuth();
7924
+ if (json && !yes) {
7925
+ throw new CLIError(
7926
+ "Use --yes with --json to delete a Stripe product non-interactively."
7927
+ );
7928
+ }
7929
+ if (!yes) {
7930
+ const confirm6 = await confirm2({
7931
+ message: `Delete Stripe ${environment} product "${productId}"?`
7932
+ });
7933
+ if (isCancel2(confirm6) || !confirm6) process.exit(0);
7934
+ }
7935
+ const data = await deletePaymentProduct(environment, productId);
7936
+ if (json) {
7937
+ outputJson(data);
7938
+ } else {
7939
+ outputSuccess(`Stripe product deleted: ${data.stripeProductId}`);
7940
+ }
7941
+ await trackPaymentUsage("products.delete", true, { environment });
7942
+ } catch (err) {
7943
+ await trackPaymentUsage("products.delete", false, {
7944
+ environment: opts.environment
7945
+ });
7946
+ handleError(err, json);
7947
+ }
7948
+ });
7949
+ }
7950
+
7951
+ // src/commands/payments/status.ts
7952
+ function registerPaymentsStatusCommand(paymentsCmd2) {
7953
+ paymentsCmd2.command("status").description("Show Stripe payment connection, sync, and webhook status").action(async (_opts, cmd) => {
7954
+ const { json } = getRootOpts(cmd);
7955
+ try {
7956
+ await requireAuth();
7957
+ const data = await getPaymentsStatus();
7958
+ if (json) {
7959
+ outputJson(data);
7960
+ } else if (data.connections.length === 0) {
7961
+ console.log("No Stripe payment environments found.");
7962
+ } else {
7963
+ outputTable(
7964
+ ["Env", "Status", "Key", "Account", "Webhook", "Last Sync", "Synced At"],
7965
+ data.connections.map((connection) => [
7966
+ connection.environment,
7967
+ connection.status,
7968
+ connection.maskedKey ?? "-",
7969
+ connection.stripeAccountId ?? "-",
7970
+ connection.webhookEndpointId ? "Configured" : "-",
7971
+ connection.lastSyncStatus ?? "-",
7972
+ formatDate(connection.lastSyncedAt)
7973
+ ])
7974
+ );
7975
+ }
7976
+ await trackPaymentUsage("status", true);
7977
+ } catch (err) {
7978
+ await trackPaymentUsage("status", false);
7979
+ handleError(err, json);
7980
+ }
7981
+ });
7982
+ }
7983
+
7984
+ // src/commands/payments/subscriptions.ts
7985
+ function registerPaymentsSubscriptionsCommand(paymentsCmd2) {
7986
+ paymentsCmd2.command("subscriptions").description("List mirrored Stripe subscriptions").requiredOption(
7987
+ "--environment <environment>",
7988
+ "Stripe environment: test or live"
7989
+ ).option("--subject-type <type>", "Filter by billing subject type").option("--subject-id <id>", "Filter by billing subject id").option("--limit <limit>", "Maximum rows to return (1-100)", "50").action(async (opts, cmd) => {
7990
+ const { json } = getRootOpts(cmd);
7991
+ try {
7992
+ const environment = parseEnvironment(opts.environment);
7993
+ const limit = parseIntegerOption(opts.limit, "--limit", { min: 1, max: 100 }) ?? 50;
7994
+ await requireAuth();
7995
+ const data = await listSubscriptions(environment, {
7996
+ limit,
7997
+ ...opts.subjectType !== void 0 ? { subjectType: opts.subjectType } : {},
7998
+ ...opts.subjectId !== void 0 ? { subjectId: opts.subjectId } : {}
7999
+ });
8000
+ if (json) {
8001
+ outputJson(data);
8002
+ } else if (data.subscriptions.length === 0) {
8003
+ console.log("No Stripe subscriptions found.");
8004
+ } else {
8005
+ outputTable(
8006
+ [
8007
+ "Subscription ID",
8008
+ "Customer",
8009
+ "Subject",
8010
+ "Status",
8011
+ "Items",
8012
+ "Period End"
8013
+ ],
8014
+ data.subscriptions.map((subscription) => [
8015
+ subscription.stripeSubscriptionId,
8016
+ subscription.stripeCustomerId,
8017
+ subscription.subjectType && subscription.subjectId ? `${subscription.subjectType}:${subscription.subjectId}` : "-",
8018
+ subscription.status,
8019
+ String(subscription.items?.length ?? 0),
8020
+ formatDate(subscription.currentPeriodEnd)
8021
+ ])
8022
+ );
8023
+ }
8024
+ await trackPaymentUsage("subscriptions", true, { environment });
8025
+ } catch (err) {
8026
+ await trackPaymentUsage("subscriptions", false, {
8027
+ environment: opts.environment
8028
+ });
8029
+ handleError(err, json);
8030
+ }
8031
+ });
8032
+ }
8033
+
8034
+ // src/commands/payments/sync.ts
8035
+ function registerPaymentsSyncCommand(paymentsCmd2) {
8036
+ paymentsCmd2.command("sync").description(
8037
+ "Sync configured Stripe products, prices, customers, and subscriptions"
8038
+ ).option(
8039
+ "--environment <environment>",
8040
+ "Stripe environment: test, live, or all",
8041
+ "all"
8042
+ ).action(async (opts, cmd) => {
8043
+ const { json } = getRootOpts(cmd);
8044
+ try {
8045
+ const environment = parseEnvironmentOrAll(opts.environment);
8046
+ await requireAuth();
8047
+ const data = await syncPayments(environment);
8048
+ if (json) {
8049
+ outputJson(data);
8050
+ } else if (data.results.length === 0) {
8051
+ console.log("No configured Stripe environments to sync.");
8052
+ } else {
8053
+ outputTable(
8054
+ [
8055
+ "Env",
8056
+ "Status",
8057
+ "Products",
8058
+ "Prices",
8059
+ "Customers",
8060
+ "Subscriptions",
8061
+ "Unmapped",
8062
+ "Synced At"
8063
+ ],
8064
+ data.results.map((result) => [
8065
+ result.environment,
8066
+ result.connection.lastSyncStatus ?? result.connection.status,
8067
+ String(result.connection.lastSyncCounts.products ?? 0),
8068
+ String(result.connection.lastSyncCounts.prices ?? 0),
8069
+ String(result.connection.lastSyncCounts.customers ?? 0),
8070
+ String(result.subscriptions?.synced ?? 0),
8071
+ String(result.subscriptions?.unmapped ?? 0),
8072
+ formatDate(result.connection.lastSyncedAt)
8073
+ ])
8074
+ );
8075
+ outputSuccess("Stripe payments synced.");
8076
+ }
8077
+ await trackPaymentUsage("sync", true, { environment });
8078
+ } catch (err) {
8079
+ await trackPaymentUsage("sync", false, {
8080
+ environment: opts.environment
8081
+ });
8082
+ handleError(err, json);
8083
+ }
8084
+ });
8085
+ }
8086
+
8087
+ // src/commands/payments/webhooks.ts
8088
+ function registerPaymentsWebhooksCommand(paymentsCmd2) {
8089
+ const webhooksCmd = paymentsCmd2.command("webhooks").description("Manage InsForge-managed Stripe webhooks");
8090
+ webhooksCmd.command("configure <environment>").description("Create or recreate the managed Stripe webhook endpoint").action(async (environmentValue, _opts, cmd) => {
8091
+ const { json } = getRootOpts(cmd);
8092
+ try {
8093
+ const environment = parseEnvironment(environmentValue);
8094
+ await requireAuth();
8095
+ const data = await configurePaymentWebhook(environment);
8096
+ if (json) {
8097
+ outputJson(data);
8098
+ } else {
8099
+ outputTable(
8100
+ ["Env", "Webhook ID", "URL", "Configured At"],
8101
+ [[
8102
+ data.connection.environment,
8103
+ data.connection.webhookEndpointId ?? "-",
8104
+ data.connection.webhookEndpointUrl ?? "-",
8105
+ formatDate(data.connection.webhookConfiguredAt)
8106
+ ]]
8107
+ );
8108
+ outputSuccess(`Stripe ${environment} webhook configured.`);
8109
+ }
8110
+ await trackPaymentUsage("webhooks.configure", true, { environment });
8111
+ } catch (err) {
8112
+ await trackPaymentUsage("webhooks.configure", false, { environment: environmentValue });
8113
+ handleError(err, json);
8114
+ }
8115
+ });
8116
+ }
8117
+
8118
+ // src/commands/payments/index.ts
8119
+ function registerPaymentsCommands(paymentsCmd2) {
8120
+ paymentsCmd2.description("Manage Stripe payments");
8121
+ registerPaymentsStatusCommand(paymentsCmd2);
8122
+ registerPaymentsConfigCommand(paymentsCmd2);
8123
+ registerPaymentsSyncCommand(paymentsCmd2);
8124
+ registerPaymentsWebhooksCommand(paymentsCmd2);
8125
+ registerPaymentsCatalogCommand(paymentsCmd2);
8126
+ registerPaymentsCustomersCommand(paymentsCmd2);
8127
+ registerPaymentsProductsCommand(paymentsCmd2);
8128
+ registerPaymentsPricesCommand(paymentsCmd2);
8129
+ registerPaymentsSubscriptionsCommand(paymentsCmd2);
8130
+ registerPaymentsHistoryCommand(paymentsCmd2);
8131
+ }
8132
+
6049
8133
  // src/index.ts
6050
- var __dirname = dirname(fileURLToPath(import.meta.url));
6051
- var pkg = JSON.parse(readFileSync7(join13(__dirname, "../package.json"), "utf-8"));
8134
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
8135
+ var pkg = JSON.parse(readFileSync8(join14(__dirname, "../package.json"), "utf-8"));
6052
8136
  var INSFORGE_LOGO = `
6053
8137
  \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
6054
8138
  \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
@@ -6072,6 +8156,7 @@ var orgsCmd = program.command("orgs", { hidden: true }).description("Manage orga
6072
8156
  registerOrgsCommands(orgsCmd);
6073
8157
  var projectsCmd = program.command("projects", { hidden: true }).description("Manage projects");
6074
8158
  registerProjectsCommands(projectsCmd);
8159
+ registerBranchCommands(program);
6075
8160
  var dbCmd = program.command("db").description("Database operations");
6076
8161
  registerDbCommands(dbCmd);
6077
8162
  registerDbTablesCommand(dbCmd);
@@ -6117,6 +8202,8 @@ registerLogsCommand(program);
6117
8202
  registerMetadataCommand(program);
6118
8203
  var diagnoseCmd = program.command("diagnose");
6119
8204
  registerDiagnoseCommands(diagnoseCmd);
8205
+ var paymentsCmd = program.command("payments").description("Manage Stripe payments");
8206
+ registerPaymentsCommands(paymentsCmd);
6120
8207
  var computeCmd = program.command("compute").description("Manage compute services (Docker containers on Fly.io)");
6121
8208
  registerComputeListCommand(computeCmd);
6122
8209
  registerComputeGetCommand(computeCmd);
@@ -6125,7 +8212,7 @@ registerComputeUpdateCommand(computeCmd);
6125
8212
  registerComputeDeleteCommand(computeCmd);
6126
8213
  registerComputeStartCommand(computeCmd);
6127
8214
  registerComputeStopCommand(computeCmd);
6128
- registerComputeLogsCommand(computeCmd);
8215
+ registerComputeEventsCommand(computeCmd);
6129
8216
  var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
6130
8217
  registerSchedulesListCommand(schedulesCmd);
6131
8218
  registerSchedulesGetCommand(schedulesCmd);
@@ -6150,7 +8237,7 @@ async function showInteractiveMenu() {
6150
8237
  } catch {
6151
8238
  }
6152
8239
  console.log(INSFORGE_LOGO);
6153
- clack11.intro(`InsForge CLI v${pkg.version}`);
8240
+ clack15.intro(`InsForge CLI v${pkg.version}`);
6154
8241
  const options = [];
6155
8242
  if (!isLoggedIn) {
6156
8243
  options.push({ value: "login", label: "Log in to InsForge" });
@@ -6171,7 +8258,7 @@ async function showInteractiveMenu() {
6171
8258
  options
6172
8259
  });
6173
8260
  if (isCancel2(action)) {
6174
- clack11.cancel("Bye!");
8261
+ clack15.cancel("Bye!");
6175
8262
  process.exit(0);
6176
8263
  }
6177
8264
  switch (action) {