@eide/foir-cli 0.54.0 → 0.55.0

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.
Files changed (2) hide show
  1. package/dist/cli.js +196 -4
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -305,7 +305,7 @@ async function findAvailablePort(start, end) {
305
305
  for (let port = start; port <= end; port++) {
306
306
  const available = await new Promise((resolve9) => {
307
307
  const server = http.createServer();
308
- server.listen(port, () => {
308
+ server.listen(port, "127.0.0.1", () => {
309
309
  server.close();
310
310
  resolve9(true);
311
311
  });
@@ -365,6 +365,15 @@ async function loginAction(globalOpts) {
365
365
  reject(new Error(`Authentication failed: ${error}`));
366
366
  return;
367
367
  }
368
+ if (!code) {
369
+ res.writeHead(400, { "Content-Type": "text/html" });
370
+ res.end(
371
+ `<html><body style="font-family:system-ui;text-align:center;padding:50px"><h1>Authentication failed</h1><p>No authorization code returned. Please try again.</p></body></html>`
372
+ );
373
+ server.close();
374
+ reject(new Error("No authorization code in callback"));
375
+ return;
376
+ }
368
377
  if (returnedState !== state) {
369
378
  res.writeHead(400, { "Content-Type": "text/html" });
370
379
  res.end(
@@ -389,7 +398,7 @@ async function loginAction(globalOpts) {
389
398
  resolve9(code);
390
399
  }
391
400
  });
392
- server.listen(port);
401
+ server.listen(port, "127.0.0.1");
393
402
  timeoutId = setTimeout(
394
403
  () => {
395
404
  server.close();
@@ -5501,6 +5510,7 @@ Edit the files, then run:
5501
5510
 
5502
5511
  // src/commands/push.ts
5503
5512
  import chalk6 from "chalk";
5513
+ import inquirer5 from "inquirer";
5504
5514
  import { existsSync as existsSync4, readFileSync, writeFileSync as writeFileSync2 } from "fs";
5505
5515
  import { resolve as resolve4 } from "path";
5506
5516
 
@@ -5696,6 +5706,120 @@ async function reconcileConfig(client, configId, manifest, options = {}) {
5696
5706
  }
5697
5707
  return summary;
5698
5708
  }
5709
+ async function planReconcile(client, manifest, options = {}) {
5710
+ const plan = { creates: [], updates: [], deletions: [] };
5711
+ const existingConfig = await client.configs.getConfigByKey(manifest.key);
5712
+ const configId = existingConfig?.id;
5713
+ const classify = (resource, manifestKeys, matchable, owned) => {
5714
+ for (const key of manifestKeys) {
5715
+ if (matchable.has(key)) plan.updates.push({ resource, key });
5716
+ else plan.creates.push({ resource, key });
5717
+ }
5718
+ for (const key of owned) {
5719
+ if (!manifestKeys.has(key)) plan.deletions.push({ resource, key });
5720
+ }
5721
+ };
5722
+ {
5723
+ const existing = await client.models.listModels({ first: 200 });
5724
+ const items = existing.items;
5725
+ const manifestKeys = new Set(
5726
+ (manifest.models ?? []).filter((m) => m.key && m.name).map((m) => m.key)
5727
+ );
5728
+ const owned = configId ? items.filter((m) => m.configId === configId).map((m) => m.key) : [];
5729
+ classify("model", manifestKeys, new Set(items.map((m) => m.key)), new Set(owned));
5730
+ }
5731
+ if (configId) {
5732
+ const [operations, hooks, segments, schedules] = await Promise.all([
5733
+ client.operations.listOperations({ where: { configId: { eq: configId } }, first: 200 }),
5734
+ client.hooks.listHooks({ where: { configId: { eq: configId } }, first: 200 }),
5735
+ client.segments.listSegments({ where: { configId: { eq: configId } }, first: 200 }),
5736
+ client.cronSchedules.listCronSchedules({ where: { configId: { eq: configId } }, first: 200 })
5737
+ ]);
5738
+ const opKeys = new Set(
5739
+ (operations.operations ?? []).map((o) => o.key)
5740
+ );
5741
+ classify(
5742
+ "operation",
5743
+ new Set((manifest.operations ?? []).filter((o) => o.key && o.name).map((o) => o.key)),
5744
+ opKeys,
5745
+ opKeys
5746
+ );
5747
+ const hookKeys = new Set(
5748
+ (hooks.hooks ?? []).map((h) => h.key)
5749
+ );
5750
+ classify(
5751
+ "hook",
5752
+ new Set(
5753
+ (manifest.hooks ?? []).filter((h) => h.event).map((h) => h.key || `${h.event}-${h.operationKey ?? "default"}`)
5754
+ ),
5755
+ hookKeys,
5756
+ hookKeys
5757
+ );
5758
+ const segmentKeys = new Set(
5759
+ (segments.segments ?? []).map((s) => s.key)
5760
+ );
5761
+ classify(
5762
+ "segment",
5763
+ new Set((manifest.segments ?? []).filter((s) => s.key && s.name).map((s) => s.key)),
5764
+ segmentKeys,
5765
+ segmentKeys
5766
+ );
5767
+ const scheduleKeys = new Set(
5768
+ (schedules.schedules ?? []).map((s) => s.key)
5769
+ );
5770
+ classify(
5771
+ "schedule",
5772
+ new Set(
5773
+ (manifest.schedules ?? []).filter((s) => (s.key ?? s.operationKey) && s.cron).map((s) => s.key ?? s.operationKey)
5774
+ ),
5775
+ scheduleKeys,
5776
+ scheduleKeys
5777
+ );
5778
+ } else {
5779
+ for (const o of manifest.operations ?? []) {
5780
+ if (o.key && o.name) plan.creates.push({ resource: "operation", key: o.key });
5781
+ }
5782
+ for (const h of manifest.hooks ?? []) {
5783
+ if (h.event) plan.creates.push({ resource: "hook", key: h.key || `${h.event}-${h.operationKey ?? "default"}` });
5784
+ }
5785
+ for (const s of manifest.segments ?? []) {
5786
+ if (s.key && s.name) plan.creates.push({ resource: "segment", key: s.key });
5787
+ }
5788
+ for (const s of manifest.schedules ?? []) {
5789
+ const key = s.key ?? s.operationKey;
5790
+ if (key && s.cron) plan.creates.push({ resource: "schedule", key });
5791
+ }
5792
+ }
5793
+ if (options.tenantId && options.projectId) {
5794
+ const declaredRps = (manifest.relyingParties ?? []).filter(
5795
+ (rp) => rp.clientId && (rp.kind ?? "customer") === "customer" && rp.customDomains !== void 0
5796
+ );
5797
+ if (declaredRps.length > 0) {
5798
+ const existing = await client.identity.listRelyingParties({
5799
+ principalKind: "customer",
5800
+ tenantId: options.tenantId,
5801
+ projectId: options.projectId
5802
+ });
5803
+ const rpByClientId = new Map(
5804
+ existing.items.map((c) => [c.clientId, c])
5805
+ );
5806
+ for (const rp of declaredRps) {
5807
+ const ex = rpByClientId.get(rp.clientId);
5808
+ if (!ex) continue;
5809
+ const declared = new Set(
5810
+ (rp.customDomains ?? []).map((h) => h.trim().toLowerCase().replace(/\.$/, "")).filter(Boolean)
5811
+ );
5812
+ const domains = await client.identity.listCustomDomains({ relyingPartyId: ex.id });
5813
+ for (const d of domains.items) {
5814
+ if (!declared.has(d.hostname)) {
5815
+ plan.deletions.push({ resource: "custom domain", key: d.hostname });
5816
+ }
5817
+ }
5818
+ }
5819
+ }
5820
+ }
5821
+ return plan;
5822
+ }
5699
5823
  async function reconcileModels(client, configId, models, summary, force, allowLookupRebuild, conflictOut) {
5700
5824
  const existing = await client.models.listModels({ first: 200 });
5701
5825
  const allByKey = new Map(
@@ -6530,6 +6654,46 @@ function syncEnvVar(envPath, key, value) {
6530
6654
  `, "utf-8");
6531
6655
  return "written";
6532
6656
  }
6657
+ function printPlannedChanges(label, changes, color) {
6658
+ if (changes.length === 0) return;
6659
+ console.log(color(` ${label}:`));
6660
+ for (const c of changes) {
6661
+ console.log(` ${color("\u2022")} ${c.resource} ${chalk6.cyan(c.key)}`);
6662
+ }
6663
+ }
6664
+ function printPlan(plan) {
6665
+ if (plan.creates.length + plan.updates.length + plan.deletions.length === 0) {
6666
+ console.log(chalk6.dim(" No resource changes (config blob, settings, and keys still apply on a real push)."));
6667
+ return;
6668
+ }
6669
+ printPlannedChanges("Create", plan.creates, chalk6.green);
6670
+ printPlannedChanges("Update", plan.updates, chalk6.yellow);
6671
+ printPlannedChanges("Delete", plan.deletions, chalk6.red);
6672
+ }
6673
+ async function confirmDeletions(plan) {
6674
+ console.log();
6675
+ console.log(chalk6.red(`\u26A0 This push deletes ${plan.deletions.length} resource${plan.deletions.length === 1 ? "" : "s"}:`));
6676
+ for (const d of plan.deletions) {
6677
+ const note = d.resource === "model" ? chalk6.dim(" (including all its records)") : "";
6678
+ console.log(` ${chalk6.red("\u2022")} ${d.resource} ${chalk6.cyan(d.key)}${note}`);
6679
+ }
6680
+ console.log();
6681
+ if (!process.stdin.isTTY) {
6682
+ console.error(
6683
+ chalk6.red("\u2716 Push aborted \u2014 deletions require confirmation.") + chalk6.dim(" Re-run with --allow-delete to proceed non-interactively, or --dry-run to inspect the plan.")
6684
+ );
6685
+ return false;
6686
+ }
6687
+ const { confirmed } = await inquirer5.prompt([
6688
+ {
6689
+ type: "confirm",
6690
+ name: "confirmed",
6691
+ message: "Proceed with these deletions?",
6692
+ default: false
6693
+ }
6694
+ ]);
6695
+ return confirmed;
6696
+ }
6533
6697
  function printSummary(summary) {
6534
6698
  const lines = [];
6535
6699
  const fmt = (label, c) => {
@@ -6576,6 +6740,14 @@ function registerPushCommand(program2, globalOpts) {
6576
6740
  "--rebuild",
6577
6741
  "Accept lookup renames. A lookup with the same keyBy and a changed `name` override rebuilds its projection rows (old rows are dropped, new rows are re-emitted from records). Without this flag, the server rejects renames with a pointer here. Pure add / remove on lookups does not require this flag.",
6578
6742
  false
6743
+ ).option(
6744
+ "--dry-run",
6745
+ "Print what the push would create / update / delete and exit without changing anything.",
6746
+ false
6747
+ ).option(
6748
+ "--allow-delete",
6749
+ "Skip the interactive confirmation when the push deletes resources (for CI). Without this flag, deletions prompt on a TTY and abort otherwise.",
6750
+ false
6579
6751
  ).option("--env <path>", "Path to .env file (default: .env)").action(
6580
6752
  withErrorHandler(
6581
6753
  globalOpts,
@@ -6615,6 +6787,26 @@ function registerPushCommand(program2, globalOpts) {
6615
6787
  }
6616
6788
  }
6617
6789
  const client = await createPlatformClient(gOpts);
6790
+ if (opts.dryRun || !opts.allowDelete) {
6791
+ const plan = await planReconcile(client, config2, {
6792
+ tenantId: resolved?.project.tenantId,
6793
+ projectId: resolved?.project.id
6794
+ });
6795
+ if (opts.dryRun) {
6796
+ console.log();
6797
+ console.log(chalk6.bold(`Plan for config "${config2.key}" (dry run \u2014 nothing applied):`));
6798
+ printPlan(plan);
6799
+ console.log();
6800
+ return;
6801
+ }
6802
+ if (plan.deletions.length > 0) {
6803
+ const confirmed = await confirmDeletions(plan);
6804
+ if (!confirmed) {
6805
+ console.log(chalk6.dim("Cancelled \u2014 nothing was changed."));
6806
+ process.exit(1);
6807
+ }
6808
+ }
6809
+ }
6618
6810
  console.log(
6619
6811
  chalk6.dim(`Pushing config "${config2.key}" to platform...`)
6620
6812
  );
@@ -7046,7 +7238,7 @@ function appToInput(a) {
7046
7238
 
7047
7239
  // src/commands/remove.ts
7048
7240
  import chalk8 from "chalk";
7049
- import inquirer5 from "inquirer";
7241
+ import inquirer6 from "inquirer";
7050
7242
  function registerRemoveCommand(program2, globalOpts) {
7051
7243
  program2.command("remove <key>").description("Remove a config and all its provisioned resources").option("--force", "Skip confirmation prompt", false).action(
7052
7244
  withErrorHandler(
@@ -7058,7 +7250,7 @@ function registerRemoveCommand(program2, globalOpts) {
7058
7250
  throw new Error(`Config not found: ${key}`);
7059
7251
  }
7060
7252
  if (!opts.force) {
7061
- const { confirmed } = await inquirer5.prompt([
7253
+ const { confirmed } = await inquirer6.prompt([
7062
7254
  {
7063
7255
  type: "confirm",
7064
7256
  name: "confirmed",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eide/foir-cli",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "description": "Universal platform CLI for Foir platform",
5
5
  "type": "module",
6
6
  "publishConfig": {