@insforge/cli 0.1.62 → 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,20 +1160,517 @@ function registerProjectsCommands(projectsCmd2) {
1098
1160
  });
1099
1161
  }
1100
1162
 
1163
+ // src/lib/analytics.ts
1164
+ import { PostHog } from "posthog-node";
1165
+ var POSTHOG_API_KEY = "phc_ueV1ii62wdBTkH7E70ugyeqHIHu8dFDdjs0qq3TZhJz";
1166
+ var POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
1167
+ var client = null;
1168
+ function getClient() {
1169
+ if (!POSTHOG_API_KEY) return null;
1170
+ if (!client) {
1171
+ client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST });
1172
+ }
1173
+ return client;
1174
+ }
1175
+ function captureEvent(distinctId, event, properties) {
1176
+ try {
1177
+ getClient()?.capture({ distinctId, event, properties });
1178
+ } catch {
1179
+ }
1180
+ }
1181
+ function trackCommand(command, distinctId, properties) {
1182
+ captureEvent(distinctId, "cli_command_invoked", {
1183
+ command,
1184
+ ...properties
1185
+ });
1186
+ }
1187
+ function trackDiagnose(subcommand, config) {
1188
+ captureEvent(config.project_id, "cli_diagnose_invoked", {
1189
+ subcommand,
1190
+ project_id: config.project_id,
1191
+ project_name: config.project_name,
1192
+ org_id: config.org_id,
1193
+ region: config.region,
1194
+ oss_mode: config.project_id === FAKE_PROJECT_ID
1195
+ });
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
+ }
1208
+ async function shutdownAnalytics() {
1209
+ try {
1210
+ if (client) await client.shutdown();
1211
+ } catch {
1212
+ }
1213
+ }
1214
+
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;
1245
+ }
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
1278
+ });
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
+ }
1287
+ }
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
+ });
1299
+ }
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();
1352
+ }
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})`);
1363
+ }
1364
+ if (showProgress && branch2.branch_state !== lastState) {
1365
+ outputInfo(` state: ${branch2.branch_state}\u2026`);
1366
+ lastState = branch2.branch_state;
1367
+ }
1368
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
1369
+ }
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;
1375
+ }
1376
+
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.");
1386
+ }
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();
1411
+ }
1412
+ });
1413
+ }
1414
+
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
1522
+ import * as clack6 from "@clack/prompts";
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
+
1101
1660
  // src/commands/projects/link.ts
1102
1661
  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";
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";
1107
1666
  import pc2 from "picocolors";
1108
1667
 
1109
1668
  // src/lib/skills.ts
1110
1669
  import { exec } from "child_process";
1111
- import { existsSync as existsSync2, readFileSync as readFileSync2, appendFileSync } from "fs";
1670
+ import { existsSync as existsSync3, readFileSync as readFileSync2, appendFileSync } from "fs";
1112
1671
  import { join as join2 } from "path";
1113
1672
  import { promisify } from "util";
1114
- import * as clack5 from "@clack/prompts";
1673
+ import * as clack8 from "@clack/prompts";
1115
1674
  var execAsync = promisify(exec);
1116
1675
  var SKILL_INSTALL_TIMEOUT_MS = 6e4;
1117
1676
  function describeExecError(err) {
@@ -1152,7 +1711,7 @@ var GITIGNORE_ENTRIES = [
1152
1711
  ];
1153
1712
  function updateGitignore() {
1154
1713
  const gitignorePath = join2(process.cwd(), ".gitignore");
1155
- const existing = existsSync2(gitignorePath) ? readFileSync2(gitignorePath, "utf-8") : "";
1714
+ const existing = existsSync3(gitignorePath) ? readFileSync2(gitignorePath, "utf-8") : "";
1156
1715
  const lines = new Set(existing.split("\n").map((l) => l.trim()));
1157
1716
  const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.has(entry));
1158
1717
  if (!missing.length) return;
@@ -1164,29 +1723,29 @@ ${missing.join("\n")}
1164
1723
  }
1165
1724
  async function installSkills(json) {
1166
1725
  try {
1167
- if (!json) clack5.log.info("Installing InsForge agent skills (global)...");
1726
+ if (!json) clack8.log.info("Installing InsForge agent skills (global)...");
1168
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", {
1169
1728
  cwd: process.cwd(),
1170
1729
  timeout: SKILL_INSTALL_TIMEOUT_MS
1171
1730
  });
1172
- if (!json) clack5.log.success("InsForge agent skills installed.");
1731
+ if (!json) clack8.log.success("InsForge agent skills installed.");
1173
1732
  } catch (err) {
1174
1733
  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.");
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.");
1177
1736
  }
1178
1737
  }
1179
1738
  try {
1180
- if (!json) clack5.log.info("Installing find-skills (global)...");
1739
+ if (!json) clack8.log.info("Installing find-skills (global)...");
1181
1740
  await execAsync("npx skills add https://github.com/vercel-labs/skills --skill find-skills -g -y", {
1182
1741
  cwd: process.cwd(),
1183
1742
  timeout: SKILL_INSTALL_TIMEOUT_MS
1184
1743
  });
1185
- if (!json) clack5.log.success("find-skills installed.");
1744
+ if (!json) clack8.log.success("find-skills installed.");
1186
1745
  } catch (err) {
1187
1746
  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.");
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.");
1190
1749
  }
1191
1750
  }
1192
1751
  try {
@@ -1235,65 +1794,14 @@ async function reportCliUsage(toolName, success, maxRetries = 1, explicitConfig)
1235
1794
  }
1236
1795
  }
1237
1796
 
1238
- // src/lib/analytics.ts
1239
- import { PostHog } from "posthog-node";
1240
- var POSTHOG_API_KEY = "";
1241
- var POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
1242
- var client = null;
1243
- function getClient() {
1244
- if (!POSTHOG_API_KEY) return null;
1245
- if (!client) {
1246
- client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST });
1247
- }
1248
- return client;
1249
- }
1250
- function captureEvent(distinctId, event, properties) {
1251
- try {
1252
- getClient()?.capture({ distinctId, event, properties });
1253
- } catch {
1254
- }
1255
- }
1256
- function trackCommand(command, distinctId, properties) {
1257
- captureEvent(distinctId, "cli_command_invoked", {
1258
- command,
1259
- ...properties
1260
- });
1261
- }
1262
- function trackDiagnose(subcommand, config) {
1263
- captureEvent(config.project_id, "cli_diagnose_invoked", {
1264
- subcommand,
1265
- project_id: config.project_id,
1266
- project_name: config.project_name,
1267
- org_id: config.org_id,
1268
- region: config.region,
1269
- oss_mode: config.project_id === FAKE_PROJECT_ID
1270
- });
1271
- }
1272
- function trackPayments(subcommand, config, properties) {
1273
- captureEvent(config.project_id, "cli_payments_invoked", {
1274
- subcommand,
1275
- project_id: config.project_id,
1276
- project_name: config.project_name,
1277
- org_id: config.org_id,
1278
- region: config.region,
1279
- oss_mode: config.project_id === FAKE_PROJECT_ID,
1280
- ...properties
1281
- });
1282
- }
1283
- async function shutdownAnalytics() {
1284
- try {
1285
- if (client) await client.shutdown();
1286
- } catch {
1287
- }
1288
- }
1289
-
1290
- // src/commands/create.ts
1291
- import { exec as exec2 } from "child_process";
1797
+ // src/auth-providers/apply.ts
1798
+ import { promises as fs } from "fs";
1799
+ import * as path from "path";
1292
1800
  import { tmpdir } from "os";
1801
+ import { execFile } from "child_process";
1293
1802
  import { promisify as promisify2 } from "util";
1294
- import * as fs3 from "fs/promises";
1295
- import * as path3 from "path";
1296
- import * as clack7 from "@clack/prompts";
1803
+ import { randomBytes as randomBytes2 } from "crypto";
1804
+ import * as clack9 from "@clack/prompts";
1297
1805
 
1298
1806
  // src/lib/api/oss.ts
1299
1807
  function requireProjectConfig() {
@@ -1318,14 +1826,23 @@ async function getAnonKey() {
1318
1826
  const data = await res.json();
1319
1827
  return data.accessToken;
1320
1828
  }
1321
- async function ossFetch(path5, options = {}) {
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 = {}) {
1322
1839
  const config = requireProjectConfig();
1323
1840
  const headers = {
1324
1841
  "Content-Type": "application/json",
1325
1842
  Authorization: `Bearer ${config.api_key}`,
1326
1843
  ...options.headers ?? {}
1327
1844
  };
1328
- const res = await fetch(`${config.oss_host}${path5}`, { ...options, headers });
1845
+ const res = await fetch(`${config.oss_host}${path6}`, { ...options, headers });
1329
1846
  if (!res.ok) {
1330
1847
  const err = await res.json().catch(() => ({}));
1331
1848
  let message = err.message ?? err.error ?? `OSS request failed: ${res.status}`;
@@ -1334,13 +1851,13 @@ async function ossFetch(path5, options = {}) {
1334
1851
  ${err.nextActions}`;
1335
1852
  }
1336
1853
  const isRouteLevel404 = !err.error || err.error === "NOT_FOUND";
1337
- if (res.status === 404 && isRouteLevel404 && path5.startsWith("/api/compute")) {
1854
+ if (res.status === 404 && isRouteLevel404 && path6.startsWith("/api/compute")) {
1338
1855
  message = "Compute services are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin to enable compute.";
1339
1856
  }
1340
- if (res.status === 404 && isRouteLevel404 && path5.startsWith("/api/payments")) {
1857
+ if (res.status === 404 && isRouteLevel404 && path6.startsWith("/api/payments")) {
1341
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.";
1342
1859
  }
1343
- if (res.status === 404 && isRouteLevel404 && path5 === "/api/database/migrations") {
1860
+ if (res.status === 404 && isRouteLevel404 && path6 === "/api/database/migrations") {
1344
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.";
1345
1862
  }
1346
1863
  throw new CLIError(message);
@@ -1348,16 +1865,231 @@ ${err.nextActions}`;
1348
1865
  return res;
1349
1866
  }
1350
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
+
1351
2083
  // src/lib/env.ts
1352
- import * as fs from "fs/promises";
1353
- import * as path from "path";
2084
+ import * as fs2 from "fs/promises";
2085
+ import * as path2 from "path";
1354
2086
  async function readEnvFile(cwd) {
1355
2087
  const candidates = [".env.local", ".env.production", ".env"];
1356
2088
  for (const name of candidates) {
1357
- const filePath = path.join(cwd, name);
1358
- const exists = await fs.stat(filePath).catch(() => null);
2089
+ const filePath = path2.join(cwd, name);
2090
+ const exists = await fs2.stat(filePath).catch(() => null);
1359
2091
  if (!exists) continue;
1360
- const content = await fs.readFile(filePath, "utf-8");
2092
+ const content = await fs2.readFile(filePath, "utf-8");
1361
2093
  const vars = [];
1362
2094
  for (const line of content.split("\n")) {
1363
2095
  const trimmed = line.trim();
@@ -1377,14 +2109,14 @@ async function readEnvFile(cwd) {
1377
2109
  }
1378
2110
 
1379
2111
  // src/commands/deployments/deploy.ts
1380
- import * as path2 from "path";
1381
- import * as fs2 from "fs/promises";
2112
+ import * as path3 from "path";
2113
+ import * as fs3 from "fs/promises";
1382
2114
  import { createReadStream } from "fs";
1383
2115
  import { createHash as createHash2 } from "crypto";
1384
- import * as clack6 from "@clack/prompts";
2116
+ import * as clack10 from "@clack/prompts";
1385
2117
  import archiver from "archiver";
1386
- var POLL_INTERVAL_MS = 5e3;
1387
- var POLL_TIMEOUT_MS = 3e5;
2118
+ var POLL_INTERVAL_MS3 = 5e3;
2119
+ var POLL_TIMEOUT_MS3 = 3e5;
1388
2120
  var DIRECT_UPLOAD_CONCURRENCY = 8;
1389
2121
  var EXCLUDE_PATTERNS = [
1390
2122
  "node_modules",
@@ -1437,7 +2169,7 @@ function isInsforgeCloudOssHost(ossHost) {
1437
2169
  }
1438
2170
  }
1439
2171
  function normalizeRelativePath(sourceDir, absolutePath) {
1440
- return path2.relative(sourceDir, absolutePath).split(path2.sep).join("/").replace(/\\/g, "/");
2172
+ return path3.relative(sourceDir, absolutePath).split(path3.sep).join("/").replace(/\\/g, "/");
1441
2173
  }
1442
2174
  async function hashFile(filePath) {
1443
2175
  const hash = createHash2("sha1");
@@ -1452,10 +2184,10 @@ async function hashFile(filePath) {
1452
2184
  async function collectDeploymentFiles(sourceDir) {
1453
2185
  const files = [];
1454
2186
  async function walk(currentDir) {
1455
- const entries = await fs2.readdir(currentDir, { withFileTypes: true });
2187
+ const entries = await fs3.readdir(currentDir, { withFileTypes: true });
1456
2188
  entries.sort((a, b) => a.name.localeCompare(b.name));
1457
2189
  for (const entry of entries) {
1458
- const absolutePath = path2.join(currentDir, entry.name);
2190
+ const absolutePath = path3.join(currentDir, entry.name);
1459
2191
  const normalizedPath = normalizeRelativePath(sourceDir, absolutePath);
1460
2192
  if (!normalizedPath || shouldExclude(normalizedPath)) {
1461
2193
  continue;
@@ -1561,12 +2293,12 @@ async function startDirectDeployment(deploymentId, startBody) {
1561
2293
  });
1562
2294
  await response.json();
1563
2295
  }
1564
- async function pollDeployment(deploymentId, spinner7, syncBeforeRead) {
1565
- spinner7?.message("Building and deploying...");
2296
+ async function pollDeployment(deploymentId, spinner8, syncBeforeRead) {
2297
+ spinner8?.message("Building and deploying...");
1566
2298
  const startTime = Date.now();
1567
2299
  let deployment = null;
1568
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1569
- 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));
1570
2302
  try {
1571
2303
  if (syncBeforeRead) {
1572
2304
  await ossFetch(`/api/deployments/${deploymentId}/sync`, { method: "POST" });
@@ -1578,13 +2310,13 @@ async function pollDeployment(deploymentId, spinner7, syncBeforeRead) {
1578
2310
  break;
1579
2311
  }
1580
2312
  if (status === "ERROR" || status === "CANCELED") {
1581
- spinner7?.stop("Deployment failed");
2313
+ spinner8?.stop("Deployment failed");
1582
2314
  throw new CLIError(
1583
2315
  getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`
1584
2316
  );
1585
2317
  }
1586
2318
  const elapsed = Math.round((Date.now() - startTime) / 1e3);
1587
- spinner7?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
2319
+ spinner8?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
1588
2320
  } catch (err) {
1589
2321
  if (err instanceof CLIError) throw err;
1590
2322
  }
