@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.
- package/dist/cli.js +196 -4
- 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
|
|
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
|
|
7253
|
+
const { confirmed } = await inquirer6.prompt([
|
|
7062
7254
|
{
|
|
7063
7255
|
type: "confirm",
|
|
7064
7256
|
name: "confirmed",
|