@insforge/cli 0.1.74 → 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) {
@@ -1888,18 +1888,34 @@ async function getJwtSecret() {
1888
1888
  function spliceDatabasePassword(maskedUrl, password3) {
1889
1889
  return maskedUrl.replace(/^(postgresql:\/\/[^:]+:)[^@]+(@)/, `$1${password3}$2`);
1890
1890
  }
1891
+ function isMaskedDatabasePassword(value) {
1892
+ return /^\*+$/.test(value);
1893
+ }
1894
+ async function fetchDatabasePasswordOnce() {
1895
+ try {
1896
+ const res = await ossFetch("/api/metadata/database-password");
1897
+ const body = await res.json();
1898
+ const pw = body.databasePassword;
1899
+ if (typeof pw !== "string" || !pw || isMaskedDatabasePassword(pw)) return null;
1900
+ return pw;
1901
+ } catch {
1902
+ return null;
1903
+ }
1904
+ }
1891
1905
  async function getDatabaseConnectionString() {
1892
1906
  try {
1893
- const [urlRes, pwRes] = await Promise.all([
1894
- ossFetch("/api/metadata/database-connection-string"),
1895
- ossFetch("/api/metadata/database-password")
1896
- ]);
1907
+ const urlRes = await ossFetch("/api/metadata/database-connection-string");
1897
1908
  const urlBody = await urlRes.json();
1898
- const pwBody = await pwRes.json();
1899
1909
  const masked = urlBody.connectionURL;
1900
- const password3 = pwBody.databasePassword;
1901
1910
  if (typeof masked !== "string" || !masked) return null;
1902
- if (typeof password3 !== "string" || !password3) return null;
1911
+ let password3 = await fetchDatabasePasswordOnce();
1912
+ const POLL_ATTEMPTS = 9;
1913
+ const POLL_DELAY_MS = 2e3;
1914
+ for (let attempt = 0; password3 === null && attempt < POLL_ATTEMPTS; attempt++) {
1915
+ await new Promise((r) => setTimeout(r, POLL_DELAY_MS));
1916
+ password3 = await fetchDatabasePasswordOnce();
1917
+ }
1918
+ if (password3 === null) return null;
1903
1919
  return spliceDatabasePassword(masked, password3);
1904
1920
  } catch {
1905
1921
  return null;
@@ -1938,8 +1954,8 @@ ${err.nextActions}`;
1938
1954
  // src/auth-providers/apply.ts
1939
1955
  var execFileAsync = promisify2(execFile);
1940
1956
  var VALID_AUTH_PROVIDERS = ["better-auth"];
1941
- function pathExists(p) {
1942
- return fs.stat(p).then(() => true, () => false);
1957
+ function pathExists(p3) {
1958
+ return fs.stat(p3).then(() => true, () => false);
1943
1959
  }
1944
1960
  function deepMergeKeepBase(base, patch) {
1945
1961
  const out = { ...base };
@@ -2330,11 +2346,11 @@ async function collectDeploymentFiles(sourceDir) {
2330
2346
  return files;
2331
2347
  }
2332
2348
  async function createZipBuffer(sourceDir) {
2333
- return new Promise((resolve5, reject) => {
2349
+ return new Promise((resolve8, reject) => {
2334
2350
  const archive = archiver("zip", { zlib: { level: 9 } });
2335
2351
  const chunks = [];
2336
2352
  archive.on("data", (chunk) => chunks.push(chunk));
2337
- archive.on("end", () => resolve5(Buffer.concat(chunks)));
2353
+ archive.on("end", () => resolve8(Buffer.concat(chunks)));
2338
2354
  archive.on("error", (err) => reject(err));
2339
2355
  archive.directory(sourceDir, false, (entry) => {
2340
2356
  if (shouldExclude(entry.name)) return false;
@@ -2416,7 +2432,7 @@ async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
2416
2432
  const startTime = Date.now();
2417
2433
  let deployment = null;
2418
2434
  while (Date.now() - startTime < POLL_TIMEOUT_MS3) {
2419
- await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS3));
2435
+ await new Promise((resolve8) => setTimeout(resolve8, POLL_INTERVAL_MS3));
2420
2436
  try {
2421
2437
  if (syncBeforeRead) {
2422
2438
  await ossFetch(`/api/deployments/${deploymentId}/sync`, { method: "POST" });
@@ -2982,7 +2998,7 @@ function registerCreateCommand(program2) {
2982
2998
  clack12.note(
2983
2999
  `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
2984
3000
 
2985
- ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
3001
+ ${prompts.map((p3) => `\u2022 "${p3}"`).join("\n")}`,
2986
3002
  "Start building"
2987
3003
  );
2988
3004
  }
@@ -3379,9 +3395,9 @@ function registerProjectLinkCommand(program2) {
3379
3395
  }
3380
3396
  const selected = await select2({
3381
3397
  message: "Select a project to link:",
3382
- options: projects.map((p) => ({
3383
- value: p.id,
3384
- label: `${p.name} (${p.region}, ${p.status})`
3398
+ options: projects.map((p3) => ({
3399
+ value: p3.id,
3400
+ label: `${p3.name} (${p3.region}, ${p3.status})`
3385
3401
  }))
3386
3402
  });
3387
3403
  if (isCancel2(selected)) process.exit(0);
@@ -3520,7 +3536,7 @@ function registerProjectLinkCommand(program2) {
3520
3536
  clack13.note(
3521
3537
  `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
3522
3538
 
3523
- ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
3539
+ ${prompts.map((p3) => `\u2022 "${p3}"`).join("\n")}`,
3524
3540
  "Start building"
3525
3541
  );
3526
3542
  }
@@ -3677,13 +3693,13 @@ function registerDbPoliciesCommand(dbCmd2) {
3677
3693
  }
3678
3694
  outputTable(
3679
3695
  ["Table", "Policy Name", "Command", "Roles", "Qual", "With Check"],
3680
- policies.map((p) => [
3681
- String(p.tableName ?? "-"),
3682
- String(p.policyName ?? "-"),
3683
- String(p.cmd ?? "-"),
3684
- Array.isArray(p.roles) ? p.roles.join(", ") : String(p.roles ?? "-"),
3685
- String(p.qual ?? "-"),
3686
- String(p.withCheck ?? "-")
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 ?? "-")
3687
3703
  ])
3688
3704
  );
3689
3705
  }
@@ -4829,10 +4845,10 @@ function registerStorageDeleteBucketCommand(storageCmd2) {
4829
4845
  try {
4830
4846
  await requireAuth();
4831
4847
  if (!yes && !json) {
4832
- const confirm6 = await confirm2({
4848
+ const confirm8 = await confirm2({
4833
4849
  message: `Delete bucket "${name}" and all its objects? This cannot be undone.`
4834
4850
  });
4835
- if (isCancel2(confirm6) || !confirm6) {
4851
+ if (isCancel2(confirm8) || !confirm8) {
4836
4852
  process.exit(0);
4837
4853
  }
4838
4854
  }
@@ -4979,12 +4995,12 @@ function registerListCommand(program2) {
4979
4995
  id: org.id,
4980
4996
  name: org.name,
4981
4997
  type: org.type ?? null,
4982
- projects: projects.map((p) => ({
4983
- id: p.id,
4984
- name: p.name,
4985
- region: p.region,
4986
- status: p.status,
4987
- appkey: p.appkey
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
4988
5004
  }))
4989
5005
  }))
4990
5006
  );
@@ -4996,13 +5012,13 @@ function registerListCommand(program2) {
4996
5012
  rows.push([org.name, "-", "-", "-", "-"]);
4997
5013
  } else {
4998
5014
  for (let i = 0; i < projects.length; i++) {
4999
- const p = projects[i];
5015
+ const p3 = projects[i];
5000
5016
  rows.push([
5001
5017
  i === 0 ? org.name : "",
5002
- p.name,
5003
- p.region,
5004
- p.status,
5005
- p.appkey
5018
+ p3.name,
5019
+ p3.region,
5020
+ p3.status,
5021
+ p3.appkey
5006
5022
  ]);
5007
5023
  }
5008
5024
  }
@@ -5383,10 +5399,10 @@ function registerSecretsDeleteCommand(secretsCmd2) {
5383
5399
  try {
5384
5400
  await requireAuth();
5385
5401
  if (!yes && !json) {
5386
- const confirm6 = await confirm2({
5402
+ const confirm8 = await confirm2({
5387
5403
  message: `Delete secret "${key}"? This cannot be undone.`
5388
5404
  });
5389
- if (isCancel2(confirm6) || !confirm6) {
5405
+ if (isCancel2(confirm8) || !confirm8) {
5390
5406
  process.exit(0);
5391
5407
  }
5392
5408
  }
@@ -5574,10 +5590,10 @@ function registerSchedulesDeleteCommand(schedulesCmd2) {
5574
5590
  try {
5575
5591
  await requireAuth();
5576
5592
  if (!yes && !json) {
5577
- const confirm6 = await confirm2({
5593
+ const confirm8 = await confirm2({
5578
5594
  message: `Delete schedule "${id}"? This cannot be undone.`
5579
5595
  });
5580
- if (isCancel2(confirm6) || !confirm6) {
5596
+ if (isCancel2(confirm8) || !confirm8) {
5581
5597
  process.exit(0);
5582
5598
  }
5583
5599
  }
@@ -5799,6 +5815,8 @@ function registerComputeUpdateCommand(computeCmd2) {
5799
5815
  outputJson(service);
5800
5816
  } else {
5801
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)`);
5802
5820
  }
5803
5821
  await reportCliUsage("cli.compute.update", true);
5804
5822
  } catch (err) {
@@ -5999,7 +6017,7 @@ primary_region = "${opts.region}"
5999
6017
  };
6000
6018
  }