@@ -1594,20 +2326,20 @@ async function pollDeployment(deploymentId, spinner7, syncBeforeRead) {
1594
2326
  return { deploymentId, deployment, isReady, liveUrl };
1595
2327
  }
1596
2328
  async function deployProjectDirect(opts, config) {
1597
- const { sourceDir, startBody = {}, spinner: spinner7 } = opts;
1598
- spinner7?.start("Scanning source files...");
2329
+ const { sourceDir, startBody = {}, spinner: spinner8 } = opts;
2330
+ spinner8?.start("Scanning source files...");
1599
2331
  const localFiles = await collectDeploymentFiles(sourceDir);
1600
2332
  if (localFiles.length === 0) {
1601
2333
  throw new CLIError("No deployable files found in the source directory.");
1602
2334
  }
1603
- spinner7?.message("Creating deployment...");
2335
+ spinner8?.message("Creating deployment...");
1604
2336
  const createResult = await createDirectDeploymentSession(
1605
2337
  config,
1606
2338
  localFiles.map(({ path: relativePath, sha, size }) => ({ path: relativePath, sha, size }))
1607
2339
  );
1608
2340
  const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
1609
2341
  const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
1610
- spinner7?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
2342
+ spinner8?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
1611
2343
  await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
1612
2344
  const localFile = localFileByPath.get(manifestFile.path);
1613
2345
  if (!localFile) {
@@ -1618,18 +2350,18 @@ async function deployProjectDirect(opts, config) {
1618
2350
  }
1619
2351
  await uploadDirectDeploymentFile(createResult.id, manifestFile, localFile);
1620
2352
  });
1621
- spinner7?.message("Starting deployment...");
2353
+ spinner8?.message("Starting deployment...");
1622
2354
  await startDirectDeployment(createResult.id, startBody);
1623
- return await pollDeployment(createResult.id, spinner7, !isInsforgeCloudOssHost(config.oss_host));
2355
+ return await pollDeployment(createResult.id, spinner8, !isInsforgeCloudOssHost(config.oss_host));
1624
2356
  }
1625
2357
  async function deployProjectLegacy(opts) {
1626
- const { sourceDir, startBody = {}, spinner: spinner7 } = opts;
1627
- spinner7?.message("Creating deployment...");
2358
+ const { sourceDir, startBody = {}, spinner: spinner8 } = opts;
2359
+ spinner8?.message("Creating deployment...");
1628
2360
  const createRes = await ossFetch("/api/deployments", { method: "POST" });
1629
2361
  const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
1630
- spinner7?.message("Compressing source files...");
2362
+ spinner8?.message("Compressing source files...");
1631
2363
  const zipBuffer = await createZipBuffer(sourceDir);
1632
- spinner7?.message("Uploading...");
2364
+ spinner8?.message("Uploading...");
1633
2365
  const formData = new FormData();
1634
2366
  for (const [key, value] of Object.entries(uploadFields)) {
1635
2367
  formData.append(key, value);
@@ -1640,13 +2372,13 @@ async function deployProjectLegacy(opts) {
1640
2372
  const uploadErr = await uploadRes.text();
1641
2373
  throw new CLIError(`Failed to upload: ${uploadErr}`);
1642
2374
  }
1643
- spinner7?.message("Starting deployment...");
2375
+ spinner8?.message("Starting deployment...");
1644
2376
  const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
1645
2377
  method: "POST",
1646
2378
  body: JSON.stringify(startBody)
1647
2379
  });
1648
2380
  await startRes.json();
1649
- return await pollDeployment(deploymentId, spinner7, false);
2381
+ return await pollDeployment(deploymentId, spinner8, false);
1650
2382
  }
