@insforge/cli 0.1.73 → 0.1.76

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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync11 } from "fs";
4
+ import { readFileSync as readFileSync13 } from "fs";
5
5
  import { join as join17, dirname as dirname3 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { Command } from "commander";
@@ -41,8 +41,8 @@ var LineReader = class {
41
41
  this.output.write(prompt);
42
42
  if (this.queue.length > 0) return this.queue.shift();
43
43
  if (this.closed) return null;
44
- return new Promise((resolve5) => {
45
- this.waiter = resolve5;
44
+ return new Promise((resolve8) => {
45
+ this.waiter = resolve8;
46
46
  });
47
47
  }
48
48
  close() {
@@ -424,8 +424,8 @@ function startCallbackServer() {
424
424
  return new Promise((resolveServer) => {
425
425
  let resolveResult;
426
426
  let rejectResult;
427
- const resultPromise = new Promise((resolve5, reject) => {
428
- resolveResult = resolve5;
427
+ const resultPromise = new Promise((resolve8, reject) => {
428
+ resolveResult = resolve8;
429
429
  rejectResult = reject;
430
430
  });
431
431
  const server = createServer((req, res) => {
@@ -1158,7 +1158,7 @@ function registerProjectsCommands(projectsCmd2) {
1158
1158
  }
1159
1159
  outputTable(
1160
1160
  ["ID", "Name", "Region", "Status", "AppKey"],
1161
- projects.map((p) => [p.id, p.name, p.region, p.status, p.appkey])
1161
+ projects.map((p3) => [p3.id, p3.name, p3.region, p3.status, p3.appkey])
1162
1162
  );
1163
1163
  }
1164
1164
  } catch (err) {
@@ -1751,10 +1751,14 @@ ${missing.join("\n")}
1751
1751
  `;
1752
1752
  appendFileSync(gitignorePath, block);
1753
1753
  }
1754
- async function installSkills(json) {
1754
+ var AGENT_FLAGS = "-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";
1755
+ var PROVIDER_SKILLS = {
1756
+ "better-auth": { repo: "better-auth/skills", label: "Better Auth skills" }
1757
+ };
1758
+ async function installSkills(json, authProvider) {
1755
1759
  try {
1756
1760
  if (!json) clack9.log.info("Installing InsForge agent skills (global)...");
1757
- 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", {
1761
+ await execAsync(`npx skills add insforge/agent-skills -g -y ${AGENT_FLAGS}`, {
1758
1762
  cwd: process.cwd(),
1759
1763
  timeout: SKILL_INSTALL_TIMEOUT_MS
1760
1764
  });
@@ -1778,6 +1782,22 @@ async function installSkills(json) {
1778
1782
  clack9.log.info("Run `npx skills add https://github.com/vercel-labs/skills --skill find-skills` once resolved.");
1779
1783
  }
1780
1784
  }
1785
+ const providerEntry = authProvider ? PROVIDER_SKILLS[authProvider] : void 0;
1786
+ if (providerEntry) {
1787
+ try {
1788
+ if (!json) clack9.log.info(`Installing ${providerEntry.label} (global)...`);
1789
+ await execAsync(`npx skills add ${providerEntry.repo} -g -y ${AGENT_FLAGS}`, {
1790
+ cwd: process.cwd(),
1791
+ timeout: SKILL_INSTALL_TIMEOUT_MS
1792
+ });
1793
+ if (!json) clack9.log.success(`${providerEntry.label} installed.`);
1794
+ } catch (err) {
1795
+ if (!json) {
1796
+ clack9.log.warn(`Could not install ${providerEntry.label}: ${describeExecError(err)}`);
1797
+ clack9.log.info(`Run \`npx skills add ${providerEntry.repo}\` once resolved to see the full output.`);
1798
+ }
1799
+ }
1800
+ }
1781
1801
  try {
1782
1802
  updateGitignore();
1783
1803
  } catch {
@@ -1868,18 +1888,34 @@ async function getJwtSecret() {
1868
1888
  function spliceDatabasePassword(maskedUrl, password3) {
1869
1889
  return maskedUrl.replace(/^(postgresql:\/\/[^:]+:)[^@]+(@)/, `$1${password3}$2`);
1870
1890
  }
1891
+ function isMaskedDatabasePassword(value) {
1892
+ return /^\*+$/.test(value);
1893
+ }
1894
+ async function fetchDatabasePasswordOnce() {
1895
+ try {
1896
+ const res = await ossFetch("/api/metadata/database-password");
1897
+ const body = await res.json();
1898
+ const pw = body.databasePassword;
1899
+ if (typeof pw !== "string" || !pw || isMaskedDatabasePassword(pw)) return null;
1900
+ return pw;
1901
+ } catch {
1902
+ return null;
1903
+ }
1904
+ }
1871
1905
  async function getDatabaseConnectionString() {
1872
1906
  try {
1873
- const [urlRes, pwRes] = await Promise.all([
1874
- ossFetch("/api/metadata/database-connection-string"),
1875
- ossFetch("/api/metadata/database-password")
1876
- ]);
1907
+ const urlRes = await ossFetch("/api/metadata/database-connection-string");
1877
1908
  const urlBody = await urlRes.json();
1878
- const pwBody = await pwRes.json();
1879
1909
  const masked = urlBody.connectionURL;
1880
- const password3 = pwBody.databasePassword;
1881
1910
  if (typeof masked !== "string" || !masked) return null;
1882
- if (typeof password3 !== "string" || !password3) return null;
1911
+ let password3 = await fetchDatabasePasswordOnce();
1912
+ const POLL_ATTEMPTS = 9;
1913
+ const POLL_DELAY_MS = 2e3;
1914
+ for (let attempt = 0; password3 === null && attempt < POLL_ATTEMPTS; attempt++) {
1915
+ await new Promise((r) => setTimeout(r, POLL_DELAY_MS));
1916
+ password3 = await fetchDatabasePasswordOnce();
1917
+ }
1918
+ if (password3 === null) return null;
1883
1919
  return spliceDatabasePassword(masked, password3);
1884
1920
  } catch {
1885
1921
  return null;
@@ -1918,8 +1954,8 @@ ${err.nextActions}`;
1918
1954
  // src/auth-providers/apply.ts
1919
1955
  var execFileAsync = promisify2(execFile);
1920
1956
  var VALID_AUTH_PROVIDERS = ["better-auth"];
1921
- function pathExists(p) {
1922
- return fs.stat(p).then(() => true, () => false);
1957
+ function pathExists(p3) {
1958
+ return fs.stat(p3).then(() => true, () => false);
1923
1959
  }
1924
1960
  function deepMergeKeepBase(base, patch) {
1925
1961
  const out = { ...base };
@@ -2310,11 +2346,11 @@ async function collectDeploymentFiles(sourceDir) {
2310
2346
  return files;
2311
2347
  }
2312
2348
  async function createZipBuffer(sourceDir) {
2313
- return new Promise((resolve5, reject) => {
2349
+ return new Promise((resolve8, reject) => {
2314
2350
  const archive = archiver("zip", { zlib: { level: 9 } });
2315
2351
  const chunks = [];
2316
2352
  archive.on("data", (chunk) => chunks.push(chunk));
2317
- archive.on("end", () => resolve5(Buffer.concat(chunks)));
2353
+ archive.on("end", () => resolve8(Buffer.concat(chunks)));
2318
2354
  archive.on("error", (err) => reject(err));
2319
2355
  archive.directory(sourceDir, false, (entry) => {
2320
2356
  if (shouldExclude(entry.name)) return false;
@@ -2396,7 +2432,7 @@ async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
2396
2432
  const startTime = Date.now();
2397
2433
  let deployment = null;
2398
2434
  while (Date.now() - startTime < POLL_TIMEOUT_MS3) {
2399
- await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS3));
2435
+ await new Promise((resolve8) => setTimeout(resolve8, POLL_INTERVAL_MS3));
2400
2436
  try {
2401
2437
  if (syncBeforeRead) {
2402
2438
  await ossFetch(`/api/deployments/${deploymentId}/sync`, { method: "POST" });
@@ -2877,7 +2913,7 @@ function registerCreateCommand(program2) {
2877
2913
  else clack12.log.warn(msg);
2878
2914
  }
2879
2915
  }
2880
- await installSkills(json);
2916
+ await installSkills(json, opts.auth);
2881
2917
  trackCommand("create", orgId);
2882
2918
  await reportCliUsage("cli.create", true, 6);
2883
2919
  const templateDownloaded = hasTemplate ? await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null) : null;
@@ -2962,7 +2998,7 @@ function registerCreateCommand(program2) {
2962
2998
  clack12.note(
2963
2999
  `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
2964
3000
 
2965
- ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
3001
+ ${prompts.map((p3) => `\u2022 "${p3}"`).join("\n")}`,
2966
3002
  "Start building"
2967
3003
  );
2968
3004
  }
@@ -3147,7 +3183,7 @@ async function runNpmSetupIfPresent() {
3147
3183
  }
3148
3184
  }
3149
3185
  function registerProjectLinkCommand(program2) {
3150
- 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) => {
3186
+ program2.command("link").description("Link current directory to an InsForge project (no args: installs agent skills only)").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) => {
3151
3187
  const { json, apiUrl } = getRootOpts(cmd);
3152
3188
  const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo"];
3153
3189
  if (opts.auth && !VALID_AUTH_PROVIDERS.includes(opts.auth)) {
@@ -3157,6 +3193,28 @@ function registerProjectLinkCommand(program2) {
3157
3193
  if (opts.template && !validTemplates.includes(opts.template)) {
3158
3194
  throw new CLIError(`Invalid template "${opts.template}". Valid options: ${validTemplates.join(", ")}`);
3159
3195
  }
3196
+ const isSkillsOnly = opts.projectId === void 0 && opts.orgId === void 0 && opts.template === void 0 && opts.auth === void 0 && opts.apiBaseUrl === void 0 && opts.apiKey === void 0;
3197
+ if (isSkillsOnly) {
3198
+ try {
3199
+ await installSkills(json);
3200
+ trackCommand("link", "skills-only", { skills_only: true });
3201
+ await reportCliUsage("cli.link_skills_only", true, 1);
3202
+ if (json) {
3203
+ outputJson({ success: true, skills_only: true });
3204
+ } else {
3205
+ clack13.note(
3206
+ `Open your coding agent (Claude Code, Codex, Cursor, etc.) and ask it to build something. It will walk you through provisioning an InsForge project when needed.`,
3207
+ "What's next"
3208
+ );
3209
+ }
3210
+ return;
3211
+ } catch (err) {
3212
+ await reportCliUsage("cli.link_skills_only", false);
3213
+ await shutdownAnalytics();
3214
+ handleError(err, json);
3215
+ return;
3216
+ }
3217
+ }
3160
3218
  if (opts.apiBaseUrl || opts.apiKey) {
3161
3219
  try {
3162
3220
  if (!opts.apiBaseUrl || !opts.apiKey) {
@@ -3235,7 +3293,7 @@ function registerProjectLinkCommand(program2) {
3235
3293
  await runNpmSetupIfPresent();
3236
3294
  }
3237
3295
  }
3238
- await installSkills(json);
3296
+ await installSkills(json, opts.auth);
3239
3297
  trackCommand("link", "oss-org", { direct: true, template: template2 });
3240
3298
  await reportCliUsage("cli.link_direct", true, 6, projectConfig2);
3241
3299
  try {
@@ -3282,7 +3340,7 @@ function registerProjectLinkCommand(program2) {
3282
3340
  }
3283
3341
  }
3284
3342
  trackCommand("link", "oss-org", { direct: true });
3285
- await installSkills(json);
3343
+ await installSkills(json, opts.auth);
3286
3344
  await reportCliUsage("cli.link_direct", true, 6, projectConfig2);
3287
3345
  try {
3288
3346
  const urlMatch = opts.apiBaseUrl.match(/^https?:\/\/([^.]+)\.[^.]+\.insforge\.app/);
@@ -3337,9 +3395,9 @@ function registerProjectLinkCommand(program2) {
3337
3395
  }
3338
3396
  const selected = await select2({
3339
3397
  message: "Select a project to link:",
3340
- options: projects.map((p) => ({
3341
- value: p.id,
3342
- label: `${p.name} (${p.region}, ${p.status})`
3398
+ options: projects.map((p3) => ({
3399
+ value: p3.id,
3400
+ label: `${p3.name} (${p3.region}, ${p3.status})`
3343
3401
  }))
3344
3402
  });
3345
3403
  if (isCancel2(selected)) process.exit(0);
@@ -3432,7 +3490,7 @@ function registerProjectLinkCommand(program2) {
3432
3490
  await runNpmSetupIfPresent();
3433
3491
  }
3434
3492
  }
3435
- await installSkills(json);
3493
+ await installSkills(json, opts.auth);
3436
3494
  await reportCliUsage("cli.link", true, 6, projectConfig);
3437
3495
  if (!json) {
3438
3496
  const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
@@ -3465,7 +3523,7 @@ function registerProjectLinkCommand(program2) {
3465
3523
  else clack13.log.warn(msg);
3466
3524
  }
3467
3525
  }
3468
- await installSkills(json);
3526
+ await installSkills(json, opts.auth);
3469
3527
  await reportCliUsage("cli.link", true, 6, projectConfig);
3470
3528
  if (!json) {
3471
3529
  const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
@@ -3478,7 +3536,7 @@ function registerProjectLinkCommand(program2) {
3478
3536
  clack13.note(
3479
3537
  `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
3480
3538
 
3481
- ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
3539
+ ${prompts.map((p3) => `\u2022 "${p3}"`).join("\n")}`,
3482
3540
  "Start building"
3483
3541
  );
3484
3542
  }
@@ -3635,13 +3693,13 @@ function registerDbPoliciesCommand(dbCmd2) {
3635
3693
  }
3636
3694
  outputTable(
3637
3695
  ["Table", "Policy Name", "Command", "Roles", "Qual", "With Check"],
3638
- policies.map((p) => [
3639
- String(p.tableName ?? "-"),
3640
- String(p.policyName ?? "-"),
3641
- String(p.cmd ?? "-"),
3642
- Array.isArray(p.roles) ? p.roles.join(", ") : String(p.roles ?? "-"),
3643
- String(p.qual ?? "-"),
3644
- String(p.withCheck ?? "-")
3696
+ policies.map((p3) => [
3697
+ String(p3.tableName ?? "-"),
3698
+ String(p3.policyName ?? "-"),
3699
+ String(p3.cmd ?? "-"),
3700
+ Array.isArray(p3.roles) ? p3.roles.join(", ") : String(p3.roles ?? "-"),
3701
+ String(p3.qual ?? "-"),
3702
+ String(p3.withCheck ?? "-")
3645
3703
  ])
3646
3704
  );
3647
3705
  }
@@ -4787,10 +4845,10 @@ function registerStorageDeleteBucketCommand(storageCmd2) {
4787
4845
  try {
4788
4846
  await requireAuth();
4789
4847
  if (!yes && !json) {
4790
- const confirm6 = await confirm2({
4848
+ const confirm8 = await confirm2({
4791
4849
  message: `Delete bucket "${name}" and all its objects? This cannot be undone.`
4792
4850
  });
4793
- if (isCancel2(confirm6) || !confirm6) {
4851
+ if (isCancel2(confirm8) || !confirm8) {
4794
4852
  process.exit(0);
4795
4853
  }
4796
4854
  }
@@ -4937,12 +4995,12 @@ function registerListCommand(program2) {
4937
4995
  id: org.id,
4938
4996
  name: org.name,
4939
4997
  type: org.type ?? null,
4940
- projects: projects.map((p) => ({
4941
- id: p.id,
4942
- name: p.name,
4943
- region: p.region,
4944
- status: p.status,
4945
- appkey: p.appkey
4998
+ projects: projects.map((p3) => ({
4999
+ id: p3.id,
5000
+ name: p3.name,
5001
+ region: p3.region,
5002
+ status: p3.status,
5003
+ appkey: p3.appkey
4946
5004
  }))
4947
5005
  }))
4948
5006
  );
@@ -4954,13 +5012,13 @@ function registerListCommand(program2) {
4954
5012
  rows.push([org.name, "-", "-", "-", "-"]);
4955
5013
  } else {
4956
5014
  for (let i = 0; i < projects.length; i++) {
4957
- const p = projects[i];
5015
+ const p3 = projects[i];
4958
5016
  rows.push([
4959
5017
  i === 0 ? org.name : "",
4960
- p.name,
4961
- p.region,
4962
- p.status,
4963
- p.appkey
5018
+ p3.name,
5019
+ p3.region,
5020
+ p3.status,
5021
+ p3.appkey
4964
5022
  ]);
4965
5023
  }
4966
5024
  }
@@ -5341,10 +5399,10 @@ function registerSecretsDeleteCommand(secretsCmd2) {
5341
5399
  try {
5342
5400
  await requireAuth();
5343
5401
  if (!yes && !json) {
5344
- const confirm6 = await confirm2({
5402
+ const confirm8 = await confirm2({
5345
5403
  message: `Delete secret "${key}"? This cannot be undone.`
5346
5404
  });
5347
- if (isCancel2(confirm6) || !confirm6) {
5405
+ if (isCancel2(confirm8) || !confirm8) {
5348
5406
  process.exit(0);
5349
5407
  }
5350
5408
  }
@@ -5532,10 +5590,10 @@ function registerSchedulesDeleteCommand(schedulesCmd2) {
5532
5590
  try {
5533
5591
  await requireAuth();
5534
5592
  if (!yes && !json) {
5535
- const confirm6 = await confirm2({
5593
+ const confirm8 = await confirm2({
5536
5594
  message: `Delete schedule "${id}"? This cannot be undone.`
5537
5595
  });
5538
- if (isCancel2(confirm6) || !confirm6) {
5596
+ if (isCancel2(confirm8) || !confirm8) {
5539
5597
  process.exit(0);
5540
5598
  }
5541
5599
  }
@@ -5757,6 +5815,8 @@ function registerComputeUpdateCommand(computeCmd2) {
5757
5815
  outputJson(service);
5758
5816
  } else {
5759
5817
  outputSuccess(`Service "${service.name}" updated [${service.status}]`);
5818
+ if (service.endpointUrl) console.log(` Endpoint: ${service.endpointUrl}`);
5819
+ if (service.port !== void 0) console.log(` Port: ${service.port} (container must listen on this port)`);
5760
5820
  }
5761
5821
  await reportCliUsage("cli.compute.update", true);
5762
5822
  } catch (err) {
@@ -5957,7 +6017,7 @@ primary_region = "${opts.region}"
5957
6017
  };
5958
6018
  }
5959
6019
  function flyctlBuildAndPush(opts) {
5960
- return new Promise((resolve5, reject) => {
6020
+ return new Promise((resolve8, reject) => {
5961
6021
  const cleanupStub = ensureFlyTomlStub({
5962
6022
  dir: opts.dir,
5963
6023
  appId: opts.appId,
@@ -6013,7 +6073,7 @@ function flyctlBuildAndPush(opts) {
6013
6073
  )
6014
6074
  );
6015
6075
  }
6016
- resolve5({ imageRef: `registry.fly.io/${opts.appId}@${m[1]}` });
6076
+ resolve8({ imageRef: `registry.fly.io/${opts.appId}@${m[1]}` });
6017
6077
  });
6018
6078
  });
6019
6079
  }
@@ -6112,6 +6172,7 @@ function registerComputeDeployCommand(computeCmd2) {
6112
6172
  const verb = existing2 ? "updated" : "deployed";
6113
6173
  outputSuccess(`Service "${service2.name}" ${verb} [${service2.status}]`);
6114
6174
  if (service2.endpointUrl) console.log(` Endpoint: ${service2.endpointUrl}`);
6175
+ if (service2.port !== void 0) console.log(` Port: ${service2.port} (container must listen on this port)`);
6115
6176
  }
6116
6177
  await reportCliUsage("cli.compute.deploy", true);
6117
6178
  return;
@@ -6207,6 +6268,7 @@ function registerComputeDeployCommand(computeCmd2) {
6207
6268
  const verb = existing ? "updated" : "deployed";
6208
6269
  outputSuccess(`Service "${service.name}" ${verb} [${service.status}]`);
6209
6270
  if (service.endpointUrl) console.log(` Endpoint: ${service.endpointUrl}`);
6271
+ if (service.port !== void 0) console.log(` Port: ${service.port} (container must listen on this port)`);
6210
6272
  console.log(` Image: ${imageRef} (built remotely; no local image to clean up)`);
6211
6273
  }
6212
6274
  await reportCliUsage("cli.compute.deploy", true);
@@ -6896,7 +6958,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
6896
6958
  const s = !json ? clack15.spinner() : null;
6897
6959
  s?.start("Collecting diagnostic data...");
6898
6960
  const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
6899
- const cliVersion = "0.1.73";
6961
+ const cliVersion = "0.1.76";
6900
6962
  s?.stop("Data collected");
6901
6963
  if (!json) {
6902
6964
  console.log(`
@@ -7530,10 +7592,10 @@ function registerPaymentsConfigCommand(paymentsCmd2) {
7530
7592
  throw new CLIError("Use --yes with --json to remove a Stripe key non-interactively.");
7531
7593
  }
7532
7594
  if (!yes) {
7533
- const confirm6 = await confirm2({
7595
+ const confirm8 = await confirm2({
7534
7596
  message: `Remove Stripe ${environment} key? Payment sync and mutations for this environment will stop.`
7535
7597
  });
7536
- if (isCancel2(confirm6) || !confirm6) process.exit(0);
7598
+ if (isCancel2(confirm8) || !confirm8) process.exit(0);
7537
7599
  }
7538
7600
  const data = await removeStripeSecretKey(environment);
7539
7601
  if (json) {
@@ -8056,10 +8118,10 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
8056
8118
  );
8057
8119
  }
8058
8120
  if (!yes) {
8059
- const confirm6 = await confirm2({
8121
+ const confirm8 = await confirm2({
8060
8122
  message: `Delete Stripe ${environment} product "${productId}"?`
8061
8123
  });
8062
- if (isCancel2(confirm6) || !confirm6) process.exit(0);
8124
+ if (isCancel2(confirm8) || !confirm8) process.exit(0);
8063
8125
  }
8064
8126
  const data = await deletePaymentProduct(environment, productId);
8065
8127
  if (json) {
@@ -8410,14 +8472,14 @@ async function startPosthogCliFlow(projectId, jwt, apiUrl) {
8410
8472
  throw new CLIError("PostHog cli-start returned an unexpected response shape.");
8411
8473
  }
8412
8474
  function sleep(ms, signal) {
8413
- return new Promise((resolve5, reject) => {
8475
+ return new Promise((resolve8, reject) => {
8414
8476
  if (signal?.aborted) {
8415
8477
  reject(new CLIError("Connection wait cancelled."));
8416
8478
  return;
8417
8479
  }
8418
8480
  const timer = setTimeout(() => {
8419
8481
  signal?.removeEventListener("abort", onAbort);
8420
- resolve5();
8482
+ resolve8();
8421
8483
  }, ms);
8422
8484
  const onAbort = () => {
8423
8485
  clearTimeout(timer);
@@ -8985,13 +9047,373 @@ function frameworkLabel(framework) {
8985
9047
  return "Astro";
8986
9048
  }
8987
9049
  }
8988
- function relative3(p) {
8989
- return p.replace(process.cwd() + "/", "");
9050
+ function relative3(p3) {
9051
+ return p3.replace(process.cwd() + "/", "");
9052
+ }
9053
+
9054
+ // src/commands/config/export.ts
9055
+ import { writeFileSync as writeFileSync9, existsSync as existsSync14 } from "fs";
9056
+ import { resolve as resolve5 } from "path";
9057
+ import * as p from "@clack/prompts";
9058
+ import pc4 from "picocolors";
9059
+
9060
+ // src/lib/config-toml.ts
9061
+ import * as smolToml from "smol-toml";
9062
+
9063
+ // src/lib/config-schema.ts
9064
+ var ConfigValidationError = class extends Error {
9065
+ constructor(path6, message) {
9066
+ super(`config.${path6}: ${message}`);
9067
+ this.path = path6;
9068
+ this.name = "ConfigValidationError";
9069
+ }
9070
+ };
9071
+ function validateConfig(input) {
9072
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
9073
+ throw new ConfigValidationError("", "must be an object");
9074
+ }
9075
+ const obj = input;
9076
+ const out = {};
9077
+ if ("project_id" in obj) {
9078
+ if (typeof obj.project_id !== "string") {
9079
+ throw new ConfigValidationError("project_id", "must be a string");
9080
+ }
9081
+ out.project_id = obj.project_id;
9082
+ }
9083
+ if ("auth" in obj) out.auth = validateAuth(obj.auth);
9084
+ return out;
9085
+ }
9086
+ function validateAuth(input) {
9087
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
9088
+ throw new ConfigValidationError("auth", "must be an object");
9089
+ }
9090
+ const obj = input;
9091
+ const out = {};
9092
+ if ("allowed_redirect_urls" in obj) {
9093
+ const v = obj.allowed_redirect_urls;
9094
+ if (!Array.isArray(v) || !v.every((u) => typeof u === "string")) {
9095
+ throw new ConfigValidationError(
9096
+ "auth.allowed_redirect_urls",
9097
+ "must be an array of strings"
9098
+ );
9099
+ }
9100
+ out.allowed_redirect_urls = v;
9101
+ }
9102
+ return out;
9103
+ }
9104
+
9105
+ // src/lib/config-toml.ts
9106
+ function parseConfigToml(input) {
9107
+ let parsed;
9108
+ try {
9109
+ parsed = smolToml.parse(input);
9110
+ } catch (err) {
9111
+ throw new Error(`TOML parse error: ${err.message}`, { cause: err });
9112
+ }
9113
+ return validateConfig(parsed);
9114
+ }
9115
+ function stringifyConfigToml(config) {
9116
+ const lines = [];
9117
+ if (config.project_id !== void 0) {
9118
+ lines.push(`project_id = ${JSON.stringify(config.project_id)}`);
9119
+ lines.push("");
9120
+ }
9121
+ if (config.auth) {
9122
+ lines.push("[auth]");
9123
+ if (config.auth.allowed_redirect_urls !== void 0) {
9124
+ const urls = config.auth.allowed_redirect_urls.map((u) => JSON.stringify(u)).join(", ");
9125
+ lines.push(`allowed_redirect_urls = [${urls}]`);
9126
+ }
9127
+ lines.push("");
9128
+ }
9129
+ return lines.join("\n").replace(/\n+$/, "\n");
9130
+ }
9131
+
9132
+ // src/commands/config/export.ts
9133
+ function registerConfigExportCommand(cfg) {
9134
+ cfg.command("export").description("Pull live project config and write insforge.toml").option("--out <path>", "output path", "insforge.toml").option("--force", "overwrite without confirmation").action(async (opts, cmd) => {
9135
+ const { json } = getRootOpts(cmd);
9136
+ try {
9137
+ await requireAuth();
9138
+ const target = resolve5(process.cwd(), opts.out);
9139
+ if (existsSync14(target) && !opts.force) {
9140
+ if (json) {
9141
+ throw new CLIError(
9142
+ `${opts.out} exists. Re-run with --force to overwrite.`,
9143
+ 1,
9144
+ "OUTPUT_EXISTS"
9145
+ );
9146
+ }
9147
+ const ok = await p.confirm({
9148
+ message: `${opts.out} exists. Overwrite?`,
9149
+ initialValue: false
9150
+ });
9151
+ if (!ok || p.isCancel(ok)) {
9152
+ console.log("Aborted.");
9153
+ return;
9154
+ }
9155
+ }
9156
+ const res = await ossFetch("/api/metadata");
9157
+ const raw = await res.json();
9158
+ const config = {};
9159
+ const skipped = [];
9160
+ const authSlice = raw?.auth;
9161
+ if (authSlice && typeof authSlice === "object" && "allowedRedirectUrls" in authSlice) {
9162
+ config.auth = {
9163
+ allowed_redirect_urls: authSlice.allowedRedirectUrls ?? []
9164
+ };
9165
+ } else {
9166
+ skipped.push("auth.allowed_redirect_urls");
9167
+ }
9168
+ const toml = stringifyConfigToml(config);
9169
+ writeFileSync9(target, toml, "utf8");
9170
+ if (json) {
9171
+ console.log(JSON.stringify({ written: target, config, skipped }, null, 2));
9172
+ } else {
9173
+ console.log(`${pc4.green("\u2713")} Wrote ${target}`);
9174
+ if (skipped.length) {
9175
+ console.warn(
9176
+ pc4.yellow(
9177
+ `\u26A0 Skipped ${skipped.length} section(s) not supported by this backend:`
9178
+ ) + "\n" + skipped.map((k) => ` - ${k}`).join("\n")
9179
+ );
9180
+ }
9181
+ }
9182
+ await reportCliUsage("cli.config.export", true);
9183
+ } catch (err) {
9184
+ await reportCliUsage("cli.config.export", false);
9185
+ handleError(err, json);
9186
+ }
9187
+ });
9188
+ }
9189
+
9190
+ // src/commands/config/plan.ts
9191
+ import { readFileSync as readFileSync11 } from "fs";
9192
+ import { resolve as resolve6 } from "path";
9193
+ import pc5 from "picocolors";
9194
+
9195
+ // src/lib/config-diff.ts
9196
+ function diffConfig({ live, file }) {
9197
+ const changes = [];
9198
+ const fileAuth = file.auth;
9199
+ const liveAuth = live.auth ?? {};
9200
+ if (fileAuth && "allowed_redirect_urls" in fileAuth) {
9201
+ const fromV = normalizeUrlList(liveAuth.allowed_redirect_urls);
9202
+ const toV = normalizeUrlList(fileAuth.allowed_redirect_urls);
9203
+ if (!arrayEquals(fromV, toV)) {
9204
+ changes.push({
9205
+ section: "auth",
9206
+ op: "modify",
9207
+ key: "allowed_redirect_urls",
9208
+ from: fromV,
9209
+ to: toV
9210
+ });
9211
+ }
9212
+ }
9213
+ return { changes, summary: summarize(changes) };
9214
+ }
9215
+ function summarize(changes) {
9216
+ const s = { add: 0, modify: 0, remove: 0, kept: 0 };
9217
+ for (const c of changes) {
9218
+ if (c.op === "modify") s.modify++;
9219
+ }
9220
+ return s;
9221
+ }
9222
+ function normalizeUrlList(input) {
9223
+ return Array.from(new Set(input ?? [])).sort();
9224
+ }
9225
+ function arrayEquals(a, b) {
9226
+ if (a.length !== b.length) return false;
9227
+ return a.every((v, i) => v === b[i]);
9228
+ }
9229
+
9230
+ // src/lib/config-format.ts
9231
+ function formatPlan(result) {
9232
+ if (result.changes.length === 0) {
9233
+ return "No changes. Live state matches insforge.toml.";
9234
+ }
9235
+ const bySection = /* @__PURE__ */ new Map();
9236
+ for (const c of result.changes) {
9237
+ const arr = bySection.get(c.section) ?? [];
9238
+ arr.push(c);
9239
+ bySection.set(c.section, arr);
9240
+ }
9241
+ const lines = [];
9242
+ for (const [section, changes] of bySection) {
9243
+ lines.push(` ${section}:`);
9244
+ for (const c of changes) {
9245
+ lines.push(` ${formatChange(c)}`);
9246
+ }
9247
+ lines.push("");
9248
+ }
9249
+ const s = result.summary;
9250
+ lines.push(
9251
+ `${s.add} add, ${s.modify} modify, ${s.remove} remove, ${s.kept} untracked kept.`
9252
+ );
9253
+ return lines.join("\n");
9254
+ }
9255
+ function formatChange(c) {
9256
+ return `~ ${c.key}: ${JSON.stringify(c.from)} \u2192 ${JSON.stringify(c.to)}`;
9257
+ }
9258
+
9259
+ // src/lib/config-capabilities.ts
9260
+ function metadataSupports(raw, change) {
9261
+ if (change.section === "auth" && change.key === "allowed_redirect_urls") {
9262
+ return raw?.auth !== void 0 && raw.auth !== null && typeof raw.auth === "object" && "allowedRedirectUrls" in raw.auth;
9263
+ }
9264
+ return false;
9265
+ }
9266
+ function changePath(change) {
9267
+ return `${change.section}.${change.key}`;
9268
+ }
9269
+
9270
+ // src/commands/config/plan.ts
9271
+ function registerConfigPlanCommand(cfg) {
9272
+ cfg.command("plan").description("Show diff between insforge.toml and live project state").option("--file <path>", "path to insforge.toml", "insforge.toml").action(async (opts, cmd) => {
9273
+ const { json } = getRootOpts(cmd);
9274
+ try {
9275
+ await requireAuth();
9276
+ const tomlPath = resolve6(process.cwd(), opts.file);
9277
+ const tomlSource = readFileSync11(tomlPath, "utf8");
9278
+ const file = parseConfigToml(tomlSource);
9279
+ const res = await ossFetch("/api/metadata");
9280
+ const raw = await res.json();
9281
+ const live = {
9282
+ auth: { allowed_redirect_urls: raw.auth?.allowedRedirectUrls ?? [] }
9283
+ };
9284
+ const result = diffConfig({ live, file });
9285
+ const skipped = result.changes.filter((c) => !metadataSupports(raw, c)).map((c) => changePath(c));
9286
+ if (json) {
9287
+ console.log(JSON.stringify({ ...result, skipped }, null, 2));
9288
+ } else {
9289
+ console.log(`Plan for insforge.toml (file: ${opts.file}):
9290
+ `);
9291
+ console.log(formatPlan(result));
9292
+ if (skipped.length) {
9293
+ console.warn(
9294
+ "\n" + pc5.yellow(`\u26A0 Apply will skip ${skipped.length} section(s) \u2014 backend doesn't support them yet:`) + "\n" + skipped.map((k) => ` - ${k}`).join("\n")
9295
+ );
9296
+ }
9297
+ }
9298
+ await reportCliUsage("cli.config.plan", true);
9299
+ } catch (err) {
9300
+ await reportCliUsage("cli.config.plan", false);
9301
+ handleError(err, json);
9302
+ }
9303
+ });
9304
+ }
9305
+
9306
+ // src/commands/config/apply.ts
9307
+ import { readFileSync as readFileSync12 } from "fs";
9308
+ import { resolve as resolve7 } from "path";
9309
+ import * as p2 from "@clack/prompts";
9310
+ import pc6 from "picocolors";
9311
+ function registerConfigApplyCommand(cfg) {
9312
+ cfg.command("apply").description("Apply insforge.toml to the live project").option("--file <path>", "path to insforge.toml", "insforge.toml").option("--dry-run", "show plan, do not apply").option("--auto-approve", "skip confirmation prompt").action(async (opts, cmd) => {
9313
+ const { json, yes } = getRootOpts(cmd);
9314
+ try {
9315
+ await requireAuth();
9316
+ const tomlPath = resolve7(process.cwd(), opts.file);
9317
+ const tomlSource = readFileSync12(tomlPath, "utf8");
9318
+ const file = parseConfigToml(tomlSource);
9319
+ const res = await ossFetch("/api/metadata");
9320
+ const raw = await res.json();
9321
+ const live = {
9322
+ auth: { allowed_redirect_urls: raw.auth?.allowedRedirectUrls ?? [] }
9323
+ };
9324
+ const result = diffConfig({ live, file });
9325
+ const approved = opts.autoApprove || yes;
9326
+ if (!json) {
9327
+ console.log(formatPlan(result));
9328
+ }
9329
+ if (result.changes.length === 0 || opts.dryRun) {
9330
+ if (json) {
9331
+ console.log(
9332
+ JSON.stringify({ plan: result, applied: false, dryRun: !!opts.dryRun }, null, 2)
9333
+ );
9334
+ }
9335
+ await reportCliUsage("cli.config.apply", true);
9336
+ return;
9337
+ }
9338
+ if (!approved) {
9339
+ if (json) {
9340
+ throw new CLIError(
9341
+ "Refusing to apply in --json mode without --auto-approve or --yes.",
9342
+ 1,
9343
+ "CONFIRMATION_REQUIRED"
9344
+ );
9345
+ }
9346
+ const ok = await p2.confirm({
9347
+ message: "Apply these changes?",
9348
+ initialValue: false
9349
+ });
9350
+ if (!ok || p2.isCancel(ok)) {
9351
+ console.log("Aborted.");
9352
+ await reportCliUsage("cli.config.apply", true);
9353
+ return;
9354
+ }
9355
+ }
9356
+ const applied = [];
9357
+ const skipped = [];
9358
+ for (const change of result.changes) {
9359
+ const path6 = changePath(change);
9360
+ if (!metadataSupports(raw, change)) {
9361
+ skipped.push({
9362
+ key: path6,
9363
+ reason: `your backend doesn't expose ${path6} \u2014 upgrade the project to apply this section`
9364
+ });
9365
+ continue;
9366
+ }
9367
+ await applyChange(change);
9368
+ applied.push(change);
9369
+ }
9370
+ if (json) {
9371
+ console.log(
9372
+ JSON.stringify({ plan: result, applied, skipped }, null, 2)
9373
+ );
9374
+ } else {
9375
+ if (skipped.length) {
9376
+ console.warn(
9377
+ pc6.yellow(`\u26A0 Skipped ${skipped.length} section(s):`) + "\n" + skipped.map((s) => ` - ${s.key}: ${s.reason}`).join("\n")
9378
+ );
9379
+ }
9380
+ if (applied.length) {
9381
+ console.log(
9382
+ `${pc6.green("\u2713")} Applied ${applied.length} of ${result.changes.length} change(s).`
9383
+ );
9384
+ } else {
9385
+ console.log("Nothing applied.");
9386
+ }
9387
+ }
9388
+ await reportCliUsage("cli.config.apply", true);
9389
+ } catch (err) {
9390
+ await reportCliUsage("cli.config.apply", false);
9391
+ handleError(err, json);
9392
+ }
9393
+ });
9394
+ }
9395
+ async function applyChange(change) {
9396
+ if (change.section === "auth" && change.key === "allowed_redirect_urls") {
9397
+ await ossFetch("/api/auth/config", {
9398
+ method: "PUT",
9399
+ body: JSON.stringify({ allowedRedirectUrls: change.to })
9400
+ });
9401
+ return;
9402
+ }
9403
+ throw new Error(`Unsupported change type: ${change.section}.${change.key}`);
9404
+ }
9405
+
9406
+ // src/commands/config/index.ts
9407
+ function registerConfigCommand(program2) {
9408
+ const cfg = program2.command("config").description("Manage insforge.toml (declarative project configuration)");
9409
+ registerConfigExportCommand(cfg);
9410
+ registerConfigPlanCommand(cfg);
9411
+ registerConfigApplyCommand(cfg);
8990
9412
  }
8991
9413
 
8992
9414
  // src/index.ts
8993
9415
  var __dirname = dirname3(fileURLToPath(import.meta.url));
8994
- var pkg = JSON.parse(readFileSync11(join17(__dirname, "../package.json"), "utf-8"));
9416
+ var pkg = JSON.parse(readFileSync13(join17(__dirname, "../package.json"), "utf-8"));
8995
9417
  var INSFORGE_LOGO = `
8996
9418
  \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
8997
9419
  \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
@@ -9082,6 +9504,7 @@ registerSchedulesCreateCommand(schedulesCmd);
9082
9504
  registerSchedulesUpdateCommand(schedulesCmd);
9083
9505
  registerSchedulesDeleteCommand(schedulesCmd);
9084
9506
  registerSchedulesLogsCommand(schedulesCmd);
9507
+ registerConfigCommand(program);
9085
9508
  if (process.argv.length <= 2 && process.stdout.isTTY) {
9086
9509
  await showInteractiveMenu();
9087
9510
  } else {