@eide/foir-cli 0.10.1 → 0.11.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 +328 -46
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -64,6 +64,9 @@ function isTokenExpired(credentials) {
|
|
|
64
64
|
return Date.now() > expiresAt.getTime() - bufferMs;
|
|
65
65
|
}
|
|
66
66
|
var REPO_ROOT_MARKERS = [
|
|
67
|
+
// `.foir` first so a nested sub-project with its own `.foir/project.json`
|
|
68
|
+
// wins over a parent monorepo's `.git` or `foir.config.ts`.
|
|
69
|
+
".foir",
|
|
67
70
|
".git",
|
|
68
71
|
"foir.config.ts",
|
|
69
72
|
".foirrc.ts",
|
|
@@ -1185,7 +1188,8 @@ function createAppsMethods(client) {
|
|
|
1185
1188
|
sourceMappings: params.sourceMappings ?? {},
|
|
1186
1189
|
sinkMappings: params.sinkMappings ?? {},
|
|
1187
1190
|
settings: params.settings,
|
|
1188
|
-
placementFieldChoices: params.placementFieldChoices ?? {}
|
|
1191
|
+
placementFieldChoices: params.placementFieldChoices ?? {},
|
|
1192
|
+
updatePushSnapshot: params.updatePushSnapshot ?? false
|
|
1189
1193
|
})
|
|
1190
1194
|
);
|
|
1191
1195
|
return resp.app;
|
|
@@ -1219,7 +1223,9 @@ function createAppsMethods(client) {
|
|
|
1219
1223
|
name: params.name,
|
|
1220
1224
|
sourceMappings: params.sourceMappings ?? {},
|
|
1221
1225
|
sinkMappings: params.sinkMappings ?? {},
|
|
1222
|
-
placementFieldChoices: params.placementFieldChoices ?? {}
|
|
1226
|
+
placementFieldChoices: params.placementFieldChoices ?? {},
|
|
1227
|
+
updatePushSnapshot: params.updatePushSnapshot ?? false,
|
|
1228
|
+
snapshotOnly: params.snapshotOnly ?? false
|
|
1223
1229
|
})
|
|
1224
1230
|
);
|
|
1225
1231
|
return resp.app;
|
|
@@ -1344,7 +1350,8 @@ function createModelsMethods(client) {
|
|
|
1344
1350
|
name: params.name,
|
|
1345
1351
|
fields: params.fields.map(jsFieldToProto),
|
|
1346
1352
|
config: params.config ? jsConfigToProto(params.config) : void 0,
|
|
1347
|
-
configId: params.configId
|
|
1353
|
+
configId: params.configId,
|
|
1354
|
+
updatePushSnapshot: params.updatePushSnapshot ?? false
|
|
1348
1355
|
})
|
|
1349
1356
|
);
|
|
1350
1357
|
return resp.model ?? null;
|
|
@@ -1357,7 +1364,9 @@ function createModelsMethods(client) {
|
|
|
1357
1364
|
updateFields: params.fields != null,
|
|
1358
1365
|
fields: params.fields?.map(jsFieldToProto) ?? [],
|
|
1359
1366
|
config: params.config ? jsConfigToProto(params.config) : void 0,
|
|
1360
|
-
changeDescription: params.changeDescription
|
|
1367
|
+
changeDescription: params.changeDescription,
|
|
1368
|
+
updatePushSnapshot: params.updatePushSnapshot ?? false,
|
|
1369
|
+
snapshotOnly: params.snapshotOnly ?? false
|
|
1361
1370
|
})
|
|
1362
1371
|
);
|
|
1363
1372
|
return resp.model ?? null;
|
|
@@ -4863,6 +4872,93 @@ import { resolve as resolve4 } from "path";
|
|
|
4863
4872
|
function zeroCounts() {
|
|
4864
4873
|
return { created: 0, updated: 0, deleted: 0 };
|
|
4865
4874
|
}
|
|
4875
|
+
var PushConflictError = class extends Error {
|
|
4876
|
+
conflicts;
|
|
4877
|
+
constructor(conflicts) {
|
|
4878
|
+
super(
|
|
4879
|
+
`Push conflicts with admin-UI changes on ${conflicts.length} field${conflicts.length === 1 ? "" : "s"}. Run \`foir pull\` to sync, or re-run with --force to overwrite.`
|
|
4880
|
+
);
|
|
4881
|
+
this.name = "PushConflictError";
|
|
4882
|
+
this.conflicts = conflicts;
|
|
4883
|
+
}
|
|
4884
|
+
};
|
|
4885
|
+
function canonicalize(v) {
|
|
4886
|
+
if (v === void 0) return "undefined";
|
|
4887
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v);
|
|
4888
|
+
if (Array.isArray(v)) return "[" + v.map(canonicalize).join(",") + "]";
|
|
4889
|
+
const obj = v;
|
|
4890
|
+
const keys = Object.keys(obj).sort();
|
|
4891
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
|
|
4892
|
+
}
|
|
4893
|
+
function deepEqual(a, b) {
|
|
4894
|
+
return canonicalize(a) === canonicalize(b);
|
|
4895
|
+
}
|
|
4896
|
+
function indexByKey(items) {
|
|
4897
|
+
const out = /* @__PURE__ */ new Map();
|
|
4898
|
+
for (const it of items ?? []) {
|
|
4899
|
+
if (it && it.key) out.set(it.key, it);
|
|
4900
|
+
}
|
|
4901
|
+
return out;
|
|
4902
|
+
}
|
|
4903
|
+
function threeWayMergeKeyed(config2, platform, snapshot, parentLabel) {
|
|
4904
|
+
const conflicts = [];
|
|
4905
|
+
const merged = [];
|
|
4906
|
+
const keys = /* @__PURE__ */ new Set([
|
|
4907
|
+
...config2.keys(),
|
|
4908
|
+
...platform.keys(),
|
|
4909
|
+
...snapshot ? snapshot.keys() : []
|
|
4910
|
+
]);
|
|
4911
|
+
const noSnapshot = snapshot === null;
|
|
4912
|
+
for (const key of keys) {
|
|
4913
|
+
const inC = config2.has(key);
|
|
4914
|
+
const inP = platform.has(key);
|
|
4915
|
+
const inS = snapshot ? snapshot.has(key) : false;
|
|
4916
|
+
const c = config2.get(key);
|
|
4917
|
+
const p = platform.get(key);
|
|
4918
|
+
const s = snapshot ? snapshot.get(key) : void 0;
|
|
4919
|
+
if (noSnapshot) {
|
|
4920
|
+
if (inC) merged.push(c);
|
|
4921
|
+
else if (inP) merged.push(p);
|
|
4922
|
+
continue;
|
|
4923
|
+
}
|
|
4924
|
+
if (inC && inP && inS) {
|
|
4925
|
+
if (deepEqual(p, s)) {
|
|
4926
|
+
merged.push(c);
|
|
4927
|
+
} else if (deepEqual(c, s)) {
|
|
4928
|
+
merged.push(p);
|
|
4929
|
+
} else if (deepEqual(c, p)) {
|
|
4930
|
+
merged.push(c);
|
|
4931
|
+
} else {
|
|
4932
|
+
conflicts.push({ fieldKey: key, snapshot: s, platform: p, config: c, kind: "modified-both" });
|
|
4933
|
+
merged.push(p);
|
|
4934
|
+
}
|
|
4935
|
+
} else if (inC && !inS && !inP) {
|
|
4936
|
+
merged.push(c);
|
|
4937
|
+
} else if (inC && inS && !inP) {
|
|
4938
|
+
if (deepEqual(c, s)) {
|
|
4939
|
+
continue;
|
|
4940
|
+
}
|
|
4941
|
+
conflicts.push({ fieldKey: key, snapshot: s, platform: void 0, config: c, kind: "deleted-remotely-modified-locally" });
|
|
4942
|
+
} else if (!inC && inS && inP) {
|
|
4943
|
+
if (deepEqual(p, s)) {
|
|
4944
|
+
continue;
|
|
4945
|
+
}
|
|
4946
|
+
conflicts.push({ fieldKey: key, snapshot: s, platform: p, config: void 0, kind: "deleted-locally-modified-remotely" });
|
|
4947
|
+
merged.push(p);
|
|
4948
|
+
} else if (!inC && !inS && inP) {
|
|
4949
|
+
merged.push(p);
|
|
4950
|
+
} else if (inC && !inS && inP) {
|
|
4951
|
+
if (deepEqual(c, p)) {
|
|
4952
|
+
merged.push(c);
|
|
4953
|
+
} else {
|
|
4954
|
+
conflicts.push({ fieldKey: key, snapshot: void 0, platform: p, config: c, kind: "modified-both" });
|
|
4955
|
+
merged.push(p);
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
void parentLabel;
|
|
4960
|
+
return { merged, conflicts };
|
|
4961
|
+
}
|
|
4866
4962
|
async function reconcileConfig(client, configId, manifest, options = {}) {
|
|
4867
4963
|
const summary = {
|
|
4868
4964
|
models: zeroCounts(),
|
|
@@ -4877,7 +4973,9 @@ async function reconcileConfig(client, configId, manifest, options = {}) {
|
|
|
4877
4973
|
apps: zeroCounts()
|
|
4878
4974
|
};
|
|
4879
4975
|
const operationBaseUrl = manifest.operationBaseUrl ?? "";
|
|
4880
|
-
|
|
4976
|
+
const modelConflicts = [];
|
|
4977
|
+
const mappingConflicts = [];
|
|
4978
|
+
await reconcileModels(client, configId, manifest.models ?? [], summary, options.force ?? false, modelConflicts);
|
|
4881
4979
|
await reconcileOperations(client, configId, manifest.operations ?? [], operationBaseUrl, summary);
|
|
4882
4980
|
await reconcileHooks(client, configId, manifest.hooks ?? [], summary);
|
|
4883
4981
|
await reconcileSegments(client, configId, manifest.segments ?? [], summary);
|
|
@@ -4897,11 +4995,17 @@ async function reconcileConfig(client, configId, manifest, options = {}) {
|
|
|
4897
4995
|
options.tenantId,
|
|
4898
4996
|
options.projectId,
|
|
4899
4997
|
manifest.apps ?? {},
|
|
4900
|
-
summary
|
|
4998
|
+
summary,
|
|
4999
|
+
options.force ?? false,
|
|
5000
|
+
mappingConflicts
|
|
4901
5001
|
);
|
|
5002
|
+
const allConflicts = [...modelConflicts, ...mappingConflicts];
|
|
5003
|
+
if (allConflicts.length > 0 && !options.force) {
|
|
5004
|
+
throw new PushConflictError(allConflicts);
|
|
5005
|
+
}
|
|
4902
5006
|
return summary;
|
|
4903
5007
|
}
|
|
4904
|
-
async function reconcileModels(client, configId, models, summary) {
|
|
5008
|
+
async function reconcileModels(client, configId, models, summary, force, conflictOut) {
|
|
4905
5009
|
const existing = await client.models.listModels({ limit: 200 });
|
|
4906
5010
|
const allByKey = new Map(
|
|
4907
5011
|
existing.items.map((m) => [m.key, m])
|
|
@@ -4910,6 +5014,7 @@ async function reconcileModels(client, configId, models, summary) {
|
|
|
4910
5014
|
existing.items.filter((m) => m.configId === configId).map((m) => [m.key, m])
|
|
4911
5015
|
);
|
|
4912
5016
|
const manifestKeys = /* @__PURE__ */ new Set();
|
|
5017
|
+
const plans = [];
|
|
4913
5018
|
for (const m of models) {
|
|
4914
5019
|
if (!m.key || !m.name) continue;
|
|
4915
5020
|
manifestKeys.add(m.key);
|
|
@@ -4918,30 +5023,77 @@ async function reconcileModels(client, configId, models, summary) {
|
|
|
4918
5023
|
if (m.pluralKey) config2.pluralKey = m.pluralKey;
|
|
4919
5024
|
if (m.description) config2.description = m.description;
|
|
4920
5025
|
const ex = allByKey.get(m.key);
|
|
4921
|
-
if (ex) {
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
5026
|
+
if (!ex) {
|
|
5027
|
+
plans.push({ kind: "create", model: m, config: config2 });
|
|
5028
|
+
continue;
|
|
5029
|
+
}
|
|
5030
|
+
const cfgFields = m.fields ?? [];
|
|
5031
|
+
const platFields = ex.fields ?? [];
|
|
5032
|
+
const snapFields = ex.lastPushedFields;
|
|
5033
|
+
const hasSnapshot = snapFields !== void 0 && snapFields !== null;
|
|
5034
|
+
const { merged, conflicts } = threeWayMergeKeyed(
|
|
5035
|
+
indexByKey(cfgFields),
|
|
5036
|
+
indexByKey(platFields),
|
|
5037
|
+
hasSnapshot ? indexByKey(snapFields) : null,
|
|
5038
|
+
`model "${m.key}"`
|
|
5039
|
+
);
|
|
5040
|
+
for (const c of conflicts) {
|
|
5041
|
+
conflictOut.push({ modelKey: m.key, ...c });
|
|
5042
|
+
}
|
|
5043
|
+
plans.push({
|
|
5044
|
+
kind: "update",
|
|
5045
|
+
id: ex.id,
|
|
5046
|
+
name: m.name,
|
|
5047
|
+
mergedFields: merged,
|
|
5048
|
+
config: config2,
|
|
5049
|
+
// The snapshot we persist is the config fields the CLI pushed —
|
|
5050
|
+
// so a re-push with the same config is a no-op.
|
|
5051
|
+
pushSnapshotFields: cfgFields
|
|
5052
|
+
});
|
|
5053
|
+
}
|
|
5054
|
+
for (const [key, ex] of configOwnedByKey) {
|
|
5055
|
+
if (manifestKeys.has(key)) continue;
|
|
5056
|
+
const platFields = ex.fields ?? [];
|
|
5057
|
+
const snapFields = ex.lastPushedFields;
|
|
5058
|
+
if (snapFields !== void 0 && !deepEqual(platFields, snapFields)) {
|
|
5059
|
+
conflictOut.push({
|
|
5060
|
+
modelKey: key,
|
|
5061
|
+
fieldKey: "<model>",
|
|
5062
|
+
snapshot: snapFields,
|
|
5063
|
+
platform: platFields,
|
|
5064
|
+
config: void 0,
|
|
5065
|
+
kind: "deleted-locally-modified-remotely"
|
|
4927
5066
|
});
|
|
4928
|
-
|
|
4929
|
-
}
|
|
5067
|
+
if (!force) continue;
|
|
5068
|
+
}
|
|
5069
|
+
plans.push({ kind: "delete", id: ex.id });
|
|
5070
|
+
}
|
|
5071
|
+
if (conflictOut.length > 0 && !force) {
|
|
5072
|
+
throw new PushConflictError(conflictOut);
|
|
5073
|
+
}
|
|
5074
|
+
for (const p of plans) {
|
|
5075
|
+
if (p.kind === "create") {
|
|
4930
5076
|
await client.models.createModel({
|
|
4931
|
-
key:
|
|
4932
|
-
name:
|
|
4933
|
-
fields:
|
|
4934
|
-
config:
|
|
4935
|
-
configId
|
|
5077
|
+
key: p.model.key,
|
|
5078
|
+
name: p.model.name,
|
|
5079
|
+
fields: p.model.fields,
|
|
5080
|
+
config: p.config,
|
|
5081
|
+
configId,
|
|
5082
|
+
updatePushSnapshot: true
|
|
4936
5083
|
});
|
|
4937
5084
|
summary.models.created++;
|
|
4938
|
-
}
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
5085
|
+
} else if (p.kind === "update") {
|
|
5086
|
+
const fieldsToWrite = force ? p.pushSnapshotFields : p.mergedFields;
|
|
5087
|
+
await client.models.updateModel({
|
|
5088
|
+
id: p.id,
|
|
5089
|
+
name: p.name,
|
|
5090
|
+
fields: fieldsToWrite,
|
|
5091
|
+
config: p.config,
|
|
5092
|
+
updatePushSnapshot: true
|
|
5093
|
+
});
|
|
5094
|
+
summary.models.updated++;
|
|
5095
|
+
} else {
|
|
5096
|
+
await client.models.deleteModel(p.id);
|
|
4945
5097
|
summary.models.deleted++;
|
|
4946
5098
|
}
|
|
4947
5099
|
}
|
|
@@ -5300,7 +5452,7 @@ async function reconcileApiKeys(client, configKey, apiKeys, summary, rotateKeys)
|
|
|
5300
5452
|
}
|
|
5301
5453
|
}
|
|
5302
5454
|
}
|
|
5303
|
-
async function reconcileApps(client, tenantId, projectId, apps, summary) {
|
|
5455
|
+
async function reconcileApps(client, tenantId, projectId, apps, summary, force, conflictOut) {
|
|
5304
5456
|
const entries = Object.entries(apps);
|
|
5305
5457
|
if (entries.length === 0) return;
|
|
5306
5458
|
if (!tenantId || !projectId) {
|
|
@@ -5311,11 +5463,11 @@ async function reconcileApps(client, tenantId, projectId, apps, summary) {
|
|
|
5311
5463
|
}
|
|
5312
5464
|
const existingList = await client.apps.listApps(tenantId, projectId);
|
|
5313
5465
|
const existingByName = new Map(existingList.map((a) => [a.name, a]));
|
|
5466
|
+
const plans = [];
|
|
5314
5467
|
for (const [name, input] of entries) {
|
|
5315
5468
|
const existing = existingByName.get(name);
|
|
5316
5469
|
if (!existing) {
|
|
5317
|
-
|
|
5318
|
-
summary.apps.created += 1;
|
|
5470
|
+
plans.push({ kind: "install", name, input });
|
|
5319
5471
|
continue;
|
|
5320
5472
|
}
|
|
5321
5473
|
if (existing.manifestUrl !== input.source) {
|
|
@@ -5324,17 +5476,82 @@ async function reconcileApps(client, tenantId, projectId, apps, summary) {
|
|
|
5324
5476
|
);
|
|
5325
5477
|
continue;
|
|
5326
5478
|
}
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5479
|
+
const toBag = (v) => {
|
|
5480
|
+
if (!v || typeof v !== "object") return { sources: {}, sinks: {}, placement_fields: {} };
|
|
5481
|
+
const m = v;
|
|
5482
|
+
return {
|
|
5483
|
+
sources: m.sources ?? {},
|
|
5484
|
+
sinks: m.sinks ?? {},
|
|
5485
|
+
placement_fields: m.placement_fields ?? {}
|
|
5486
|
+
};
|
|
5487
|
+
};
|
|
5488
|
+
const platformBag = toBag(existing.mappings);
|
|
5489
|
+
const snapshotRaw = existing.lastPushedMappings;
|
|
5490
|
+
const hasSnapshot = snapshotRaw !== void 0 && snapshotRaw !== null;
|
|
5491
|
+
const snapshotBag = hasSnapshot ? toBag(snapshotRaw) : null;
|
|
5492
|
+
const configSources = input.mappings?.sources ?? {};
|
|
5493
|
+
const configSinks = input.mappings?.sinks ?? {};
|
|
5494
|
+
const configPlacements = input.mappings?.placementFields ?? {};
|
|
5495
|
+
const mergeBag = (cfg, plat, snap, label) => {
|
|
5496
|
+
const cMap = new Map(Object.entries(cfg));
|
|
5497
|
+
const pMap = new Map(Object.entries(plat));
|
|
5498
|
+
const sMap = snap ? new Map(Object.entries(snap)) : null;
|
|
5499
|
+
const { merged, conflicts } = threeWayMergeKeyed(
|
|
5500
|
+
// The merge helper expects items to have a .key; wrap values.
|
|
5501
|
+
new Map(Array.from(cMap, ([k, v]) => [k, { key: k, value: v }])),
|
|
5502
|
+
new Map(Array.from(pMap, ([k, v]) => [k, { key: k, value: v }])),
|
|
5503
|
+
sMap ? new Map(Array.from(sMap, ([k, v]) => [k, { key: k, value: v }])) : null,
|
|
5504
|
+
`${name} ${label}`
|
|
5505
|
+
);
|
|
5506
|
+
for (const c of conflicts) {
|
|
5507
|
+
conflictOut.push({ modelKey: `app:${name}`, ...c, fieldKey: `${label}.${c.fieldKey}` });
|
|
5508
|
+
}
|
|
5509
|
+
const out = {};
|
|
5510
|
+
for (const item of merged) {
|
|
5511
|
+
out[item.key] = item.value;
|
|
5512
|
+
}
|
|
5513
|
+
return out;
|
|
5514
|
+
};
|
|
5515
|
+
const mergedSources = mergeBag(configSources, platformBag.sources, snapshotBag?.sources ?? null, "sources");
|
|
5516
|
+
const mergedSinks = mergeBag(configSinks, platformBag.sinks, snapshotBag?.sinks ?? null, "sinks");
|
|
5517
|
+
const mergedPlacements = mergeBag(configPlacements, platformBag.placement_fields, snapshotBag?.placement_fields ?? null, "placementFields");
|
|
5518
|
+
plans.push({
|
|
5519
|
+
kind: "setMapping",
|
|
5330
5520
|
name,
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5521
|
+
mergedSources,
|
|
5522
|
+
mergedSinks,
|
|
5523
|
+
mergedPlacements,
|
|
5524
|
+
configMappings: {
|
|
5525
|
+
sources: configSources,
|
|
5526
|
+
sinks: configSinks,
|
|
5527
|
+
placementFields: configPlacements
|
|
5528
|
+
}
|
|
5336
5529
|
});
|
|
5337
|
-
|
|
5530
|
+
}
|
|
5531
|
+
if (conflictOut.length > 0 && !force) {
|
|
5532
|
+
throw new PushConflictError(conflictOut);
|
|
5533
|
+
}
|
|
5534
|
+
for (const p of plans) {
|
|
5535
|
+
if (p.kind === "install") {
|
|
5536
|
+
await installApp(client, tenantId, projectId, p.name, p.input);
|
|
5537
|
+
summary.apps.created += 1;
|
|
5538
|
+
} else if (p.kind === "setMapping") {
|
|
5539
|
+
const write = force ? p.configMappings : {
|
|
5540
|
+
sources: p.mergedSources,
|
|
5541
|
+
sinks: p.mergedSinks,
|
|
5542
|
+
placementFields: p.mergedPlacements
|
|
5543
|
+
};
|
|
5544
|
+
await client.apps.setAppMapping({
|
|
5545
|
+
tenantId,
|
|
5546
|
+
projectId,
|
|
5547
|
+
name: p.name,
|
|
5548
|
+
sourceMappings: toSourceMappings(write.sources),
|
|
5549
|
+
sinkMappings: toSinkMappings(write.sinks),
|
|
5550
|
+
placementFieldChoices: toPlacementFieldChoices(write.placementFields),
|
|
5551
|
+
updatePushSnapshot: true
|
|
5552
|
+
});
|
|
5553
|
+
summary.apps.updated += 1;
|
|
5554
|
+
}
|
|
5338
5555
|
}
|
|
5339
5556
|
}
|
|
5340
5557
|
async function installApp(client, tenantId, projectId, name, input) {
|
|
@@ -5373,7 +5590,8 @@ async function installApp(client, tenantId, projectId, name, input) {
|
|
|
5373
5590
|
settings: input.settings,
|
|
5374
5591
|
placementFieldChoices: toPlacementFieldChoices(
|
|
5375
5592
|
input.mappings?.placementFields ?? {}
|
|
5376
|
-
)
|
|
5593
|
+
),
|
|
5594
|
+
updatePushSnapshot: true
|
|
5377
5595
|
});
|
|
5378
5596
|
}
|
|
5379
5597
|
function toSourceMappings(input) {
|
|
@@ -5463,7 +5681,11 @@ function printSummary(summary) {
|
|
|
5463
5681
|
}
|
|
5464
5682
|
}
|
|
5465
5683
|
function registerPushCommand(program2, globalOpts) {
|
|
5466
|
-
program2.command("push").description("Push foir.config.ts to the platform").option("--config <path>", "Path to config file (default: auto-discover)").option(
|
|
5684
|
+
program2.command("push").description("Push foir.config.ts to the platform").option("--config <path>", "Path to config file (default: auto-discover)").option(
|
|
5685
|
+
"--force",
|
|
5686
|
+
"Overwrite platform state on three-way-merge conflicts with admin-UI edits. Without this flag, conflicts abort the push.",
|
|
5687
|
+
false
|
|
5688
|
+
).option(
|
|
5467
5689
|
"--rotate-keys",
|
|
5468
5690
|
"Rotate existing API keys and rewrite their values in .env",
|
|
5469
5691
|
false
|
|
@@ -5504,11 +5726,38 @@ function registerPushCommand(program2, globalOpts) {
|
|
|
5504
5726
|
}
|
|
5505
5727
|
const configId = applyResult.id;
|
|
5506
5728
|
console.log(chalk6.dim("Reconciling resources..."));
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5729
|
+
let summary;
|
|
5730
|
+
try {
|
|
5731
|
+
summary = await reconcileConfig(client, configId, config2, {
|
|
5732
|
+
rotateKeys: opts.rotateKeys ?? false,
|
|
5733
|
+
tenantId: resolved?.project.tenantId,
|
|
5734
|
+
projectId: resolved?.project.id,
|
|
5735
|
+
force: opts.force ?? false
|
|
5736
|
+
});
|
|
5737
|
+
} catch (e) {
|
|
5738
|
+
if (e instanceof PushConflictError) {
|
|
5739
|
+
console.error();
|
|
5740
|
+
console.error(chalk6.red("\u2716 Push aborted \u2014 three-way-merge conflicts detected"));
|
|
5741
|
+
console.error();
|
|
5742
|
+
console.error(
|
|
5743
|
+
chalk6.dim(
|
|
5744
|
+
" An admin-UI edit landed on the platform since the last `foir push`, and\n the local config changes the same field. Accepting either side would\n silently discard the other.\n"
|
|
5745
|
+
)
|
|
5746
|
+
);
|
|
5747
|
+
for (const c of e.conflicts) {
|
|
5748
|
+
console.error(chalk6.yellow(` ${c.modelKey} \xB7 ${c.fieldKey}`) + chalk6.dim(` (${c.kind})`));
|
|
5749
|
+
console.error(chalk6.dim(" snapshot: ") + JSON.stringify(c.snapshot));
|
|
5750
|
+
console.error(chalk6.dim(" platform: ") + JSON.stringify(c.platform));
|
|
5751
|
+
console.error(chalk6.dim(" config: ") + JSON.stringify(c.config));
|
|
5752
|
+
}
|
|
5753
|
+
console.error();
|
|
5754
|
+
console.error(chalk6.dim(" Resolution:"));
|
|
5755
|
+
console.error(chalk6.dim(" 1. `foir pull` to sync platform state into local config, OR"));
|
|
5756
|
+
console.error(chalk6.dim(" 2. re-run with --force to overwrite platform with local config"));
|
|
5757
|
+
process.exit(1);
|
|
5758
|
+
}
|
|
5759
|
+
throw e;
|
|
5760
|
+
}
|
|
5512
5761
|
console.log();
|
|
5513
5762
|
console.log(chalk6.green("\u2713 Config applied successfully"));
|
|
5514
5763
|
console.log(` Config ID: ${chalk6.cyan(configId)}`);
|
|
@@ -5668,6 +5917,39 @@ export default defineConfig(${jsonContent});
|
|
|
5668
5917
|
}
|
|
5669
5918
|
writeFileSync3(outPath, formatted, "utf-8");
|
|
5670
5919
|
console.log(chalk7.green(`\u2713 Exported to ${outPath}`));
|
|
5920
|
+
try {
|
|
5921
|
+
const { items: platformModels } = await client.models.listModels({ limit: 200 });
|
|
5922
|
+
for (const pm of platformModels) {
|
|
5923
|
+
await client.models.updateModel({
|
|
5924
|
+
id: pm.id,
|
|
5925
|
+
fields: pm.fields ?? [],
|
|
5926
|
+
snapshotOnly: true,
|
|
5927
|
+
updatePushSnapshot: true
|
|
5928
|
+
});
|
|
5929
|
+
}
|
|
5930
|
+
if (resolved && apps) {
|
|
5931
|
+
const tenantId = resolved.project.tenantId;
|
|
5932
|
+
const projectId = resolved.project.id;
|
|
5933
|
+
for (const [name, input] of Object.entries(apps)) {
|
|
5934
|
+
await client.apps.setAppMapping({
|
|
5935
|
+
tenantId,
|
|
5936
|
+
projectId,
|
|
5937
|
+
name,
|
|
5938
|
+
sourceMappings: input.mappings?.sources ?? {},
|
|
5939
|
+
sinkMappings: input.mappings?.sinks ?? {},
|
|
5940
|
+
placementFieldChoices: input.mappings?.placementFields ?? {},
|
|
5941
|
+
snapshotOnly: true
|
|
5942
|
+
});
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
console.log(chalk7.dim(" Synced push snapshots; next push will be a no-op."));
|
|
5946
|
+
} catch (e) {
|
|
5947
|
+
console.warn(
|
|
5948
|
+
chalk7.yellow(
|
|
5949
|
+
` Warning: failed to refresh push snapshots (${e.message}). Next push may flag spurious conflicts; re-run with --force if needed.`
|
|
5950
|
+
)
|
|
5951
|
+
);
|
|
5952
|
+
}
|
|
5671
5953
|
const models = configData.models ?? [];
|
|
5672
5954
|
const operations = configData.operations ?? [];
|
|
5673
5955
|
const hooks = configData.hooks ?? [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eide/foir-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Universal platform CLI for Foir platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@bufbuild/protovalidate": "^1.1.1",
|
|
51
51
|
"@connectrpc/connect": "^2.0.0",
|
|
52
52
|
"@connectrpc/connect-node": "^2.0.0",
|
|
53
|
-
"@eide/foir-proto-ts": "^0.
|
|
53
|
+
"@eide/foir-proto-ts": "^0.26.0",
|
|
54
54
|
"chalk": "^5.3.0",
|
|
55
55
|
"commander": "^12.1.0",
|
|
56
56
|
"dotenv": "^16.4.5",
|