1651
2383
  async function deployProject(opts) {
1652
2384
  const config = getProjectConfig();
@@ -1670,18 +2402,18 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
1670
2402
  await requireAuth();
1671
2403
  const config = getProjectConfig();
1672
2404
  if (!config) throw new ProjectNotLinkedError();
1673
- const sourceDir = path2.resolve(directory ?? ".");
1674
- const stats = await fs2.stat(sourceDir).catch(() => null);
2405
+ const sourceDir = path3.resolve(directory ?? ".");
2406
+ const stats = await fs3.stat(sourceDir).catch(() => null);
1675
2407
  if (!stats?.isDirectory()) {
1676
2408
  throw new CLIError(`"${sourceDir}" is not a valid directory.`);
1677
2409
  }
1678
- const dirName = path2.basename(sourceDir);
2410
+ const dirName = path3.basename(sourceDir);
1679
2411
  if (EXCLUDE_PATTERNS.includes(dirName)) {
1680
2412
  throw new CLIError(
1681
2413
  `"${dirName}" is an excluded directory and cannot be used as a deploy source. Please specify your project root or output directory instead.`
1682
2414
  );
1683
2415
  }
1684
- const spinner7 = !json ? clack6.spinner() : null;
2416
+ const spinner8 = !json ? clack10.spinner() : null;
1685
2417
  const startBody = {};
1686
2418
  if (opts.env) {
1687
2419
  try {
@@ -1707,19 +2439,19 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
1707
2439
  throw new CLIError("Invalid --meta JSON.");
1708
2440
  }
1709
2441
  }
1710
- const result = await deployProject({ sourceDir, startBody, spinner: spinner7 });
2442
+ const result = await deployProject({ sourceDir, startBody, spinner: spinner8 });
1711
2443
  if (result.isReady) {
1712
- spinner7?.stop("Deployment complete");
2444
+ spinner8?.stop("Deployment complete");
1713
2445
  if (json) {
1714
2446
  outputJson(result.deployment);
1715
2447
  } else {
1716
2448
  if (result.liveUrl) {
1717
- clack6.log.success(`Live at: ${result.liveUrl}`);
2449
+ clack10.log.success(`Live at: ${result.liveUrl}`);
1718
2450
  }
1719
- clack6.log.info(`Deployment ID: ${result.deploymentId}`);
2451
+ clack10.log.info(`Deployment ID: ${result.deploymentId}`);
1720
2452
  }
1721
2453
  } else {
1722
- spinner7?.stop("Deployment is still building");
2454
+ spinner8?.stop("Deployment is still building");
1723
2455
  if (json) {
1724
2456
  outputJson({
1725
2457
  id: result.deploymentId,
@@ -1727,9 +2459,9 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
1727
2459
  timedOut: true
1728
2460
  });
1729
2461
  } else {
1730
- clack6.log.info(`Deployment ID: ${result.deploymentId}`);
1731
- clack6.log.warn("Deployment did not finish within 5 minutes.");
1732
- 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}`);
1733
2465
  }
1734
2466
  }
1735
2467
  await reportCliUsage("cli.deployments.deploy", true);
@@ -1741,7 +2473,10 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
1741
2473
  }
1742
2474
 
1743
2475
  // src/commands/create.ts
1744
- 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._/-]+$/;
1745
2480
  function buildOssHost(appkey, region) {
1746
2481
  return `https://${appkey}.${region}.insforge.app`;
1747
2482
  }
@@ -1835,31 +2570,31 @@ async function animateBanner() {
1835
2570
  process.stderr.write("\n");
1836
2571
  }
1837
2572
  function getDefaultProjectName() {
1838
- const dirName = path3.basename(process.cwd());
2573
+ const dirName = path4.basename(process.cwd());
1839
2574
  const sanitized = dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1840
2575
  return sanitized.length >= 2 ? sanitized : "";
1841
2576
  }
1842
2577
  async function copyDir(src, dest) {
1843
- const entries = await fs3.readdir(src, { withFileTypes: true });
2578
+ const entries = await fs4.readdir(src, { withFileTypes: true });
1844
2579
  for (const entry of entries) {
1845
- const srcPath = path3.join(src, entry.name);
1846
- const destPath = path3.join(dest, entry.name);
2580
+ const srcPath = path4.join(src, entry.name);
2581
+ const destPath = path4.join(dest, entry.name);
1847
2582
  if (entry.isDirectory()) {
1848
- await fs3.mkdir(destPath, { recursive: true });
2583
+ await fs4.mkdir(destPath, { recursive: true });
1849
2584
  await copyDir(srcPath, destPath);
1850
2585
  } else {
1851
- await fs3.copyFile(srcPath, destPath);
2586
+ await fs4.copyFile(srcPath, destPath);
1852
2587
  }
1853
2588
  }
1854
2589
  }
1855
2590
  function registerCreateCommand(program2) {
1856
- 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) => {
1857
2592
  const { json, apiUrl } = getRootOpts(cmd);
1858
2593
  try {
1859
2594
  await requireAuth(apiUrl, false);
1860
2595
  if (!json) {
1861
2596
  await animateBanner();
1862
- clack7.intro("Let's build something great");
2597
+ clack11.intro("Let's build something great");
1863
2598
  }
1864
2599
  let orgId = opts.orgId;
1865
2600
  if (!orgId) {
@@ -1869,7 +2604,7 @@ function registerCreateCommand(program2) {
1869
2604
  }
1870
2605
  if (orgs.length === 1) {
1871
2606
  orgId = orgs[0].id;
1872
- if (!json) clack7.log.info(`Using organization: ${orgs[0].name}`);
2607
+ if (!json) clack11.log.info(`Using organization: ${orgs[0].name}`);
1873
2608
  } else {
1874
2609
  if (json) {
1875
2610
  throw new CLIError("Multiple organizations found. Specify --org-id.");
@@ -1900,12 +2635,15 @@ function registerCreateCommand(program2) {
1900
2635
  if (isCancel2(name)) process.exit(0);
1901
2636
  projectName = name;
1902
2637
  }
1903
- 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, ".");
1904
2639
  if (projectName.length < 2 || projectName === "." || projectName === "..") {
1905
2640
  throw new CLIError("Project name must be at least 2 safe characters (letters, numbers, hyphens).");
1906
2641
  }
1907
2642
  const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo", "empty"];
1908
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
+ }
1909
2647
  if (template && !validTemplates.includes(template)) {
1910
2648
  throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
1911
2649
  }
@@ -1959,27 +2697,27 @@ function registerCreateCommand(program2) {
1959
2697
  initialValue: projectName,
1960
2698
  validate: (v) => {
1961
2699
  if (v.length < 1) return "Directory name is required";
1962
- const normalized = path3.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
2700
+ const normalized = path4.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
1963
2701
  if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
1964
2702
  return void 0;
1965
2703
  }
1966
2704
  });
1967
2705
  if (isCancel2(inputDir)) process.exit(0);
1968
- dirName = path3.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
2706
+ dirName = path4.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
1969
2707
  }
1970
2708
  if (!dirName || dirName === "." || dirName === "..") {
1971
2709
  throw new CLIError("Invalid directory name.");
1972
2710
  }
1973
- projectDir = path3.resolve(originalCwd, dirName);
1974
- const dirExists = await fs3.stat(projectDir).catch(() => null);
2711
+ projectDir = path4.resolve(originalCwd, dirName);
2712
+ const dirExists = await fs4.stat(projectDir).catch(() => null);
1975
2713
  if (dirExists) {
1976
2714
  throw new CLIError(`Directory "${dirName}" already exists.`);
1977
2715
  }
1978
- await fs3.mkdir(projectDir);
2716
+ await fs4.mkdir(projectDir);
1979
2717
  process.chdir(projectDir);
1980
2718
  }
1981
2719
  let projectLinked = false;
1982
- const s = !json ? clack7.spinner() : null;
2720
+ const s = !json ? clack11.spinner() : null;
1983
2721
  try {
1984
2722
  s?.start("Creating project...");
1985
2723
  const project = await createProject(orgId, projectName, opts.region, apiUrl);
@@ -2007,37 +2745,49 @@ function registerCreateCommand(program2) {
2007
2745
  try {
2008
2746
  const anonKey = await getAnonKey();
2009
2747
  if (!anonKey) {
2010
- 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.");
2011
2749
  } else {
2012
- const envPath = path3.join(process.cwd(), ".env.local");
2750
+ const envPath = path4.join(process.cwd(), ".env.local");
2013
2751
  const envContent = [
2014
2752
  "# InsForge",
2015
2753
  `NEXT_PUBLIC_INSFORGE_URL=${projectConfig.oss_host}`,
2016
2754
  `NEXT_PUBLIC_INSFORGE_ANON_KEY=${anonKey}`,
2017
2755
  ""
2018
2756
  ].join("\n");
2019
- await fs3.writeFile(envPath, envContent, { flag: "wx" });
2757
+ await fs4.writeFile(envPath, envContent, { flag: "wx" });
2020
2758
  if (!json) {
2021
- clack7.log.success("Created .env.local with your InsForge credentials");
2759
+ clack11.log.success("Created .env.local with your InsForge credentials");
2022
2760
  }
2023
2761
  }
2024
2762
  } catch (err) {
2025
2763
  const error = err;
2026
2764
  if (!json) {
2027
2765
  if (error.code === "EEXIST") {
2028
- clack7.log.warn(".env.local already exists; skipping InsForge key seeding.");
2766
+ clack11.log.warn(".env.local already exists; skipping InsForge key seeding.");
2029
2767
  } else {
2030
- clack7.log.warn(`Failed to create .env.local: ${error.message}`);
2768
+ clack11.log.warn(`Failed to create .env.local: ${error.message}`);
2031
2769
  }
2032
2770
  }
2033
2771
  }
2034
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
+ }
2035
2785
  await installSkills(json);
2036
2786
  trackCommand("create", orgId);
2037
2787
  await reportCliUsage("cli.create", true, 6);
2038
- 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;
2039
2789
  if (templateDownloaded) {
2040
- const installSpinner = !json ? clack7.spinner() : null;
2790
+ const installSpinner = !json ? clack11.spinner() : null;
2041
2791
  installSpinner?.start("Installing dependencies...");
2042
2792
  try {
2043
2793
  await execAsync2("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
@@ -2045,8 +2795,8 @@ function registerCreateCommand(program2) {
2045
2795
  } catch (err) {
2046
2796
  installSpinner?.stop("Failed to install dependencies");
2047
2797
  if (!json) {
2048
- clack7.log.warn(`npm install failed: ${err.message}`);
2049
- 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.");
2050
2800
  }
2051
2801
  }
2052
2802
  }
@@ -2062,7 +2812,7 @@ function registerCreateCommand(program2) {
2062
2812
  if (envVars.length > 0) {
2063
2813
  startBody.envVars = envVars;
2064
2814
  }
2065
- const deploySpinner = clack7.spinner();
2815
+ const deploySpinner = clack11.spinner();
2066
2816
  const result = await deployProject({
2067
2817
  sourceDir: process.cwd(),
2068
2818
  startBody,
@@ -2073,12 +2823,12 @@ function registerCreateCommand(program2) {
2073
2823
  liveUrl = result.liveUrl;
2074
2824
  } else {
2075
2825
  deploySpinner.stop("Deployment is still building");
2076
- clack7.log.info(`Deployment ID: ${result.deploymentId}`);
2077
- clack7.log.warn("Deployment did not finish within 2 minutes.");
2078
- 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}`);
2079
2829
  }
2080
2830
  } catch (err) {
2081
- clack7.log.warn(`Deploy failed: ${err.message}`);
2831
+ clack11.log.warn(`Deploy failed: ${err.message}`);
2082
2832
  }
2083
2833
  }
2084
2834
  }
@@ -2095,38 +2845,38 @@ function registerCreateCommand(program2) {
2095
2845
  }
2096
2846
  });
2097
2847
  } else {
2098
- clack7.log.step(`Dashboard: ${dashboardUrl}`);
2848
+ clack11.log.step(`Dashboard: ${dashboardUrl}`);
2099
2849
  if (liveUrl) {
2100
- clack7.log.success(`Live site: ${liveUrl}`);
2850
+ clack11.log.success(`Live site: ${liveUrl}`);
2101
2851
  }
2102
2852
  if (templateDownloaded) {
2103
2853
  const steps = [
2104
2854
  `cd ${dirName}`,
2105
2855
  "npm run dev"
2106
2856
  ];
2107
- clack7.note(steps.join("\n"), "Next steps");
2108
- 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");
2109
2859
  } else if (hasTemplate && !templateDownloaded) {
2110
- 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.");
2111
2861
  } else {
2112
2862
  const prompts = [
2113
2863
  "Build a todo app with Google OAuth sign-in",
2114
2864
  "Build an Instagram clone where users can upload photos, like, and comment",
2115
2865
  "Build an AI chatbot with conversation history"
2116
2866
  ];
2117
- clack7.note(
2867
+ clack11.note(
2118
2868
  `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
2119
2869
 
2120
2870
  ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
2121
2871
  "Start building"
2122
2872
  );
2123
2873
  }
2124
- clack7.outro("Done!");
2874
+ clack11.outro("Done!");
2125
2875
  }
2126
2876
  } catch (err) {
2127
2877
  if (!projectLinked && hasTemplate && projectDir !== originalCwd) {
2128
2878
  process.chdir(originalCwd);
2129
- await fs3.rm(projectDir, { recursive: true, force: true }).catch(() => {
2879
+ await fs4.rm(projectDir, { recursive: true, force: true }).catch(() => {
2130
2880
  });
2131
2881
  }
2132
2882
  throw err;
@@ -2140,18 +2890,18 @@ ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
2140
2890
  });
2141
2891
  }
2142
2892
  async function downloadTemplate(framework, projectConfig, projectName, json, _apiUrl) {
2143
- const s = !json ? clack7.spinner() : null;
2893
+ const s = !json ? clack11.spinner() : null;
2144
2894
  s?.start("Downloading template...");
2145
2895
  try {
2146
2896
  const anonKey = await getAnonKey();
2147
2897
  if (!anonKey) {
2148
2898
  throw new Error("Failed to retrieve anon key from backend");
2149
2899
  }
2150
- const tempDir = tmpdir();
2900
+ const tempDir = tmpdir2();
2151
2901
  const targetDir = projectName;
2152
- const templatePath = path3.join(tempDir, targetDir);
2902
+ const templatePath = path4.join(tempDir, targetDir);
2153
2903
  try {
2154
- await fs3.rm(templatePath, { recursive: true, force: true });
2904
+ await fs4.rm(templatePath, { recursive: true, force: true });
2155
2905
  } catch {
2156
2906
  }
2157
2907
  const frame = framework === "nextjs" ? "nextjs" : "react";
@@ -2165,41 +2915,53 @@ async function downloadTemplate(framework, projectConfig, projectName, json, _ap
2165
2915
  s?.message("Copying template files...");
2166
2916
  const cwd = process.cwd();
2167
2917
  await copyDir(templatePath, cwd);
2168
- await fs3.rm(templatePath, { recursive: true, force: true }).catch(() => {
2918
+ await fs4.rm(templatePath, { recursive: true, force: true }).catch(() => {
2169
2919
  });
2170
2920
  s?.stop("Template files downloaded");
2171
2921
  } catch (err) {
2172
2922
  s?.stop("Template download failed");
2173
2923
  if (!json) {
2174
- clack7.log.warn(`Failed to download template: ${err.message}`);
2175
- 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.");
2176
2926
  }
2177
2927
  }
2178
2928
  }
2179
2929
  async function downloadGitHubTemplate(templateName, projectConfig, json) {
2180
- const s = !json ? clack7.spinner() : null;
2930
+ const s = !json ? clack11.spinner() : null;
2181
2931
  s?.start(`Downloading ${templateName} template...`);
2182
- const tempDir = path3.join(tmpdir(), `insforge-template-${Date.now()}`);
2932
+ const tempDir = path4.join(tmpdir2(), `insforge-template-${Date.now()}`);
2183
2933
  try {
2184
- await fs3.mkdir(tempDir, { recursive: true });
2185
- await execAsync2(
2186
- "git clone --depth 1 https://github.com/InsForge/insforge-templates.git .",
2187
- { cwd: tempDir, maxBuffer: 10 * 1024 * 1024, timeout: 6e4 }
2188
- );
2189
- const templateDir = path3.join(tempDir, templateName);
2190
- 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);
2191
2953
  if (!stat5?.isDirectory()) {
2192
2954
  throw new Error(`Template "${templateName}" not found in repository`);
2193
2955
  }
2194
2956
  s?.message("Copying template files...");
2195
2957
  const cwd = process.cwd();
2196
2958
  await copyDir(templateDir, cwd);
2197
- const envExamplePath = path3.join(cwd, ".env.example");
2198
- 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);
2199
2961
  if (envExampleExists) {
2200
2962
  const anonKey = await getAnonKey();
2201
- const envExample = await fs3.readFile(envExamplePath, "utf-8");
2202
- const envContent = envExample.replace(
2963
+ const envExample = await fs4.readFile(envExamplePath, "utf-8");
2964
+ const envFinal = envExample.replace(
2203
2965
  /^([A-Z][A-Z0-9_]*=)(.*)$/gm,
2204
2966
  (_, prefix, _value) => {
2205
2967
  const key = prefix.slice(0, -1);
@@ -2209,32 +2971,32 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
2209
2971
  return `${prefix}${_value}`;
2210
2972
  }
2211
2973
  );
2212
- const envLocalPath = path3.join(cwd, ".env.local");
2974
+ const envLocalPath = path4.join(cwd, ".env.local");
2213
2975
  try {
2214
- await fs3.writeFile(envLocalPath, envContent, { flag: "wx" });
2976
+ await fs4.writeFile(envLocalPath, envFinal, { flag: "wx" });
2215
2977
  } catch (e) {
2216
2978
  if (e.code === "EEXIST") {
2217
- 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.");
2218
2980
  } else {
2219
2981
  throw e;
2220
2982
  }
2221
2983
  }
2222
2984
  }
2223
2985
  s?.stop(`${templateName} template downloaded`);
2224
- const migrationPath = path3.join(cwd, "migrations", "db_init.sql");
2225
- 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);
2226
2988
  if (migrationExists) {
2227
- const dbSpinner = !json ? clack7.spinner() : null;
2989
+ const dbSpinner = !json ? clack11.spinner() : null;
2228
2990
  dbSpinner?.start("Running database migrations...");
2229
2991
  try {
2230
- const sql = await fs3.readFile(migrationPath, "utf-8");
2992
+ const sql = await fs4.readFile(migrationPath, "utf-8");
2231
2993
  await runRawSql(sql, true);
2232
2994
  dbSpinner?.stop("Database migrations applied");
2233
2995
  } catch (err) {
2234
2996
  dbSpinner?.stop("Database migration failed");
2235
2997
  if (!json) {
2236
- clack7.log.warn(`Migration failed: ${err.message}`);
2237
- 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)"');
2238
3000
  } else {
2239
3001
  throw err;
2240
3002
  }
@@ -2242,25 +3004,31 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
2242
3004
  }
2243
3005
  } catch (err) {
2244
3006
  s?.stop(`${templateName} template download failed`);
2245
- if (!json) {
2246
- clack7.log.warn(`Failed to download ${templateName} template: ${err.message}`);
2247
- 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");
2248
3013
  }
2249
3014
  } finally {
2250
- await fs3.rm(tempDir, { recursive: true, force: true }).catch(() => {
3015
+ await fs4.rm(tempDir, { recursive: true, force: true }).catch(() => {
2251
3016
  });
2252
3017
  }
2253
3018
  }
2254
3019
 
2255
3020
  // src/commands/projects/link.ts
2256
- var execAsync3 = promisify3(exec3);
3021
+ var execAsync3 = promisify4(exec3);
2257
3022
  function buildOssHost2(appkey, region) {
2258
3023
  return `https://${appkey}.${region}.insforge.app`;
2259
3024
  }
2260
3025
  function registerProjectLinkCommand(program2) {
2261
- 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) => {
2262
3027
  const { json, apiUrl } = getRootOpts(cmd);
2263
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
+ }
2264
3032
  try {
2265
3033
  if (opts.template && !validTemplates.includes(opts.template)) {
2266
3034
  throw new CLIError(`Invalid template "${opts.template}". Valid options: ${validTemplates.join(", ")}`);
@@ -2295,23 +3063,23 @@ function registerProjectLinkCommand(program2) {
2295
3063
  initialValue: defaultDir,
2296
3064
  validate: (v) => {
2297
3065
  if (v.length < 1) return "Directory name is required";
2298
- const normalized = path4.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
3066
+ const normalized = path5.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
2299
3067
  if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
2300
3068
  return void 0;
2301
3069
  }
2302
3070
  });
2303
3071
  if (isCancel2(inputDir)) process.exit(0);
2304
- dirName = path4.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
3072
+ dirName = path5.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
2305
3073
  }
2306
3074
  if (!dirName || dirName === "." || dirName === "..") {
2307
3075
  throw new CLIError("Invalid directory name.");
2308
3076
  }
2309
- const templateDir = path4.resolve(process.cwd(), dirName);
2310
- 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);
2311
3079
  if (dirExists) {
2312
3080
  throw new CLIError(`Directory "${dirName}" already exists.`);
2313
3081
  }
2314
- await fs4.mkdir(templateDir);
3082
+ await fs5.mkdir(templateDir);
2315
3083
  process.chdir(templateDir);
2316
3084
  saveProjectConfig(projectConfig2);
2317
3085
  if (json) {
@@ -2326,17 +3094,27 @@ function registerProjectLinkCommand(program2) {
2326
3094
  }
2327
3095
  captureEvent(FAKE_ORG_ID, "template_selected", { template: template2, source: "link_direct" });
2328
3096
  await downloadGitHubTemplate(template2, projectConfig2, json);
2329
- const templateDownloaded = await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null);
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
+ }
2330
3108
  if (templateDownloaded && !json) {
2331
- const installSpinner = clack8.spinner();
3109
+ const installSpinner = clack12.spinner();
2332
3110
  installSpinner.start("Installing dependencies...");
2333
3111
  try {
2334
3112
  await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
2335
3113
  installSpinner.stop("Dependencies installed");
2336
3114
  } catch (err) {
2337
3115
  installSpinner.stop("Failed to install dependencies");
2338
- clack8.log.warn(`npm install failed: ${err.message}`);
2339
- 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.");
2340
3118
  }
2341
3119
  }
2342
3120
  await installSkills(json);
@@ -2356,9 +3134,9 @@ function registerProjectLinkCommand(program2) {
2356
3134
  `${pc2.bold("1.")} ${runCommand}`,
2357
3135
  `${pc2.bold("2.")} Open ${pc2.cyan("Claude Code")} or ${pc2.cyan("Cursor")} and prompt your agent to add more features`
2358
3136
  ];
2359
- clack8.note(steps.join("\n"), "What's next");
3137
+ clack12.note(steps.join("\n"), "What's next");
2360
3138
  } else {
2361
- 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.");
2362
3140
  }
2363
3141
  }
2364
3142
  return;
@@ -2369,6 +3147,31 @@ function registerProjectLinkCommand(program2) {
2369
3147
  } else {
2370
3148
  outputSuccess(`Linked to direct project at ${projectConfig2.oss_host}`);
2371
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
+ }
2372
3175
  trackCommand("link", "oss-org", { direct: true });
2373
3176
  await installSkills(json);
2374
3177
  await reportCliUsage("cli.link_direct", true, 6, projectConfig2);
@@ -2396,7 +3199,7 @@ function registerProjectLinkCommand(program2) {
2396
3199
  }
2397
3200
  if (orgs.length === 1) {
2398
3201
  orgId = orgs[0].id;
2399
- if (!json) clack8.log.info(`Using organization: ${orgs[0].name}`);
3202
+ if (!json) clack12.log.info(`Using organization: ${orgs[0].name}`);
2400
3203
  } else {
2401
3204
  if (json) {
2402
3205
  throw new CLIError("Multiple organizations found. Specify --org-id.");
@@ -2482,68 +3285,91 @@ function registerProjectLinkCommand(program2) {
2482
3285
  initialValue: project.name,
2483
3286
  validate: (v) => {
2484
3287
  if (v.length < 1) return "Directory name is required";
2485
- const normalized = path4.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
3288
+ const normalized = path5.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
2486
3289
  if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
2487
3290
  return void 0;
2488
3291
  }
2489
3292
  });
2490
3293
  if (isCancel2(inputDir)) process.exit(0);
2491
- dirName = path4.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
3294
+ dirName = path5.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
2492
3295
  }
2493
3296
  if (!dirName || dirName === "." || dirName === "..") {
2494
3297
  throw new CLIError("Invalid directory name.");
2495
3298
  }
2496
- const templateDir = path4.resolve(process.cwd(), dirName);
2497
- 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);
2498
3301
  if (dirExists) {
2499
3302
  throw new CLIError(`Directory "${dirName}" already exists.`);
2500
3303
  }
2501
- await fs4.mkdir(templateDir);
3304
+ await fs5.mkdir(templateDir);
2502
3305
  process.chdir(templateDir);
2503
3306
  saveProjectConfig(projectConfig);
2504
3307
  captureEvent(orgId ?? project.organization_id, "template_selected", { template, source: "link" });
2505
3308
  await downloadGitHubTemplate(template, projectConfig, json);
2506
- 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
+ }
2507
3320
  if (templateDownloaded && !json) {
2508
- const installSpinner = clack8.spinner();
3321
+ const installSpinner = clack12.spinner();
2509
3322
  installSpinner.start("Installing dependencies...");
2510
3323
  try {
2511
3324
  await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
2512
3325
  installSpinner.stop("Dependencies installed");
2513
3326
  } catch (err) {
2514
3327
  installSpinner.stop("Failed to install dependencies");
2515
- clack8.log.warn(`npm install failed: ${err.message}`);
2516
- 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.");
2517
3330
  }
2518
3331
  }