6001
6019
  function flyctlBuildAndPush(opts) {
6002
- return new Promise((resolve5, reject) => {
6020
+ return new Promise((resolve8, reject) => {
6003
6021
  const cleanupStub = ensureFlyTomlStub({
6004
6022
  dir: opts.dir,
6005
6023
  appId: opts.appId,
@@ -6055,7 +6073,7 @@ function flyctlBuildAndPush(opts) {
6055
6073
  )
6056
6074
  );
6057
6075
  }
6058
- resolve5({ imageRef: `registry.fly.io/${opts.appId}@${m[1]}` });
6076
+ resolve8({ imageRef: `registry.fly.io/${opts.appId}@${m[1]}` });
6059
6077
  });
6060
6078
  });
6061
6079
  }
@@ -6154,6 +6172,7 @@ function registerComputeDeployCommand(computeCmd2) {
6154
6172
  const verb = existing2 ? "updated" : "deployed";
6155
6173
  outputSuccess(`Service "${service2.name}" ${verb} [${service2.status}]`);
6156
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)`);
6157
6176
  }
6158
6177
  await reportCliUsage("cli.compute.deploy", true);
6159
6178
  return;
@@ -6249,6 +6268,7 @@ function registerComputeDeployCommand(computeCmd2) {
6249
6268
  const verb = existing ? "updated" : "deployed";
6250
6269
  outputSuccess(`Service "${service.name}" ${verb} [${service.status}]`);
6251
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)`);
6252
6272
  console.log(` Image: ${imageRef} (built remotely; no local image to clean up)`);
6253
6273
  }
6254
6274
  await reportCliUsage("cli.compute.deploy", true);
@@ -6938,7 +6958,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
6938
6958
  const s = !json ? clack15.spinner() : null;
6939
6959
  s?.start("Collecting diagnostic data...");
6940
6960
  const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
6941
- const cliVersion = "0.1.74";
6961
+ const cliVersion = "0.1.76";
6942
6962
  s?.stop("Data collected");
6943
6963
  if (!json) {
6944
6964
  console.log(`
@@ -7572,10 +7592,10 @@ function registerPaymentsConfigCommand(paymentsCmd2) {
7572
7592
  throw new CLIError("Use --yes with --json to remove a Stripe key non-interactively.");
7573
7593
  }
7574
7594
  if (!yes) {
7575
- const confirm6 = await confirm2({
7595
+ const confirm8 = await confirm2({
7576
7596
  message: `Remove Stripe ${environment} key? Payment sync and mutations for this environment will stop.`
7577
7597
  });
7578
- if (isCancel2(confirm6) || !confirm6) process.exit(0);
7598
+ if (isCancel2(confirm8) || !confirm8) process.exit(0);
7579
7599
  }
7580
7600
  const data = await removeStripeSecretKey(environment);
7581
7601
  if (json) {
@@ -8098,10 +8118,10 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
8098
8118
  );
8099
8119
  }
8100
8120
  if (!yes) {
8101
- const confirm6 = await confirm2({
8121
+ const confirm8 = await confirm2({
8102
8122
  message: `Delete Stripe ${environment} product "${productId}"?`
8103
8123
  });
8104
- if (isCancel2(confirm6) || !confirm6) process.exit(0);
8124
+ if (isCancel2(confirm8) || !confirm8) process.exit(0);
8105
8125
  }
8106
8126
  const data = await deletePaymentProduct(environment, productId);
8107
8127
  if (json) {
@@ -8452,14 +8472,14 @@ async function startPosthogCliFlow(projectId, jwt, apiUrl) {
8452
8472
  throw new CLIError("PostHog cli-start returned an unexpected response shape.");
8453
8473
  }
8454
8474
  function sleep(ms, signal) {
8455
- return new Promise((resolve5, reject) => {
8475
+ return new Promise((resolve8, reject) => {
8456
8476
  if (signal?.aborted) {
8457
8477
  reject(new CLIError("Connection wait cancelled."));
8458
8478
  return;
8459
8479
  }
8460
8480
  const timer = setTimeout(() => {
8461
8481
  signal?.removeEventListener("abort", onAbort);
8462
- resolve5();
8482
+ resolve8();
8463
8483
  }, ms);
8464
8484
  const onAbort = () => {
8465
8485
  clearTimeout(timer);
@@ -9027,13 +9047,373 @@ function frameworkLabel(framework) {
9027
9047
  return "Astro";
9028
9048
  }
9029
9049
  }
9030
- function relative3(p) {
9031
- 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);
9032
9412
  }
9033
9413
 
9034
9414
  // src/index.ts
9035
9415
  var __dirname = dirname3(fileURLToPath(import.meta.url));
9036
- var pkg = JSON.parse(readFileSync11(join17(__dirname, "../package.json"), "utf-8"));
9416
+ var pkg = JSON.parse(readFileSync13(join17(__dirname, "../package.json"), "utf-8"));
9037
9417
  var INSFORGE_LOGO = `
9038
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
9039
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
@@ -9124,6 +9504,7 @@ registerSchedulesCreateCommand(schedulesCmd);
9124
9504
  registerSchedulesUpdateCommand(schedulesCmd);
9125
9505
  registerSchedulesDeleteCommand(schedulesCmd);
9126
9506
  registerSchedulesLogsCommand(schedulesCmd);
9507
+ registerConfigCommand(program);
9127
9508
  if (process.argv.length <= 2 && process.stdout.isTTY) {
9128
9509
  await showInteractiveMenu();
9129
9510
  } else {