2519
3332
  await installSkills(json);
2520
3333
  await reportCliUsage("cli.link", true, 6, projectConfig);
2521
3334
  if (!json) {
2522
3335
  const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
2523
- clack8.log.step(`Dashboard: ${pc2.underline(dashboardUrl)}`);
3336
+ clack12.log.step(`Dashboard: ${pc2.underline(dashboardUrl)}`);
2524
3337
  if (templateDownloaded) {
2525
3338
  const runCommand = `${pc2.cyan("cd")} ${pc2.green(dirName)} ${pc2.dim("&&")} ${pc2.cyan("npm run dev")}`;
2526
3339
  const steps = [
2527
3340
  `${pc2.bold("1.")} ${runCommand}`,
2528
3341
  `${pc2.bold("2.")} Open ${pc2.cyan("Claude Code")} or ${pc2.cyan("Cursor")} and prompt your agent to add more features`
2529
3342
  ];
2530
- clack8.note(steps.join("\n"), "What's next");
3343
+ clack12.note(steps.join("\n"), "What's next");
2531
3344
  } else {
2532
- 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.");
2533
3346
  }
2534
3347
  }
2535
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
+ }
2536
3362
  await installSkills(json);
2537
3363
  await reportCliUsage("cli.link", true, 6, projectConfig);
2538
3364
  if (!json) {
2539
3365
  const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
2540
- clack8.log.step(`Dashboard: ${dashboardUrl}`);
3366
+ clack12.log.step(`Dashboard: ${dashboardUrl}`);
2541
3367
  const prompts = [
2542
3368
  "Build a todo app with Google OAuth sign-in",
2543
3369
  "Build an Instagram clone where users can upload photos, like, and comment",
2544
3370
  "Build an AI chatbot with conversation history and deploy it to a live URL"
2545
3371
  ];
2546
- clack8.note(
3372
+ clack12.note(
2547
3373
  `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
2548
3374
 
2549
3375
  ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
@@ -2783,7 +3609,7 @@ function registerDbRpcCommand(dbCmd2) {
2783
3609
  }
2784
3610
 
2785
3611
  // src/commands/db/export.ts
2786
- import { writeFileSync as writeFileSync2 } from "fs";
3612
+ import { writeFileSync as writeFileSync3 } from "fs";
2787
3613
  function registerDbExportCommand(dbCmd2) {
2788
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) => {
2789
3615
  const { json } = getRootOpts(cmd);
@@ -2823,7 +3649,7 @@ function registerDbExportCommand(dbCmd2) {
2823
3649
  return;
2824
3650
  }
2825
3651
  if (opts.output) {
2826
- writeFileSync2(opts.output, content);
3652
+ writeFileSync3(opts.output, content);
2827
3653
  const tableCount = meta?.tables?.length;
2828
3654
  const suffix = tableCount ? ` (${tableCount} tables, format: ${meta?.format ?? opts.format})` : "";
2829
3655
  outputSuccess(`Exported to ${opts.output}${suffix}`);
@@ -2838,7 +3664,7 @@ function registerDbExportCommand(dbCmd2) {
2838
3664
 
2839
3665
  // src/commands/db/import.ts
2840
3666
  import { readFileSync as readFileSync3 } from "fs";
2841
- import { basename as basename4 } from "path";
3667
+ import { basename as basename5 } from "path";
2842
3668
  function registerDbImportCommand(dbCmd2) {
2843
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) => {
2844
3670
  const { json } = getRootOpts(cmd);
@@ -2847,7 +3673,7 @@ function registerDbImportCommand(dbCmd2) {
2847
3673
  const config = getProjectConfig();
2848
3674
  if (!config) throw new ProjectNotLinkedError();
2849
3675
  const fileContent = readFileSync3(file);
2850
- const fileName = basename4(file);
3676
+ const fileName = basename5(file);
2851
3677
  const formData = new FormData();
2852
3678
  formData.append("file", new Blob([fileContent]), fileName);
2853
3679
  if (opts.truncate) {
@@ -2877,12 +3703,12 @@ function registerDbImportCommand(dbCmd2) {
2877
3703
  }
2878
3704
 
2879
3705
  // src/commands/db/migrations.ts
2880
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2881
- 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";
2882
3708
 
2883
3709
  // src/lib/migrations.ts
2884
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync } from "fs";
2885
- import { join as join7 } from "path";
3710
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync } from "fs";
3711
+ import { join as join8 } from "path";
2886
3712
  var MIGRATION_VERSION_REGEX = /^\d{1,64}$/u;
2887
3713
  var MIGRATION_FILENAME_REGEX = /^(\d{1,64})_([a-z0-9-]+)\.sql$/u;
2888
3714
  function assertValidMigrationVersion(version) {
@@ -2946,18 +3772,18 @@ function incrementMigrationVersion(version) {
2946
3772
  return formatMigrationVersion(new Date(nextTimestamp));
2947
3773
  }
2948
3774
  function getMigrationsDir(cwd = process.cwd()) {
2949
- return join7(cwd, "migrations");
3775
+ return join8(cwd, "migrations");
2950
3776
  }
2951
3777
  function ensureMigrationsDir(cwd = process.cwd()) {
2952
3778
  const migrationsDir = getMigrationsDir(cwd);
2953
- if (!existsSync3(migrationsDir)) {
3779
+ if (!existsSync4(migrationsDir)) {
2954
3780
  mkdirSync2(migrationsDir, { recursive: true });
2955
3781
  }
2956
3782
  return migrationsDir;
2957
3783
  }
2958
3784
  function listLocalMigrationFilenames(cwd = process.cwd()) {
2959
3785
  const migrationsDir = getMigrationsDir(cwd);
2960
- if (!existsSync3(migrationsDir)) {
3786
+ if (!existsSync4(migrationsDir)) {
2961
3787
  return [];
2962
3788
  }
2963
3789
  return readdirSync(migrationsDir).sort((left, right) => left.localeCompare(right));
@@ -3145,12 +3971,12 @@ function registerDbMigrationsCommand(dbCmd2) {
3145
3971
  migration.version,
3146
3972
  migration.name
3147
3973
  );
3148
- const filePath = join8(migrationsDir, filename);
3149
- if (existingLocalVersions.has(migration.version) || existsSync4(filePath)) {
3974
+ const filePath = join9(migrationsDir, filename);
3975
+ if (existingLocalVersions.has(migration.version) || existsSync5(filePath)) {
3150
3976
  skippedFiles.push(filename);
3151
3977
  continue;
3152
3978
  }
3153
- writeFileSync3(filePath, formatMigrationSql(migration.statements));
3979
+ writeFileSync4(filePath, formatMigrationSql(migration.statements));
3154
3980
  createdFiles.push(filename);
3155
3981
  existingLocalVersions.add(migration.version);
3156
3982
  }
@@ -3188,9 +4014,9 @@ function registerDbMigrationsCommand(dbCmd2) {
3188
4014
  );
3189
4015
  const filename = buildMigrationFilename(nextVersion, migrationName);
3190
4016
  const migrationsDir = ensureMigrationsDir();
3191
- const filePath = join8(migrationsDir, filename);
4017
+ const filePath = join9(migrationsDir, filename);
3192
4018
  try {
3193
- writeFileSync3(filePath, "", { flag: "wx" });
4019
+ writeFileSync4(filePath, "", { flag: "wx" });
3194
4020
  } catch (error) {
3195
4021
  if (error.code === "EEXIST") {
3196
4022
  throw new CLIError(`Migration file already exists: ${filename}`);
@@ -3246,8 +4072,8 @@ function registerDbMigrationsCommand(dbCmd2) {
3246
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.`
3247
4073
  );
3248
4074
  }
3249
- const filePath = join8(getMigrationsDir(), targetMigration.filename);
3250
- if (!existsSync4(filePath)) {
4075
+ const filePath = join9(getMigrationsDir(), targetMigration.filename);
4076
+ if (!existsSync5(filePath)) {
3251
4077
  throw new CLIError(`Local migration file not found: ${targetMigration.filename}`);
3252
4078
  }
3253
4079
  const sql = readFileSync4(filePath, "utf-8");
@@ -3312,8 +4138,8 @@ function registerDbMigrationsCommand(dbCmd2) {
3312
4138
  }
3313
4139
  }
3314
4140
  for (const migration of migrationsToApply) {
3315
- const filePath = join8(getMigrationsDir(), migration.filename);
3316
- if (!existsSync4(filePath)) {
4141
+ const filePath = join9(getMigrationsDir(), migration.filename);
4142
+ if (!existsSync5(filePath)) {
3317
4143
  throw new CLIError(`Local migration file not found: ${migration.filename}`);
3318
4144
  }
3319
4145
  const sql = readFileSync4(filePath, "utf-8");
@@ -3352,8 +4178,8 @@ function registerRecordsCommands(recordsCmd2) {
3352
4178
  if (opts.limit) params.set("limit", String(opts.limit));
3353
4179
  if (opts.offset) params.set("offset", String(opts.offset));
3354
4180
  const query = params.toString();
3355
- const path5 = `/api/database/records/${encodeURIComponent(table)}${query ? `?${query}` : ""}`;
3356
- const res = await ossFetch(path5);
4181
+ const path6 = `/api/database/records/${encodeURIComponent(table)}${query ? `?${query}` : ""}`;
4182
+ const res = await ossFetch(path6);
3357
4183
  const data = await res.json();
3358
4184
  const records = data.data ?? [];
3359
4185
  if (json) {
@@ -3522,18 +4348,18 @@ function registerFunctionsCommands(functionsCmd2) {
3522
4348
  }
3523
4349
 
3524
4350
  // src/commands/functions/deploy.ts
3525
- import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
3526
- import { join as join9 } from "path";
4351
+ import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
4352
+ import { join as join10 } from "path";
3527
4353
  function registerFunctionsDeployCommand(functionsCmd2) {
3528
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) => {
3529
4355
  const { json } = getRootOpts(cmd);
3530
4356
  try {
3531
4357
  await requireAuth();
3532
- const filePath = opts.file ?? join9(process.cwd(), "insforge", "functions", slug, "index.ts");
3533
- if (!existsSync5(filePath)) {
4358
+ const filePath = opts.file ?? join10(process.cwd(), "insforge", "functions", slug, "index.ts");
4359
+ if (!existsSync6(filePath)) {
3534
4360
  throw new CLIError(
3535
4361
  `Source file not found: ${filePath}
3536
- Specify --file <path> or create ${join9("insforge", "functions", slug, "index.ts")}`
4362
+ Specify --file <path> or create ${join10("insforge", "functions", slug, "index.ts")}`
3537
4363
  );
3538
4364
  }
3539
4365
  const code = readFileSync5(filePath, "utf-8");
@@ -3657,7 +4483,7 @@ function registerFunctionsCodeCommand(functionsCmd2) {
3657
4483
  }
3658
4484
 
3659
4485
  // src/commands/functions/delete.ts
3660
- import * as clack9 from "@clack/prompts";
4486
+ import * as clack13 from "@clack/prompts";
3661
4487
  function registerFunctionsDeleteCommand(functionsCmd2) {
3662
4488
  functionsCmd2.command("delete <slug>").description("Delete an edge function").action(async (slug, _opts, cmd) => {
3663
4489
  const { json, yes } = getRootOpts(cmd);
@@ -3668,7 +4494,7 @@ function registerFunctionsDeleteCommand(functionsCmd2) {
3668
4494
  message: `Delete function "${slug}"? This cannot be undone.`
3669
4495
  });
3670
4496
  if (isCancel2(confirmed) || !confirmed) {
3671
- clack9.log.info("Cancelled.");
4497
+ clack13.log.info("Cancelled.");
3672
4498
  return;
3673
4499
  }
3674
4500
  }
@@ -3723,8 +4549,8 @@ function registerStorageBucketsCommand(storageCmd2) {
3723
4549
  }
3724
4550
 
3725
4551
  // src/commands/storage/upload.ts
3726
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
3727
- import { basename as basename5 } from "path";
4552
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
4553
+ import { basename as basename6 } from "path";
3728
4554
  function registerStorageUploadCommand(storageCmd2) {
3729
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) => {
3730
4556
  const { json } = getRootOpts(cmd);
@@ -3732,11 +4558,11 @@ function registerStorageUploadCommand(storageCmd2) {
3732
4558
  await requireAuth();
3733
4559
  const config = getProjectConfig();
3734
4560
  if (!config) throw new ProjectNotLinkedError();
3735
- if (!existsSync6(file)) {
4561
+ if (!existsSync7(file)) {
3736
4562
  throw new CLIError(`File not found: ${file}`);
3737
4563
  }
3738
4564
  const fileContent = readFileSync6(file);
3739
- const objectKey = opts.key ?? basename5(file);
4565
+ const objectKey = opts.key ?? basename6(file);
3740
4566
  const bucketName = opts.bucket;
3741
4567
  const formData = new FormData();
3742
4568
  const blob = new Blob([fileContent]);
@@ -3757,7 +4583,7 @@ function registerStorageUploadCommand(storageCmd2) {
3757
4583
  if (json) {
3758
4584
  outputJson(data);
3759
4585
  } else {
3760
- outputSuccess(`Uploaded "${basename5(file)}" to bucket "${bucketName}".`);
4586
+ outputSuccess(`Uploaded "${basename6(file)}" to bucket "${bucketName}".`);
3761
4587
  }
3762
4588
  } catch (err) {
3763
4589
  handleError(err, json);
@@ -3766,8 +4592,8 @@ function registerStorageUploadCommand(storageCmd2) {
3766
4592
  }
3767
4593
 
3768
4594
  // src/commands/storage/download.ts
3769
- import { writeFileSync as writeFileSync4 } from "fs";
3770
- 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";
3771
4597
  function registerStorageDownloadCommand(storageCmd2) {
3772
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) => {
3773
4599
  const { json } = getRootOpts(cmd);
@@ -3787,8 +4613,8 @@ function registerStorageDownloadCommand(storageCmd2) {
3787
4613
  throw new CLIError(err.error ?? `Download failed: ${res.status}`);
3788
4614
  }
3789
4615
  const buffer = Buffer.from(await res.arrayBuffer());
3790
- const outputPath = opts.output ?? join10(process.cwd(), basename6(objectKey));
3791
- writeFileSync4(outputPath, buffer);
4616
+ const outputPath = opts.output ?? join11(process.cwd(), basename7(objectKey));
4617
+ writeFileSync5(outputPath, buffer);
3792
4618
  if (json) {
3793
4619
  outputJson({ success: true, path: outputPath, size: buffer.length });
3794
4620
  } else {
@@ -3832,10 +4658,10 @@ function registerStorageDeleteBucketCommand(storageCmd2) {
3832
4658
  try {
3833
4659
  await requireAuth();
3834
4660
  if (!yes && !json) {
3835
- const confirm3 = await confirm2({
4661
+ const confirm6 = await confirm2({
3836
4662
  message: `Delete bucket "${name}" and all its objects? This cannot be undone.`
3837
4663
  });
3838
- if (isCancel2(confirm3) || !confirm3) {
4664
+ if (isCancel2(confirm6) || !confirm6) {
3839
4665
  process.exit(0);
3840
4666
  }
3841
4667
  }
@@ -4245,8 +5071,8 @@ async function listDocs(json) {
4245
5071
  );
4246
5072
  }
4247
5073
  }
4248
- async function fetchDoc(path5, label, json) {
4249
- const res = await ossFetch(path5);
5074
+ async function fetchDoc(path6, label, json) {
5075
+ const res = await ossFetch(path6);
4250
5076
  const data = await res.json();
4251
5077
  const doc = data.data ?? data;
4252
5078
  if (json) {
@@ -4386,10 +5212,10 @@ function registerSecretsDeleteCommand(secretsCmd2) {
4386
5212
  try {
4387
5213
  await requireAuth();
4388
5214
  if (!yes && !json) {
4389
- const confirm3 = await confirm2({
5215
+ const confirm6 = await confirm2({
4390
5216
  message: `Delete secret "${key}"? This cannot be undone.`
4391
5217
  });
4392
- if (isCancel2(confirm3) || !confirm3) {
5218
+ if (isCancel2(confirm6) || !confirm6) {
4393
5219
  process.exit(0);
4394
5220
  }
4395
5221
  }
@@ -4577,10 +5403,10 @@ function registerSchedulesDeleteCommand(schedulesCmd2) {
4577
5403
  try {
4578
5404
  await requireAuth();
4579
5405
  if (!yes && !json) {
4580
- const confirm3 = await confirm2({
5406
+ const confirm6 = await confirm2({
4581
5407
  message: `Delete schedule "${id}"? This cannot be undone.`
4582
5408
  });
4583
- if (isCancel2(confirm3) || !confirm3) {
5409
+ if (isCancel2(confirm6) || !confirm6) {
4584
5410
  process.exit(0);
4585
5411
  }
4586
5412
  }
@@ -4704,8 +5530,44 @@ function registerComputeGetCommand(computeCmd2) {
4704
5530
  }
4705
5531
 
4706
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
+ }
4707
5556
  function registerComputeUpdateCommand(computeCmd2) {
4708
- 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) => {
4709
5571
  const { json } = getRootOpts(cmd);
4710
5572
  try {
4711
5573
  await requireAuth();
@@ -4725,6 +5587,14 @@ function registerComputeUpdateCommand(computeCmd2) {
4725
5587
  body.memory = Number(opts.memory);
4726
5588
  }
4727
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
+ }
4728
5598
  if (opts.env) {
4729
5599
  try {
4730
5600
  body.envVars = JSON.parse(opts.env);
@@ -4732,8 +5602,22 @@ function registerComputeUpdateCommand(computeCmd2) {
4732
5602
  throw new CLIError("Invalid JSON for --env");
4733
5603
  }
4734
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
+ }
4735
5617
  if (Object.keys(body).length === 0) {
4736
- 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
+ );
4737
5621
  }
4738
5622
  const res = await ossFetch(`/api/compute/services/${encodeURIComponent(id)}`, {
4739
5623
  method: "PATCH",
@@ -4825,46 +5709,86 @@ function registerComputeStopCommand(computeCmd2) {
4825
5709
  });
4826
5710
  }
4827
5711
 
4828
- // src/commands/compute/logs.ts
4829
- function registerComputeLogsCommand(computeCmd2) {
4830
- 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) => {
4831
5715
  const { json } = getRootOpts(cmd);
4832
5716
  try {
4833
5717
  await requireAuth();
4834
5718
  const limit = Math.max(1, Math.min(Number(opts.limit) || 50, 1e3));
4835
5719
  const res = await ossFetch(
4836
- `/api/compute/services/${encodeURIComponent(id)}/logs?limit=${limit}`
5720
+ `/api/compute/services/${encodeURIComponent(id)}/events?limit=${limit}`
4837
5721
  );
4838
- const logs = await res.json();
5722
+ const events = await res.json();
4839
5723
  if (json) {
4840
- outputJson(logs);
5724
+ outputJson(events);
4841
5725
  } else {
4842
- if (!Array.isArray(logs) || logs.length === 0) {
4843
- console.log("No logs found.");
4844
- 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);
4845
5729
  return;
4846
5730
  }
4847
- for (const entry of logs) {
5731
+ for (const entry of events) {
4848
5732
  const ts = new Date(entry.timestamp).toISOString();
4849
5733
  console.log(`${ts} ${entry.message}`);
4850
5734
  }
4851
5735
  }
4852
- await reportCliUsage("cli.compute.logs", true);
5736
+ await reportCliUsage("cli.compute.events", true);
4853
5737
  } catch (err) {
4854
- await reportCliUsage("cli.compute.logs", false);
5738
+ await reportCliUsage("cli.compute.events", false);
4855
5739
  handleError(err, json);
4856
5740
  }
4857
5741
  });
4858
5742
  }
4859
5743
 
4860
5744
  // src/commands/compute/deploy.ts
4861
- import { existsSync as existsSync8 } from "fs";
4862
- 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
+ }
4863
5787
 
4864
5788
  // src/lib/flyctl.ts
4865
5789
  import { spawn, spawnSync } from "child_process";
4866
- import { existsSync as existsSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
4867
- 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";
4868
5792
  function ensureFlyctlAvailable() {
4869
5793
  const r = spawnSync("flyctl", ["version"], {
4870
5794
  encoding: "utf8",
@@ -4877,8 +5801,8 @@ function ensureFlyctlAvailable() {
4877
5801
  }
4878
5802
  }
4879
5803
  function ensureFlyTomlStub(opts) {
4880
- const path5 = join11(opts.dir, "fly.toml");
4881
- if (existsSync7(path5)) {
5804
+ const path6 = join12(opts.dir, "fly.toml");
5805
+ if (existsSync8(path6)) {
4882
5806
  return () => {
4883
5807
  };
4884
5808
  }
@@ -4895,10 +5819,10 @@ primary_region = "${opts.region}"
4895
5819
  auto_start_machines = true
4896
5820
  min_machines_running = 0
4897
5821
  `;
4898
- writeFileSync5(path5, stub, "utf8");
5822
+ writeFileSync6(path6, stub, "utf8");
4899
5823
  return () => {
4900
5824
  try {
4901
- unlinkSync2(path5);
5825
+ unlinkSync3(path6);
4902
5826
  } catch {
4903
5827
  }
4904
5828
  };
@@ -4973,7 +5897,10 @@ function registerComputeDeployCommand(computeCmd2) {
4973
5897
  "--cpu <tier>",
4974
5898
  "CPU tier in <kind>-<N>x format (e.g. shared-1x, performance-2x)",
4975
5899
  "shared-1x"
4976
- ).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) => {
4977
5904
  const { json } = getRootOpts(cmd);
4978
5905
  try {
4979
5906
  await requireAuth();
@@ -4993,6 +5920,11 @@ function registerComputeDeployCommand(computeCmd2) {
4993
5920
  if (!Number.isInteger(memory) || memory <= 0) {
4994
5921
  throw new CLIError(`Invalid --memory: ${opts.memory}`);
4995
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
+ }
4996
5928
  let envVars;
4997
5929
  if (opts.env) {
4998
5930
  let parsed;
@@ -5012,6 +5944,8 @@ function registerComputeDeployCommand(computeCmd2) {
5012
5944
  }
5013
5945
  }
5014
5946
  envVars = parsed;
5947
+ } else if (opts.envFile) {
5948
+ envVars = parseEnvFile(resolve4(opts.envFile));
5015
5949
  }
5016
5950
  const baseBody = {
5017
5951
  name: opts.name,
@@ -5054,8 +5988,8 @@ function registerComputeDeployCommand(computeCmd2) {
5054
5988
  return;
5055
5989
  }
5056
5990
  const absDir = resolve4(dir);
5057
- const dockerfilePath = join12(absDir, "Dockerfile");
5058
- if (!existsSync8(dockerfilePath)) {
5991
+ const dockerfilePath = join13(absDir, "Dockerfile");
5992
+ if (!existsSync9(dockerfilePath)) {
5059
5993
  throw new CLIError(
5060
5994
  `No Dockerfile at ${dockerfilePath}.
5061
5995
  Either:
@@ -5289,7 +6223,7 @@ function formatSize2(gb) {
5289
6223
 
5290
6224
  // src/commands/diagnose/index.ts
5291
6225
  import * as os from "os";
5292
- import * as clack10 from "@clack/prompts";
6226
+ import * as clack14 from "@clack/prompts";
5293
6227
 
5294
6228
  // src/commands/diagnose/metrics.ts
5295
6229
  var METRIC_LABELS = {
@@ -5830,10 +6764,10 @@ function registerDiagnoseCommands(diagnoseCmd2) {
5830
6764
  if (question.length === 0 || question.length > 2e3) {
5831
6765
  throw new CLIError("Question must be between 1 and 2000 characters.");
5832
6766
  }
5833
- const s = !json ? clack10.spinner() : null;
6767
+ const s = !json ? clack14.spinner() : null;
5834
6768
  s?.start("Collecting diagnostic data...");
5835
6769
  const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
5836
- const cliVersion = "0.1.62";
6770
+ const cliVersion = "0.1.63";
5837
6771
  s?.stop("Data collected");
5838
6772
  if (!json) {
5839
6773
  console.log(`
@@ -5966,9 +6900,9 @@ function registerDiagnoseCommands(diagnoseCmd2) {
5966
6900
  void 0,
5967
6901
  apiUrl
5968
6902
  );
5969
- clack10.log.success("Thanks for your feedback!");
6903
+ clack14.log.success("Thanks for your feedback!");
5970
6904
  } catch {
5971
- clack10.log.warn("Failed to submit rating.");
6905
+ clack14.log.warn("Failed to submit rating.");
5972
6906
  }
5973
6907
  }
5974
6908
  }
@@ -6061,17 +6995,20 @@ function formatBytesCompact(bytes) {
6061
6995
  }
6062
6996
 
6063
6997
  // src/lib/api/payments.ts
6064
- function withQuery(path5, params) {
6998
+ function withQuery(path6, params) {
6065
6999
  const query = new URLSearchParams();
6066
7000
  for (const [key, value] of Object.entries(params)) {
6067
7001
  if (value !== void 0) query.set(key, String(value));
6068
7002
  }
6069
7003
  const suffix = query.toString();
6070
- return suffix ? `${path5}?${suffix}` : path5;
7004
+ return suffix ? `${path6}?${suffix}` : path6;
6071
7005
  }
6072
7006
  async function readJson(res) {
6073
7007
  return await res.json();
6074
7008
  }
7009
+ function withEnvironmentPath(environment, suffix) {
7010
+ return `/api/payments/${encodeURIComponent(environment)}${suffix}`;
7011
+ }
6075
7012
  async function getPaymentsStatus() {
6076
7013
  return readJson(await ossFetch("/api/payments/status"));
6077
7014
  }
@@ -6079,93 +7016,158 @@ async function getPaymentsConfig() {
6079
7016
  return readJson(await ossFetch("/api/payments/config"));
6080
7017
  }
6081
7018
  async function setStripeSecretKey(environment, secretKey) {
6082
- return readJson(await ossFetch("/api/payments/config", {
6083
- method: "POST",
6084
- body: JSON.stringify({ environment, secretKey })
6085
- }));
7019
+ return readJson(
7020
+ await ossFetch(withEnvironmentPath(environment, "/config"), {
7021
+ method: "PUT",
7022
+ body: JSON.stringify({ secretKey })
7023
+ })
7024
+ );
6086
7025
  }
6087
7026
  async function removeStripeSecretKey(environment) {
6088
- return readJson(await ossFetch(`/api/payments/config/${encodeURIComponent(environment)}`, {
6089
- method: "DELETE"
6090
- }));
7027
+ return readJson(
7028
+ await ossFetch(withEnvironmentPath(environment, "/config"), {
7029
+ method: "DELETE"
7030
+ })
7031
+ );
6091
7032
  }
6092
7033
  async function syncPayments(environment = "all") {
6093
- return readJson(await ossFetch("/api/payments/sync", {
6094
- method: "POST",
6095
- body: JSON.stringify({ environment })
6096
- }));
7034
+ return readJson(
7035
+ await ossFetch(
7036
+ environment === "all" ? "/api/payments/sync" : withEnvironmentPath(environment, "/sync"),
7037
+ { method: "POST" }
7038
+ )
7039
+ );
6097
7040
  }
6098
7041
  async function configurePaymentWebhook(environment) {
6099
- return readJson(await ossFetch(
6100
- `/api/payments/webhooks/${encodeURIComponent(environment)}/configure`,
6101
- { method: "POST" }
6102
- ));
7042
+ return readJson(
7043
+ await ossFetch(withEnvironmentPath(environment, "/webhook"), {
7044
+ method: "POST"
7045
+ })
7046
+ );
6103
7047
  }
6104
7048
  async function listPaymentCatalog(environment) {
6105
- return readJson(await ossFetch(withQuery("/api/payments/catalog", { environment })));
7049
+ return readJson(await ossFetch(withEnvironmentPath(environment, "/catalog")));
6106
7050
  }
6107
7051
  async function listPaymentProducts(environment) {
6108
- return readJson(await ossFetch(withQuery("/api/payments/products", { environment })));
7052
+ return readJson(
7053
+ await ossFetch(withEnvironmentPath(environment, "/catalog/products"))
7054
+ );
6109
7055
  }
6110
7056
  async function getPaymentProduct(environment, productId) {
6111
- return readJson(await ossFetch(withQuery(
6112
- `/api/payments/products/${encodeURIComponent(productId)}`,
6113
- { environment }
6114
- )));
7057
+ return readJson(
7058
+ await ossFetch(
7059
+ withEnvironmentPath(
7060
+ environment,
7061
+ `/catalog/products/${encodeURIComponent(productId)}`
7062
+ )
7063
+ )
7064
+ );
6115
7065
  }
6116
- async function createPaymentProduct(request) {
6117
- return readJson(await ossFetch("/api/payments/products", {
6118
- method: "POST",
6119
- body: JSON.stringify(request)
6120
- }));
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
+ );
6121
7073
  }
6122
- async function updatePaymentProduct(productId, request) {
6123
- return readJson(await ossFetch(`/api/payments/products/${encodeURIComponent(productId)}`, {
6124
- method: "PATCH",
6125
- body: JSON.stringify(request)
6126
- }));
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
+ );
6127
7087
  }
6128
7088
  async function deletePaymentProduct(environment, productId) {
6129
- return readJson(await ossFetch(withQuery(
6130
- `/api/payments/products/${encodeURIComponent(productId)}`,
6131
- { environment }
6132
- ), { method: "DELETE" }));
7089
+ return readJson(
7090
+ await ossFetch(
7091
+ withEnvironmentPath(
7092
+ environment,
7093
+ `/catalog/products/${encodeURIComponent(productId)}`
7094
+ ),
7095
+ { method: "DELETE" }
7096
+ )
7097
+ );
6133
7098
  }
6134
7099
  async function listPaymentPrices(environment, stripeProductId) {
6135
- return readJson(await ossFetch(withQuery("/api/payments/prices", {
6136
- environment,
6137
- stripeProductId
6138
- })));
7100
+ return readJson(
7101
+ await ossFetch(
7102
+ withQuery(withEnvironmentPath(environment, "/catalog/prices"), {
7103
+ stripeProductId
7104
+ })
7105
+ )
7106
+ );
6139
7107
  }
6140
7108
  async function getPaymentPrice(environment, priceId) {
6141
- return readJson(await ossFetch(withQuery(
6142
- `/api/payments/prices/${encodeURIComponent(priceId)}`,
6143
- { environment }
6144
- )));
7109
+ return readJson(
7110
+ await ossFetch(
7111
+ withEnvironmentPath(
7112
+ environment,
7113
+ `/catalog/prices/${encodeURIComponent(priceId)}`
7114
+ )
7115
+ )
7116
+ );
6145
7117
  }
6146
- async function createPaymentPrice(request) {
6147
- return readJson(await ossFetch("/api/payments/prices", {
6148
- method: "POST",
6149
- body: JSON.stringify(request)
6150
- }));
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
+ );
6151
7125
  }
6152
- async function updatePaymentPrice(priceId, request) {
6153
- return readJson(await ossFetch(`/api/payments/prices/${encodeURIComponent(priceId)}`, {
6154
- method: "PATCH",
6155
- body: JSON.stringify(request)
6156
- }));
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
+ );
6157
7139
  }
6158
7140
  async function archivePaymentPrice(environment, priceId) {
6159
- return readJson(await ossFetch(withQuery(
6160
- `/api/payments/prices/${encodeURIComponent(priceId)}`,
6161
- { environment }
6162
- ), { method: "DELETE" }));
7141
+ return readJson(
7142
+ await ossFetch(
7143
+ withEnvironmentPath(
7144
+ environment,
7145
+ `/catalog/prices/${encodeURIComponent(priceId)}`
7146
+ ),
7147
+ { method: "DELETE" }
7148
+ )
7149
+ );
6163
7150
  }
6164
- async function listSubscriptions(request) {
6165
- return readJson(await ossFetch(withQuery("/api/payments/subscriptions", request)));
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
+ );
6166
7164
  }
6167
- async function listPaymentHistory(request) {
6168
- return readJson(await ossFetch(withQuery("/api/payments/payment-history", request)));
7165
+ async function listPaymentHistory(environment, request) {
7166
+ return readJson(
7167
+ await ossFetch(
7168
+ withQuery(withEnvironmentPath(environment, "/payment-history"), request)
7169
+ )
7170
+ );
6169
7171
  }
6170
7172
 
6171
7173
  // src/commands/payments/utils.ts
@@ -6263,10 +7265,13 @@ async function trackPaymentUsage(subcommand, success, properties = {}) {
6263
7265
 
6264
7266
  // src/commands/payments/catalog.ts
6265
7267
  function registerPaymentsCatalogCommand(paymentsCmd2) {
6266
- paymentsCmd2.command("catalog").description("List mirrored Stripe products and prices").option("--environment <environment>", "Stripe environment: test or live").action(async (opts, cmd) => {
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) => {
6267
7272
  const { json } = getRootOpts(cmd);
6268
7273
  try {
6269
- const environment = opts.environment ? parseEnvironment(opts.environment) : void 0;
7274
+ const environment = parseEnvironment(opts.environment);
6270
7275
  await requireAuth();
6271
7276
  const data = await listPaymentCatalog(environment);
6272
7277
  if (json) {
@@ -6293,7 +7298,15 @@ function registerPaymentsCatalogCommand(paymentsCmd2) {
6293
7298
  if (data.prices.length > 0) {
6294
7299
  console.log("Prices");
6295
7300
  outputTable(
6296
- ["Env", "Price ID", "Product ID", "Amount", "Type", "Active", "Recurring"],
7301
+ [
7302
+ "Env",
7303
+ "Price ID",
7304
+ "Product ID",
7305
+ "Amount",
7306
+ "Type",
7307
+ "Active",
7308
+ "Recurring"
7309
+ ],
6297
7310
  data.prices.map((price) => [
6298
7311
  price.environment,
6299
7312
  price.stripePriceId,
@@ -6301,14 +7314,19 @@ function registerPaymentsCatalogCommand(paymentsCmd2) {
6301
7314
  formatAmount(price.unitAmount, price.currency),
6302
7315
  price.type,
6303
7316
  price.active ? "Yes" : "No",
6304
- formatRecurring(price.recurringInterval, price.recurringIntervalCount)
7317
+ formatRecurring(
7318
+ price.recurringInterval,
7319
+ price.recurringIntervalCount
7320
+ )
6305
7321
  ])
6306
7322
  );
6307
7323
  }
6308
7324
  }
6309
7325
  await trackPaymentUsage("catalog", true, { environment });
6310
7326
  } catch (err) {
6311
- await trackPaymentUsage("catalog", false, { environment: opts.environment });
7327
+ await trackPaymentUsage("catalog", false, {
7328
+ environment: opts.environment
7329
+ });
6312
7330
  handleError(err, json);
6313
7331
  }
6314
7332
  });
@@ -6383,10 +7401,10 @@ function registerPaymentsConfigCommand(paymentsCmd2) {
6383
7401
  throw new CLIError("Use --yes with --json to remove a Stripe key non-interactively.");
6384
7402
  }
6385
7403
  if (!yes) {
6386
- const confirm3 = await confirm2({
7404
+ const confirm6 = await confirm2({
6387
7405
  message: `Remove Stripe ${environment} key? Payment sync and mutations for this environment will stop.`
6388
7406
  });
6389
- if (isCancel2(confirm3) || !confirm3) process.exit(0);
7407
+ if (isCancel2(confirm6) || !confirm6) process.exit(0);
6390
7408
  }
6391
7409
  const data = await removeStripeSecretKey(environment);
6392
7410
  if (json) {
@@ -6402,16 +7420,80 @@ function registerPaymentsConfigCommand(paymentsCmd2) {
6402
7420
  });
6403
7421
  }
6404
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
+
6405
7485
  // src/commands/payments/history.ts
6406
7486
  function registerPaymentsHistoryCommand(paymentsCmd2) {
6407
- paymentsCmd2.command("history").description("List mirrored Stripe payment history").requiredOption("--environment <environment>", "Stripe environment: test or live").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) => {
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) => {
6408
7491
  const { json } = getRootOpts(cmd);
6409
7492
  try {
6410
7493
  const environment = parseEnvironment(opts.environment);
6411
7494
  const limit = parseIntegerOption(opts.limit, "--limit", { min: 1, max: 100 }) ?? 50;
6412
7495
  await requireAuth();
6413
- const data = await listPaymentHistory({
6414
- environment,
7496
+ const data = await listPaymentHistory(environment, {
6415
7497
  limit,
6416
7498
  ...opts.subjectType !== void 0 ? { subjectType: opts.subjectType } : {},
6417
7499
  ...opts.subjectId !== void 0 ? { subjectId: opts.subjectId } : {}
@@ -6422,7 +7504,15 @@ function registerPaymentsHistoryCommand(paymentsCmd2) {
6422
7504
  console.log("No Stripe payment history found.");
6423
7505
  } else {
6424
7506
  outputTable(
6425
- ["Type", "Status", "Subject", "Amount", "Customer", "Stripe Object", "When"],
7507
+ [
7508
+ "Type",
7509
+ "Status",
7510
+ "Subject",
7511
+ "Amount",
7512
+ "Customer",
7513
+ "Stripe Object",
7514
+ "When"
7515
+ ],
6426
7516
  data.paymentHistory.map((entry) => [
6427
7517
  entry.type,
6428
7518
  entry.status,
@@ -6430,13 +7520,17 @@ function registerPaymentsHistoryCommand(paymentsCmd2) {
6430
7520
  formatAmount(entry.amount, entry.currency),
6431
7521
  entry.stripeCustomerId ?? "-",
6432
7522
  entry.stripeCheckoutSessionId ?? entry.stripeInvoiceId ?? entry.stripePaymentIntentId ?? entry.stripeRefundId ?? "-",
6433
- formatDate(entry.paidAt ?? entry.failedAt ?? entry.refundedAt ?? entry.stripeCreatedAt)
7523
+ formatDate(
7524
+ entry.paidAt ?? entry.failedAt ?? entry.refundedAt ?? entry.stripeCreatedAt
7525
+ )
6434
7526
  ])
6435
7527
  );
6436
7528
  }
6437
7529
  await trackPaymentUsage("history", true, { environment });
6438
7530
  } catch (err) {
6439
- await trackPaymentUsage("history", false, { environment: opts.environment });
7531
+ await trackPaymentUsage("history", false, {
7532
+ environment: opts.environment
7533
+ });
6440
7534
  handleError(err, json);
6441
7535
  }
6442
7536
  });
@@ -6448,14 +7542,24 @@ function nullableString(value) {
6448
7542
  return value === "null" ? null : value;
6449
7543
  }
6450
7544
  function parseRecurringInterval(value) {
6451
- if (value === void 0) return void 0;
6452
- if (value === "day" || value === "week" || value === "month" || value === "year") return 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
+ }
6453
7551
  throw new CLIError("--interval must be one of: day, week, month, year.");
6454
7552
  }
6455
7553
  function parseTaxBehavior(value) {
6456
- if (value === void 0) return void 0;
6457
- if (value === "exclusive" || value === "inclusive" || value === "unspecified") return value;
6458
- throw new CLIError("--tax-behavior must be one of: exclusive, inclusive, unspecified.");
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
+ );
6459
7563
  }
6460
7564
  function outputPricesTable(prices) {
6461
7565
  if (prices.length === 0) {
@@ -6463,7 +7567,16 @@ function outputPricesTable(prices) {
6463
7567
  return;
6464
7568
  }
6465
7569
  outputTable(
6466
- ["Env", "Price ID", "Product ID", "Amount", "Type", "Active", "Recurring", "Synced At"],
7570
+ [
7571
+ "Env",
7572
+ "Price ID",
7573
+ "Product ID",
7574
+ "Amount",
7575
+ "Type",
7576
+ "Active",
7577
+ "Recurring",
7578
+ "Synced At"
7579
+ ],
6467
7580
  prices.map((price) => [
6468
7581
  price.environment,
6469
7582
  price.stripePriceId,
@@ -6478,7 +7591,10 @@ function outputPricesTable(prices) {
6478
7591
  }
6479
7592
  function registerPaymentsPricesCommand(paymentsCmd2) {
6480
7593
  const pricesCmd = paymentsCmd2.command("prices").description("Manage Stripe prices");
6481
- pricesCmd.command("list").description("List mirrored Stripe prices").requiredOption("--environment <environment>", "Stripe environment: test or live").option("--product <productId>", "Filter by Stripe product id").action(async (opts, cmd) => {
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) => {
6482
7598
  const { json } = getRootOpts(cmd);
6483
7599
  try {
6484
7600
  const environment = parseEnvironment(opts.environment);
@@ -6491,11 +7607,16 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
6491
7607
  }
6492
7608
  await trackPaymentUsage("prices.list", true, { environment });
6493
7609
  } catch (err) {
6494
- await trackPaymentUsage("prices.list", false, { environment: opts.environment });
7610
+ await trackPaymentUsage("prices.list", false, {
7611
+ environment: opts.environment
7612
+ });
6495
7613
  handleError(err, json);
6496
7614
  }
6497
7615
  });
6498
- pricesCmd.command("get <priceId>").description("Show one Stripe price").requiredOption("--environment <environment>", "Stripe environment: test or live").action(async (priceId, opts, cmd) => {
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) => {
6499
7620
  const { json } = getRootOpts(cmd);
6500
7621
  try {
6501
7622
  const environment = parseEnvironment(opts.environment);
@@ -6508,22 +7629,39 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
6508
7629
  }
6509
7630
  await trackPaymentUsage("prices.get", true, { environment });
6510
7631
  } catch (err) {
6511
- await trackPaymentUsage("prices.get", false, { environment: opts.environment });
7632
+ await trackPaymentUsage("prices.get", false, {
7633
+ environment: opts.environment
7634
+ });
6512
7635
  handleError(err, json);
6513
7636
  }
6514
7637
  });
6515
- pricesCmd.command("create").description("Create a Stripe one-time or recurring price").requiredOption("--environment <environment>", "Stripe environment: test or live").requiredOption("--product <productId>", "Stripe product id").requiredOption("--currency <currency>", "Three-letter currency code, e.g. usd").requiredOption("--unit-amount <amount>", "Unit amount in the smallest currency unit, e.g. cents").option("--interval <interval>", "Recurring interval: day, week, month, or year").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) => {
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) => {
6516
7651
  const { json } = getRootOpts(cmd);
6517
7652
  try {
6518
7653
  const environment = parseEnvironment(opts.environment);
6519
7654
  await requireAuth();
6520
7655
  const interval = parseRecurringInterval(opts.interval);
6521
- const intervalCount = parseIntegerOption(opts.intervalCount, "--interval-count", { min: 1 });
7656
+ const intervalCount = parseIntegerOption(
7657
+ opts.intervalCount,
7658
+ "--interval-count",
7659
+ { min: 1 }
7660
+ );
6522
7661
  if (!interval && intervalCount !== void 0) {
6523
7662
  throw new CLIError("Provide --interval when using --interval-count.");
6524
7663
  }
6525
7664
  const request = {
6526
- environment,
6527
7665
  stripeProductId: opts.product,
6528
7666
  currency: opts.currency,
6529
7667
  unitAmount: parseIntegerOption(opts.unitAmount, "--unit-amount", { min: 0 }) ?? 0
@@ -6536,14 +7674,16 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
6536
7674
  if (active !== void 0) request.active = active;
6537
7675
  if (taxBehavior !== void 0) request.taxBehavior = taxBehavior;
6538
7676
  if (metadata !== void 0) request.metadata = metadata;
6539
- if (opts.idempotencyKey !== void 0) request.idempotencyKey = opts.idempotencyKey;
7677
+ if (opts.idempotencyKey !== void 0) {
7678
+ request.idempotencyKey = opts.idempotencyKey;
7679
+ }
6540
7680
  if (interval) {
6541
7681
  request.recurring = {
6542
7682
  interval,
6543
7683
  ...intervalCount !== void 0 ? { intervalCount } : {}
6544
7684
  };
6545
7685
  }
6546
- const data = await createPaymentPrice(request);
7686
+ const data = await createPaymentPrice(environment, request);
6547
7687
  if (json) {
6548
7688
  outputJson(data);
6549
7689
  } else {
@@ -6551,16 +7691,21 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
6551
7691
  }
6552
7692
  await trackPaymentUsage("prices.create", true, { environment });
6553
7693
  } catch (err) {
6554
- await trackPaymentUsage("prices.create", false, { environment: opts.environment });
7694
+ await trackPaymentUsage("prices.create", false, {
7695
+ environment: opts.environment
7696
+ });
6555
7697
  handleError(err, json);
6556
7698
  }
6557
7699
  });
6558
- pricesCmd.command("update <priceId>").description("Update a Stripe price").requiredOption("--environment <environment>", "Stripe environment: test or live").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) => {
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) => {
6559
7704
  const { json } = getRootOpts(cmd);
6560
7705
  try {
6561
7706
  const environment = parseEnvironment(opts.environment);
6562
7707
  await requireAuth();
6563
- const request = { environment };
7708
+ const request = {};
6564
7709
  const active = parseBooleanOption(opts.active, "--active");
6565
7710
  const lookupKey = nullableString(opts.lookupKey);
6566
7711
  const taxBehavior = parseTaxBehavior(opts.taxBehavior);
@@ -6569,10 +7714,12 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
6569
7714
  if (lookupKey !== void 0) request.lookupKey = lookupKey;
6570
7715
  if (taxBehavior !== void 0) request.taxBehavior = taxBehavior;
6571
7716
  if (metadata !== void 0) request.metadata = metadata;
6572
- if (Object.keys(request).length === 1) {
6573
- throw new CLIError("Provide at least one option to update (--active, --lookup-key, --tax-behavior, --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
+ );
6574
7721
  }
6575
- const data = await updatePaymentPrice(priceId, request);
7722
+ const data = await updatePaymentPrice(environment, priceId, request);
6576
7723
  if (json) {
6577
7724
  outputJson(data);
6578
7725
  } else {
@@ -6580,11 +7727,16 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
6580
7727
  }
6581
7728
  await trackPaymentUsage("prices.update", true, { environment });
6582
7729
  } catch (err) {
6583
- await trackPaymentUsage("prices.update", false, { environment: opts.environment });
7730
+ await trackPaymentUsage("prices.update", false, {
7731
+ environment: opts.environment
7732
+ });
6584
7733
  handleError(err, json);
6585
7734
  }
6586
7735
  });
6587
- pricesCmd.command("archive <priceId>").alias("delete").description("Archive a Stripe price").requiredOption("--environment <environment>", "Stripe environment: test or live").action(async (priceId, opts, cmd) => {
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) => {
6588
7740
  const { json } = getRootOpts(cmd);
6589
7741
  try {
6590
7742
  const environment = parseEnvironment(opts.environment);
@@ -6597,7 +7749,9 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
6597
7749
  }
6598
7750
  await trackPaymentUsage("prices.archive", true, { environment });
6599
7751
  } catch (err) {
6600
- await trackPaymentUsage("prices.archive", false, { environment: opts.environment });
7752
+ await trackPaymentUsage("prices.archive", false, {
7753
+ environment: opts.environment
7754
+ });
6601
7755
  handleError(err, json);
6602
7756
  }
6603
7757
  });
@@ -6627,7 +7781,10 @@ function outputProductsTable(products) {
6627
7781
  }
6628
7782
  function registerPaymentsProductsCommand(paymentsCmd2) {
6629
7783
  const productsCmd = paymentsCmd2.command("products").description("Manage Stripe products");
6630
- productsCmd.command("list").description("List mirrored Stripe products").requiredOption("--environment <environment>", "Stripe environment: test or live").action(async (opts, cmd) => {
7784
+ productsCmd.command("list").description("List mirrored Stripe products").requiredOption(
7785
+ "--environment <environment>",
7786
+ "Stripe environment: test or live"
7787
+ ).action(async (opts, cmd) => {
6631
7788
  const { json } = getRootOpts(cmd);
6632
7789
  try {
6633
7790
  const environment = parseEnvironment(opts.environment);
@@ -6640,11 +7797,16 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
6640
7797
  }
6641
7798
  await trackPaymentUsage("products.list", true, { environment });
6642
7799
  } catch (err) {
6643
- await trackPaymentUsage("products.list", false, { environment: opts.environment });
7800
+ await trackPaymentUsage("products.list", false, {
7801
+ environment: opts.environment
7802
+ });
6644
7803
  handleError(err, json);
6645
7804
  }
6646
7805
  });
6647
- productsCmd.command("get <productId>").description("Show one Stripe product and its prices").requiredOption("--environment <environment>", "Stripe environment: test or live").action(async (productId, opts, cmd) => {
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) => {
6648
7810
  const { json } = getRootOpts(cmd);
6649
7811
  try {
6650
7812
  const environment = parseEnvironment(opts.environment);
@@ -6670,44 +7832,55 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
6670
7832
  }
6671
7833
  await trackPaymentUsage("products.get", true, { environment });
6672
7834
  } catch (err) {
6673
- await trackPaymentUsage("products.get", false, { environment: opts.environment });
7835
+ await trackPaymentUsage("products.get", false, {
7836
+ environment: opts.environment
7837
+ });
6674
7838
  handleError(err, json);
6675
7839
  }
6676
7840
  });
6677
- productsCmd.command("create").description("Create a Stripe product").requiredOption("--environment <environment>", "Stripe environment: test or live").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) => {
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) => {
6678
7845
  const { json } = getRootOpts(cmd);
6679
7846
  try {
6680
7847
  const environment = parseEnvironment(opts.environment);
6681
7848
  await requireAuth();
6682
- const request = {
6683
- environment,
6684
- name: opts.name
6685
- };
7849
+ const request = { name: opts.name };
6686
7850
  const description = nullableString2(opts.description);
6687
7851
  const active = parseBooleanOption(opts.active, "--active");
6688
7852
  const metadata = parseMetadataOption(opts.metadata);
6689
7853
  if (description !== void 0) request.description = description;
6690
7854
  if (active !== void 0) request.active = active;
6691
7855
  if (metadata !== void 0) request.metadata = metadata;
6692
- if (opts.idempotencyKey !== void 0) request.idempotencyKey = opts.idempotencyKey;
6693
- const data = await createPaymentProduct(request);
7856
+ if (opts.idempotencyKey !== void 0) {
7857
+ request.idempotencyKey = opts.idempotencyKey;
7858
+ }
7859
+ const data = await createPaymentProduct(environment, request);
6694
7860
  if (json) {
6695
7861
  outputJson(data);
6696
7862
  } else {
6697
- outputSuccess(`Stripe product created: ${data.product.stripeProductId}`);
7863
+ outputSuccess(
7864
+ `Stripe product created: ${data.product.stripeProductId}`
7865
+ );
6698
7866
  }
6699
7867
  await trackPaymentUsage("products.create", true, { environment });
6700
7868
  } catch (err) {
6701
- await trackPaymentUsage("products.create", false, { environment: opts.environment });
7869
+ await trackPaymentUsage("products.create", false, {
7870
+ environment: opts.environment
7871
+ });
6702
7872
  handleError(err, json);
6703
7873
  }
6704
7874
  });
6705
- productsCmd.command("update <productId>").description("Update a Stripe product").requiredOption("--environment <environment>", "Stripe environment: test or live").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) => {
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) => {
6706
7879
  const { json } = getRootOpts(cmd);
6707
7880
  try {
6708
7881
  const environment = parseEnvironment(opts.environment);
6709
7882
  await requireAuth();
6710
- const request = { environment };
7883
+ const request = {};
6711
7884
  const description = nullableString2(opts.description);
6712
7885
  const active = parseBooleanOption(opts.active, "--active");
6713
7886
  const metadata = parseMetadataOption(opts.metadata);
@@ -6715,34 +7888,49 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
6715
7888
  if (description !== void 0) request.description = description;
6716
7889
  if (active !== void 0) request.active = active;
6717
7890
  if (metadata !== void 0) request.metadata = metadata;
6718
- if (Object.keys(request).length === 1) {
6719
- throw new CLIError("Provide at least one option to update (--name, --description, --active, --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
+ );
6720
7895
  }
6721
- const data = await updatePaymentProduct(productId, request);
7896
+ const data = await updatePaymentProduct(
7897
+ environment,
7898
+ productId,
7899
+ request
7900
+ );
6722
7901
  if (json) {
6723
7902
  outputJson(data);
6724
7903
  } else {
6725
- outputSuccess(`Stripe product updated: ${data.product.stripeProductId}`);
7904
+ outputSuccess(
7905
+ `Stripe product updated: ${data.product.stripeProductId}`
7906
+ );
6726
7907
  }
6727
7908
  await trackPaymentUsage("products.update", true, { environment });
6728
7909
  } catch (err) {
6729
- await trackPaymentUsage("products.update", false, { environment: opts.environment });
7910
+ await trackPaymentUsage("products.update", false, {
7911
+ environment: opts.environment
7912
+ });
6730
7913
  handleError(err, json);
6731
7914
  }
6732
7915
  });
6733
- productsCmd.command("delete <productId>").description("Delete a Stripe product that has no prices").requiredOption("--environment <environment>", "Stripe environment: test or live").action(async (productId, opts, cmd) => {
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) => {
6734
7920
  const { json, yes } = getRootOpts(cmd);
6735
7921
  try {
6736
7922
  const environment = parseEnvironment(opts.environment);
6737
7923
  await requireAuth();
6738
7924
  if (json && !yes) {
6739
- throw new CLIError("Use --yes with --json to delete a Stripe product non-interactively.");
7925
+ throw new CLIError(
7926
+ "Use --yes with --json to delete a Stripe product non-interactively."
7927
+ );
6740
7928
  }
6741
7929
  if (!yes) {
6742
- const confirm3 = await confirm2({
7930
+ const confirm6 = await confirm2({
6743
7931
  message: `Delete Stripe ${environment} product "${productId}"?`
6744
7932
  });
6745
- if (isCancel2(confirm3) || !confirm3) process.exit(0);
7933
+ if (isCancel2(confirm6) || !confirm6) process.exit(0);
6746
7934
  }
6747
7935
  const data = await deletePaymentProduct(environment, productId);
6748
7936
  if (json) {
@@ -6752,7 +7940,9 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
6752
7940
  }
6753
7941
  await trackPaymentUsage("products.delete", true, { environment });
6754
7942
  } catch (err) {
6755
- await trackPaymentUsage("products.delete", false, { environment: opts.environment });
7943
+ await trackPaymentUsage("products.delete", false, {
7944
+ environment: opts.environment
7945
+ });
6756
7946
  handleError(err, json);
6757
7947
  }
6758
7948
  });
@@ -6793,14 +7983,16 @@ function registerPaymentsStatusCommand(paymentsCmd2) {
6793
7983
 
6794
7984
  // src/commands/payments/subscriptions.ts
6795
7985
  function registerPaymentsSubscriptionsCommand(paymentsCmd2) {
6796
- paymentsCmd2.command("subscriptions").description("List mirrored Stripe subscriptions").requiredOption("--environment <environment>", "Stripe environment: test or live").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) => {
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) => {
6797
7990
  const { json } = getRootOpts(cmd);
6798
7991
  try {
6799
7992
  const environment = parseEnvironment(opts.environment);
6800
7993
  const limit = parseIntegerOption(opts.limit, "--limit", { min: 1, max: 100 }) ?? 50;
6801
7994
  await requireAuth();
6802
- const data = await listSubscriptions({
6803
- environment,
7995
+ const data = await listSubscriptions(environment, {
6804
7996
  limit,
6805
7997
  ...opts.subjectType !== void 0 ? { subjectType: opts.subjectType } : {},
6806
7998
  ...opts.subjectId !== void 0 ? { subjectId: opts.subjectId } : {}
@@ -6811,7 +8003,14 @@ function registerPaymentsSubscriptionsCommand(paymentsCmd2) {
6811
8003
  console.log("No Stripe subscriptions found.");
6812
8004
  } else {
6813
8005
  outputTable(
6814
- ["Subscription ID", "Customer", "Subject", "Status", "Items", "Period End"],
8006
+ [
8007
+ "Subscription ID",
8008
+ "Customer",
8009
+ "Subject",
8010
+ "Status",
8011
+ "Items",
8012
+ "Period End"
8013
+ ],
6815
8014
  data.subscriptions.map((subscription) => [
6816
8015
  subscription.stripeSubscriptionId,
6817
8016
  subscription.stripeCustomerId,
@@ -6824,7 +8023,9 @@ function registerPaymentsSubscriptionsCommand(paymentsCmd2) {
6824
8023
  }
6825
8024
  await trackPaymentUsage("subscriptions", true, { environment });
6826
8025
  } catch (err) {
6827
- await trackPaymentUsage("subscriptions", false, { environment: opts.environment });
8026
+ await trackPaymentUsage("subscriptions", false, {
8027
+ environment: opts.environment
8028
+ });
6828
8029
  handleError(err, json);
6829
8030
  }
6830
8031
  });
@@ -6832,7 +8033,13 @@ function registerPaymentsSubscriptionsCommand(paymentsCmd2) {
6832
8033
 
6833
8034
  // src/commands/payments/sync.ts
6834
8035
  function registerPaymentsSyncCommand(paymentsCmd2) {
6835
- paymentsCmd2.command("sync").description("Sync configured Stripe products, prices, and subscriptions").option("--environment <environment>", "Stripe environment: test, live, or all", "all").action(async (opts, cmd) => {
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) => {
6836
8043
  const { json } = getRootOpts(cmd);
6837
8044
  try {
6838
8045
  const environment = parseEnvironmentOrAll(opts.environment);
@@ -6844,12 +8051,22 @@ function registerPaymentsSyncCommand(paymentsCmd2) {
6844
8051
  console.log("No configured Stripe environments to sync.");
6845
8052
  } else {
6846
8053
  outputTable(
6847
- ["Env", "Status", "Products", "Prices", "Subscriptions", "Unmapped", "Synced At"],
8054
+ [
8055
+ "Env",
8056
+ "Status",
8057
+ "Products",
8058
+ "Prices",
8059
+ "Customers",
8060
+ "Subscriptions",
8061
+ "Unmapped",
8062
+ "Synced At"
8063
+ ],
6848
8064
  data.results.map((result) => [
6849
8065
  result.environment,
6850
8066
  result.connection.lastSyncStatus ?? result.connection.status,
6851
8067
  String(result.connection.lastSyncCounts.products ?? 0),
6852
8068
  String(result.connection.lastSyncCounts.prices ?? 0),
8069
+ String(result.connection.lastSyncCounts.customers ?? 0),
6853
8070
  String(result.subscriptions?.synced ?? 0),
6854
8071
  String(result.subscriptions?.unmapped ?? 0),
6855
8072
  formatDate(result.connection.lastSyncedAt)
@@ -6859,7 +8076,9 @@ function registerPaymentsSyncCommand(paymentsCmd2) {
6859
8076
  }
6860
8077
  await trackPaymentUsage("sync", true, { environment });
6861
8078
  } catch (err) {
6862
- await trackPaymentUsage("sync", false, { environment: opts.environment });
8079
+ await trackPaymentUsage("sync", false, {
8080
+ environment: opts.environment
8081
+ });
6863
8082
  handleError(err, json);
6864
8083
  }
6865
8084
  });
@@ -6904,6 +8123,7 @@ function registerPaymentsCommands(paymentsCmd2) {
6904
8123
  registerPaymentsSyncCommand(paymentsCmd2);
6905
8124
  registerPaymentsWebhooksCommand(paymentsCmd2);
6906
8125
  registerPaymentsCatalogCommand(paymentsCmd2);
8126
+ registerPaymentsCustomersCommand(paymentsCmd2);
6907
8127
  registerPaymentsProductsCommand(paymentsCmd2);
6908
8128
  registerPaymentsPricesCommand(paymentsCmd2);
6909
8129
  registerPaymentsSubscriptionsCommand(paymentsCmd2);
@@ -6911,8 +8131,8 @@ function registerPaymentsCommands(paymentsCmd2) {
6911
8131
  }
6912
8132
 
6913
8133
  // src/index.ts
6914
- var __dirname = dirname(fileURLToPath(import.meta.url));
6915
- 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"));
6916
8136
  var INSFORGE_LOGO = `
6917
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
6918
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
@@ -6936,6 +8156,7 @@ var orgsCmd = program.command("orgs", { hidden: true }).description("Manage orga
6936
8156
  registerOrgsCommands(orgsCmd);
6937
8157
  var projectsCmd = program.command("projects", { hidden: true }).description("Manage projects");
6938
8158
  registerProjectsCommands(projectsCmd);
8159
+ registerBranchCommands(program);
6939
8160
  var dbCmd = program.command("db").description("Database operations");
6940
8161
  registerDbCommands(dbCmd);
6941
8162
  registerDbTablesCommand(dbCmd);
@@ -6991,7 +8212,7 @@ registerComputeUpdateCommand(computeCmd);
6991
8212
  registerComputeDeleteCommand(computeCmd);
6992
8213
  registerComputeStartCommand(computeCmd);
6993
8214
  registerComputeStopCommand(computeCmd);
6994
- registerComputeLogsCommand(computeCmd);
8215
+ registerComputeEventsCommand(computeCmd);
6995
8216
  var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
6996
8217
  registerSchedulesListCommand(schedulesCmd);
6997
8218
  registerSchedulesGetCommand(schedulesCmd);
@@ -7016,7 +8237,7 @@ async function showInteractiveMenu() {
7016
8237
  } catch {
7017
8238
  }
7018
8239
  console.log(INSFORGE_LOGO);
7019
- clack11.intro(`InsForge CLI v${pkg.version}`);
8240
+ clack15.intro(`InsForge CLI v${pkg.version}`);
7020
8241
  const options = [];
7021
8242
  if (!isLoggedIn) {
7022
8243
  options.push({ value: "login", label: "Log in to InsForge" });
@@ -7037,7 +8258,7 @@ async function showInteractiveMenu() {
7037
8258
  options
7038
8259
  });
7039
8260
  if (isCancel2(action)) {
7040
- clack11.cancel("Bye!");
8261
+ clack15.cancel("Bye!");
7041
8262
  process.exit(0);
7042
8263
  }
7043
8264
  switch (action) {