@eide/foir-cli 0.10.2 → 0.11.1
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 +341 -82
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1188,7 +1188,8 @@ function createAppsMethods(client) {
|
|
|
1188
1188
|
sourceMappings: params.sourceMappings ?? {},
|
|
1189
1189
|
sinkMappings: params.sinkMappings ?? {},
|
|
1190
1190
|
settings: params.settings,
|
|
1191
|
-
placementFieldChoices: params.placementFieldChoices ?? {}
|
|
1191
|
+
placementFieldChoices: params.placementFieldChoices ?? {},
|
|
1192
|
+
updatePushSnapshot: params.updatePushSnapshot ?? false
|
|
1192
1193
|
})
|
|
1193
1194
|
);
|
|
1194
1195
|
return resp.app;
|
|
@@ -1222,7 +1223,9 @@ function createAppsMethods(client) {
|
|
|
1222
1223
|
name: params.name,
|
|
1223
1224
|
sourceMappings: params.sourceMappings ?? {},
|
|
1224
1225
|
sinkMappings: params.sinkMappings ?? {},
|
|
1225
|
-
placementFieldChoices: params.placementFieldChoices ?? {}
|
|
1226
|
+
placementFieldChoices: params.placementFieldChoices ?? {},
|
|
1227
|
+
updatePushSnapshot: params.updatePushSnapshot ?? false,
|
|
1228
|
+
snapshotOnly: params.snapshotOnly ?? false
|
|
1226
1229
|
})
|
|
1227
1230
|
);
|
|
1228
1231
|
return resp.app;
|
|
@@ -1347,7 +1350,8 @@ function createModelsMethods(client) {
|
|
|
1347
1350
|
name: params.name,
|
|
1348
1351
|
fields: params.fields.map(jsFieldToProto),
|
|
1349
1352
|
config: params.config ? jsConfigToProto(params.config) : void 0,
|
|
1350
|
-
configId: params.configId
|
|
1353
|
+
configId: params.configId,
|
|
1354
|
+
updatePushSnapshot: params.updatePushSnapshot ?? false
|
|
1351
1355
|
})
|
|
1352
1356
|
);
|
|
1353
1357
|
return resp.model ?? null;
|
|
@@ -1360,7 +1364,9 @@ function createModelsMethods(client) {
|
|
|
1360
1364
|
updateFields: params.fields != null,
|
|
1361
1365
|
fields: params.fields?.map(jsFieldToProto) ?? [],
|
|
1362
1366
|
config: params.config ? jsConfigToProto(params.config) : void 0,
|
|
1363
|
-
changeDescription: params.changeDescription
|
|
1367
|
+
changeDescription: params.changeDescription,
|
|
1368
|
+
updatePushSnapshot: params.updatePushSnapshot ?? false,
|
|
1369
|
+
snapshotOnly: params.snapshotOnly ?? false
|
|
1364
1370
|
})
|
|
1365
1371
|
);
|
|
1366
1372
|
return resp.model ?? null;
|
|
@@ -2353,9 +2359,6 @@ import {
|
|
|
2353
2359
|
UpdateCustomerProfileSchemaRequestSchema,
|
|
2354
2360
|
GetCustomerResolutionAttributesRequestSchema,
|
|
2355
2361
|
GetEditorConfigsRequestSchema,
|
|
2356
|
-
ListEmailActionsRequestSchema,
|
|
2357
|
-
ListResendTemplatesRequestSchema,
|
|
2358
|
-
UpdateEmailActionRequestSchema,
|
|
2359
2362
|
ListVariantCatalogRequestSchema,
|
|
2360
2363
|
GetVariantCatalogEntryRequestSchema,
|
|
2361
2364
|
CreateVariantCatalogEntryRequestSchema,
|
|
@@ -2735,34 +2738,6 @@ function createSettingsMethods(client) {
|
|
|
2735
2738
|
create8(ClearRecentlyOpenedRequestSchema, {})
|
|
2736
2739
|
);
|
|
2737
2740
|
return resp.success;
|
|
2738
|
-
},
|
|
2739
|
-
// ── Email Actions ──────────────────────────────────────────
|
|
2740
|
-
async listEmailActions(params = {}) {
|
|
2741
|
-
return client.listEmailActions(
|
|
2742
|
-
create8(ListEmailActionsRequestSchema, {
|
|
2743
|
-
limit: params.limit ?? 50,
|
|
2744
|
-
offset: params.offset ?? 0
|
|
2745
|
-
})
|
|
2746
|
-
);
|
|
2747
|
-
},
|
|
2748
|
-
async updateEmailAction(params) {
|
|
2749
|
-
const resp = await client.updateEmailAction(
|
|
2750
|
-
create8(UpdateEmailActionRequestSchema, {
|
|
2751
|
-
key: params.key,
|
|
2752
|
-
resendTemplateId: params.resendTemplateId,
|
|
2753
|
-
defaultFrom: params.defaultFrom,
|
|
2754
|
-
defaultSubject: params.defaultSubject,
|
|
2755
|
-
customData: params.customData,
|
|
2756
|
-
isActive: params.isActive
|
|
2757
|
-
})
|
|
2758
|
-
);
|
|
2759
|
-
return resp.action ?? null;
|
|
2760
|
-
},
|
|
2761
|
-
async listResendTemplates() {
|
|
2762
|
-
const resp = await client.listResendTemplates(
|
|
2763
|
-
create8(ListResendTemplatesRequestSchema, {})
|
|
2764
|
-
);
|
|
2765
|
-
return resp.templates ?? [];
|
|
2766
2741
|
}
|
|
2767
2742
|
};
|
|
2768
2743
|
}
|
|
@@ -3606,11 +3581,22 @@ function pad(str, width) {
|
|
|
3606
3581
|
}
|
|
3607
3582
|
return str.padEnd(width);
|
|
3608
3583
|
}
|
|
3609
|
-
function timeAgo(
|
|
3610
|
-
if (
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3584
|
+
function timeAgo(value) {
|
|
3585
|
+
if (value == null) return "\u2014";
|
|
3586
|
+
let date;
|
|
3587
|
+
if (value instanceof Date) {
|
|
3588
|
+
date = value;
|
|
3589
|
+
} else if (typeof value === "string") {
|
|
3590
|
+
date = new Date(value);
|
|
3591
|
+
} else if (typeof value === "object" && "seconds" in value && value.seconds != null) {
|
|
3592
|
+
const seconds = typeof value.seconds === "bigint" ? Number(value.seconds) : value.seconds;
|
|
3593
|
+
const nanos = value.nanos ?? 0;
|
|
3594
|
+
date = new Date(seconds * 1e3 + Math.floor(nanos / 1e6));
|
|
3595
|
+
} else {
|
|
3596
|
+
return "\u2014";
|
|
3597
|
+
}
|
|
3598
|
+
if (Number.isNaN(date.getTime())) return "\u2014";
|
|
3599
|
+
const diffMs = Date.now() - date.getTime();
|
|
3614
3600
|
if (diffMs < 0) return "future";
|
|
3615
3601
|
const minutes = Math.floor(diffMs / 6e4);
|
|
3616
3602
|
if (minutes < 1) return "just now";
|
|
@@ -4866,6 +4852,93 @@ import { resolve as resolve4 } from "path";
|
|
|
4866
4852
|
function zeroCounts() {
|
|
4867
4853
|
return { created: 0, updated: 0, deleted: 0 };
|
|
4868
4854
|
}
|
|
4855
|
+
var PushConflictError = class extends Error {
|
|
4856
|
+
conflicts;
|
|
4857
|
+
constructor(conflicts) {
|
|
4858
|
+
super(
|
|
4859
|
+
`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.`
|
|
4860
|
+
);
|
|
4861
|
+
this.name = "PushConflictError";
|
|
4862
|
+
this.conflicts = conflicts;
|
|
4863
|
+
}
|
|
4864
|
+
};
|
|
4865
|
+
function canonicalize(v) {
|
|
4866
|
+
if (v === void 0) return "undefined";
|
|
4867
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v);
|
|
4868
|
+
if (Array.isArray(v)) return "[" + v.map(canonicalize).join(",") + "]";
|
|
4869
|
+
const obj = v;
|
|
4870
|
+
const keys = Object.keys(obj).sort();
|
|
4871
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
|
|
4872
|
+
}
|
|
4873
|
+
function deepEqual(a, b) {
|
|
4874
|
+
return canonicalize(a) === canonicalize(b);
|
|
4875
|
+
}
|
|
4876
|
+
function indexByKey(items) {
|
|
4877
|
+
const out = /* @__PURE__ */ new Map();
|
|
4878
|
+
for (const it of items ?? []) {
|
|
4879
|
+
if (it && it.key) out.set(it.key, it);
|
|
4880
|
+
}
|
|
4881
|
+
return out;
|
|
4882
|
+
}
|
|
4883
|
+
function threeWayMergeKeyed(config2, platform, snapshot, parentLabel) {
|
|
4884
|
+
const conflicts = [];
|
|
4885
|
+
const merged = [];
|
|
4886
|
+
const keys = /* @__PURE__ */ new Set([
|
|
4887
|
+
...config2.keys(),
|
|
4888
|
+
...platform.keys(),
|
|
4889
|
+
...snapshot ? snapshot.keys() : []
|
|
4890
|
+
]);
|
|
4891
|
+
const noSnapshot = snapshot === null;
|
|
4892
|
+
for (const key of keys) {
|
|
4893
|
+
const inC = config2.has(key);
|
|
4894
|
+
const inP = platform.has(key);
|
|
4895
|
+
const inS = snapshot ? snapshot.has(key) : false;
|
|
4896
|
+
const c = config2.get(key);
|
|
4897
|
+
const p = platform.get(key);
|
|
4898
|
+
const s = snapshot ? snapshot.get(key) : void 0;
|
|
4899
|
+
if (noSnapshot) {
|
|
4900
|
+
if (inC) merged.push(c);
|
|
4901
|
+
else if (inP) merged.push(p);
|
|
4902
|
+
continue;
|
|
4903
|
+
}
|
|
4904
|
+
if (inC && inP && inS) {
|
|
4905
|
+
if (deepEqual(p, s)) {
|
|
4906
|
+
merged.push(c);
|
|
4907
|
+
} else if (deepEqual(c, s)) {
|
|
4908
|
+
merged.push(p);
|
|
4909
|
+
} else if (deepEqual(c, p)) {
|
|
4910
|
+
merged.push(c);
|
|
4911
|
+
} else {
|
|
4912
|
+
conflicts.push({ fieldKey: key, snapshot: s, platform: p, config: c, kind: "modified-both" });
|
|
4913
|
+
merged.push(p);
|
|
4914
|
+
}
|
|
4915
|
+
} else if (inC && !inS && !inP) {
|
|
4916
|
+
merged.push(c);
|
|
4917
|
+
} else if (inC && inS && !inP) {
|
|
4918
|
+
if (deepEqual(c, s)) {
|
|
4919
|
+
continue;
|
|
4920
|
+
}
|
|
4921
|
+
conflicts.push({ fieldKey: key, snapshot: s, platform: void 0, config: c, kind: "deleted-remotely-modified-locally" });
|
|
4922
|
+
} else if (!inC && inS && inP) {
|
|
4923
|
+
if (deepEqual(p, s)) {
|
|
4924
|
+
continue;
|
|
4925
|
+
}
|
|
4926
|
+
conflicts.push({ fieldKey: key, snapshot: s, platform: p, config: void 0, kind: "deleted-locally-modified-remotely" });
|
|
4927
|
+
merged.push(p);
|
|
4928
|
+
} else if (!inC && !inS && inP) {
|
|
4929
|
+
merged.push(p);
|
|
4930
|
+
} else if (inC && !inS && inP) {
|
|
4931
|
+
if (deepEqual(c, p)) {
|
|
4932
|
+
merged.push(c);
|
|
4933
|
+
} else {
|
|
4934
|
+
conflicts.push({ fieldKey: key, snapshot: void 0, platform: p, config: c, kind: "modified-both" });
|
|
4935
|
+
merged.push(p);
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
void parentLabel;
|
|
4940
|
+
return { merged, conflicts };
|
|
4941
|
+
}
|
|
4869
4942
|
async function reconcileConfig(client, configId, manifest, options = {}) {
|
|
4870
4943
|
const summary = {
|
|
4871
4944
|
models: zeroCounts(),
|
|
@@ -4880,7 +4953,9 @@ async function reconcileConfig(client, configId, manifest, options = {}) {
|
|
|
4880
4953
|
apps: zeroCounts()
|
|
4881
4954
|
};
|
|
4882
4955
|
const operationBaseUrl = manifest.operationBaseUrl ?? "";
|
|
4883
|
-
|
|
4956
|
+
const modelConflicts = [];
|
|
4957
|
+
const mappingConflicts = [];
|
|
4958
|
+
await reconcileModels(client, configId, manifest.models ?? [], summary, options.force ?? false, modelConflicts);
|
|
4884
4959
|
await reconcileOperations(client, configId, manifest.operations ?? [], operationBaseUrl, summary);
|
|
4885
4960
|
await reconcileHooks(client, configId, manifest.hooks ?? [], summary);
|
|
4886
4961
|
await reconcileSegments(client, configId, manifest.segments ?? [], summary);
|
|
@@ -4900,11 +4975,17 @@ async function reconcileConfig(client, configId, manifest, options = {}) {
|
|
|
4900
4975
|
options.tenantId,
|
|
4901
4976
|
options.projectId,
|
|
4902
4977
|
manifest.apps ?? {},
|
|
4903
|
-
summary
|
|
4978
|
+
summary,
|
|
4979
|
+
options.force ?? false,
|
|
4980
|
+
mappingConflicts
|
|
4904
4981
|
);
|
|
4982
|
+
const allConflicts = [...modelConflicts, ...mappingConflicts];
|
|
4983
|
+
if (allConflicts.length > 0 && !options.force) {
|
|
4984
|
+
throw new PushConflictError(allConflicts);
|
|
4985
|
+
}
|
|
4905
4986
|
return summary;
|
|
4906
4987
|
}
|
|
4907
|
-
async function reconcileModels(client, configId, models, summary) {
|
|
4988
|
+
async function reconcileModels(client, configId, models, summary, force, conflictOut) {
|
|
4908
4989
|
const existing = await client.models.listModels({ limit: 200 });
|
|
4909
4990
|
const allByKey = new Map(
|
|
4910
4991
|
existing.items.map((m) => [m.key, m])
|
|
@@ -4913,6 +4994,7 @@ async function reconcileModels(client, configId, models, summary) {
|
|
|
4913
4994
|
existing.items.filter((m) => m.configId === configId).map((m) => [m.key, m])
|
|
4914
4995
|
);
|
|
4915
4996
|
const manifestKeys = /* @__PURE__ */ new Set();
|
|
4997
|
+
const plans = [];
|
|
4916
4998
|
for (const m of models) {
|
|
4917
4999
|
if (!m.key || !m.name) continue;
|
|
4918
5000
|
manifestKeys.add(m.key);
|
|
@@ -4921,30 +5003,77 @@ async function reconcileModels(client, configId, models, summary) {
|
|
|
4921
5003
|
if (m.pluralKey) config2.pluralKey = m.pluralKey;
|
|
4922
5004
|
if (m.description) config2.description = m.description;
|
|
4923
5005
|
const ex = allByKey.get(m.key);
|
|
4924
|
-
if (ex) {
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
5006
|
+
if (!ex) {
|
|
5007
|
+
plans.push({ kind: "create", model: m, config: config2 });
|
|
5008
|
+
continue;
|
|
5009
|
+
}
|
|
5010
|
+
const cfgFields = m.fields ?? [];
|
|
5011
|
+
const platFields = ex.fields ?? [];
|
|
5012
|
+
const snapFields = ex.lastPushedFields;
|
|
5013
|
+
const hasSnapshot = snapFields !== void 0 && snapFields !== null;
|
|
5014
|
+
const { merged, conflicts } = threeWayMergeKeyed(
|
|
5015
|
+
indexByKey(cfgFields),
|
|
5016
|
+
indexByKey(platFields),
|
|
5017
|
+
hasSnapshot ? indexByKey(snapFields) : null,
|
|
5018
|
+
`model "${m.key}"`
|
|
5019
|
+
);
|
|
5020
|
+
for (const c of conflicts) {
|
|
5021
|
+
conflictOut.push({ modelKey: m.key, ...c });
|
|
5022
|
+
}
|
|
5023
|
+
plans.push({
|
|
5024
|
+
kind: "update",
|
|
5025
|
+
id: ex.id,
|
|
5026
|
+
name: m.name,
|
|
5027
|
+
mergedFields: merged,
|
|
5028
|
+
config: config2,
|
|
5029
|
+
// The snapshot we persist is the config fields the CLI pushed —
|
|
5030
|
+
// so a re-push with the same config is a no-op.
|
|
5031
|
+
pushSnapshotFields: cfgFields
|
|
5032
|
+
});
|
|
5033
|
+
}
|
|
5034
|
+
for (const [key, ex] of configOwnedByKey) {
|
|
5035
|
+
if (manifestKeys.has(key)) continue;
|
|
5036
|
+
const platFields = ex.fields ?? [];
|
|
5037
|
+
const snapFields = ex.lastPushedFields;
|
|
5038
|
+
if (snapFields !== void 0 && !deepEqual(platFields, snapFields)) {
|
|
5039
|
+
conflictOut.push({
|
|
5040
|
+
modelKey: key,
|
|
5041
|
+
fieldKey: "<model>",
|
|
5042
|
+
snapshot: snapFields,
|
|
5043
|
+
platform: platFields,
|
|
5044
|
+
config: void 0,
|
|
5045
|
+
kind: "deleted-locally-modified-remotely"
|
|
4930
5046
|
});
|
|
4931
|
-
|
|
4932
|
-
}
|
|
5047
|
+
if (!force) continue;
|
|
5048
|
+
}
|
|
5049
|
+
plans.push({ kind: "delete", id: ex.id });
|
|
5050
|
+
}
|
|
5051
|
+
if (conflictOut.length > 0 && !force) {
|
|
5052
|
+
throw new PushConflictError(conflictOut);
|
|
5053
|
+
}
|
|
5054
|
+
for (const p of plans) {
|
|
5055
|
+
if (p.kind === "create") {
|
|
4933
5056
|
await client.models.createModel({
|
|
4934
|
-
key:
|
|
4935
|
-
name:
|
|
4936
|
-
fields:
|
|
4937
|
-
config:
|
|
4938
|
-
configId
|
|
5057
|
+
key: p.model.key,
|
|
5058
|
+
name: p.model.name,
|
|
5059
|
+
fields: p.model.fields,
|
|
5060
|
+
config: p.config,
|
|
5061
|
+
configId,
|
|
5062
|
+
updatePushSnapshot: true
|
|
4939
5063
|
});
|
|
4940
5064
|
summary.models.created++;
|
|
4941
|
-
}
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
5065
|
+
} else if (p.kind === "update") {
|
|
5066
|
+
const fieldsToWrite = force ? p.pushSnapshotFields : p.mergedFields;
|
|
5067
|
+
await client.models.updateModel({
|
|
5068
|
+
id: p.id,
|
|
5069
|
+
name: p.name,
|
|
5070
|
+
fields: fieldsToWrite,
|
|
5071
|
+
config: p.config,
|
|
5072
|
+
updatePushSnapshot: true
|
|
5073
|
+
});
|
|
5074
|
+
summary.models.updated++;
|
|
5075
|
+
} else {
|
|
5076
|
+
await client.models.deleteModel(p.id);
|
|
4948
5077
|
summary.models.deleted++;
|
|
4949
5078
|
}
|
|
4950
5079
|
}
|
|
@@ -5303,7 +5432,7 @@ async function reconcileApiKeys(client, configKey, apiKeys, summary, rotateKeys)
|
|
|
5303
5432
|
}
|
|
5304
5433
|
}
|
|
5305
5434
|
}
|
|
5306
|
-
async function reconcileApps(client, tenantId, projectId, apps, summary) {
|
|
5435
|
+
async function reconcileApps(client, tenantId, projectId, apps, summary, force, conflictOut) {
|
|
5307
5436
|
const entries = Object.entries(apps);
|
|
5308
5437
|
if (entries.length === 0) return;
|
|
5309
5438
|
if (!tenantId || !projectId) {
|
|
@@ -5314,11 +5443,11 @@ async function reconcileApps(client, tenantId, projectId, apps, summary) {
|
|
|
5314
5443
|
}
|
|
5315
5444
|
const existingList = await client.apps.listApps(tenantId, projectId);
|
|
5316
5445
|
const existingByName = new Map(existingList.map((a) => [a.name, a]));
|
|
5446
|
+
const plans = [];
|
|
5317
5447
|
for (const [name, input] of entries) {
|
|
5318
5448
|
const existing = existingByName.get(name);
|
|
5319
5449
|
if (!existing) {
|
|
5320
|
-
|
|
5321
|
-
summary.apps.created += 1;
|
|
5450
|
+
plans.push({ kind: "install", name, input });
|
|
5322
5451
|
continue;
|
|
5323
5452
|
}
|
|
5324
5453
|
if (existing.manifestUrl !== input.source) {
|
|
@@ -5327,17 +5456,82 @@ async function reconcileApps(client, tenantId, projectId, apps, summary) {
|
|
|
5327
5456
|
);
|
|
5328
5457
|
continue;
|
|
5329
5458
|
}
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5459
|
+
const toBag = (v) => {
|
|
5460
|
+
if (!v || typeof v !== "object") return { sources: {}, sinks: {}, placement_fields: {} };
|
|
5461
|
+
const m = v;
|
|
5462
|
+
return {
|
|
5463
|
+
sources: m.sources ?? {},
|
|
5464
|
+
sinks: m.sinks ?? {},
|
|
5465
|
+
placement_fields: m.placement_fields ?? {}
|
|
5466
|
+
};
|
|
5467
|
+
};
|
|
5468
|
+
const platformBag = toBag(existing.mappings);
|
|
5469
|
+
const snapshotRaw = existing.lastPushedMappings;
|
|
5470
|
+
const hasSnapshot = snapshotRaw !== void 0 && snapshotRaw !== null;
|
|
5471
|
+
const snapshotBag = hasSnapshot ? toBag(snapshotRaw) : null;
|
|
5472
|
+
const configSources = input.mappings?.sources ?? {};
|
|
5473
|
+
const configSinks = input.mappings?.sinks ?? {};
|
|
5474
|
+
const configPlacements = input.mappings?.placementFields ?? {};
|
|
5475
|
+
const mergeBag = (cfg, plat, snap, label) => {
|
|
5476
|
+
const cMap = new Map(Object.entries(cfg));
|
|
5477
|
+
const pMap = new Map(Object.entries(plat));
|
|
5478
|
+
const sMap = snap ? new Map(Object.entries(snap)) : null;
|
|
5479
|
+
const { merged, conflicts } = threeWayMergeKeyed(
|
|
5480
|
+
// The merge helper expects items to have a .key; wrap values.
|
|
5481
|
+
new Map(Array.from(cMap, ([k, v]) => [k, { key: k, value: v }])),
|
|
5482
|
+
new Map(Array.from(pMap, ([k, v]) => [k, { key: k, value: v }])),
|
|
5483
|
+
sMap ? new Map(Array.from(sMap, ([k, v]) => [k, { key: k, value: v }])) : null,
|
|
5484
|
+
`${name} ${label}`
|
|
5485
|
+
);
|
|
5486
|
+
for (const c of conflicts) {
|
|
5487
|
+
conflictOut.push({ modelKey: `app:${name}`, ...c, fieldKey: `${label}.${c.fieldKey}` });
|
|
5488
|
+
}
|
|
5489
|
+
const out = {};
|
|
5490
|
+
for (const item of merged) {
|
|
5491
|
+
out[item.key] = item.value;
|
|
5492
|
+
}
|
|
5493
|
+
return out;
|
|
5494
|
+
};
|
|
5495
|
+
const mergedSources = mergeBag(configSources, platformBag.sources, snapshotBag?.sources ?? null, "sources");
|
|
5496
|
+
const mergedSinks = mergeBag(configSinks, platformBag.sinks, snapshotBag?.sinks ?? null, "sinks");
|
|
5497
|
+
const mergedPlacements = mergeBag(configPlacements, platformBag.placement_fields, snapshotBag?.placement_fields ?? null, "placementFields");
|
|
5498
|
+
plans.push({
|
|
5499
|
+
kind: "setMapping",
|
|
5333
5500
|
name,
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5501
|
+
mergedSources,
|
|
5502
|
+
mergedSinks,
|
|
5503
|
+
mergedPlacements,
|
|
5504
|
+
configMappings: {
|
|
5505
|
+
sources: configSources,
|
|
5506
|
+
sinks: configSinks,
|
|
5507
|
+
placementFields: configPlacements
|
|
5508
|
+
}
|
|
5339
5509
|
});
|
|
5340
|
-
|
|
5510
|
+
}
|
|
5511
|
+
if (conflictOut.length > 0 && !force) {
|
|
5512
|
+
throw new PushConflictError(conflictOut);
|
|
5513
|
+
}
|
|
5514
|
+
for (const p of plans) {
|
|
5515
|
+
if (p.kind === "install") {
|
|
5516
|
+
await installApp(client, tenantId, projectId, p.name, p.input);
|
|
5517
|
+
summary.apps.created += 1;
|
|
5518
|
+
} else if (p.kind === "setMapping") {
|
|
5519
|
+
const write = force ? p.configMappings : {
|
|
5520
|
+
sources: p.mergedSources,
|
|
5521
|
+
sinks: p.mergedSinks,
|
|
5522
|
+
placementFields: p.mergedPlacements
|
|
5523
|
+
};
|
|
5524
|
+
await client.apps.setAppMapping({
|
|
5525
|
+
tenantId,
|
|
5526
|
+
projectId,
|
|
5527
|
+
name: p.name,
|
|
5528
|
+
sourceMappings: toSourceMappings(write.sources),
|
|
5529
|
+
sinkMappings: toSinkMappings(write.sinks),
|
|
5530
|
+
placementFieldChoices: toPlacementFieldChoices(write.placementFields),
|
|
5531
|
+
updatePushSnapshot: true
|
|
5532
|
+
});
|
|
5533
|
+
summary.apps.updated += 1;
|
|
5534
|
+
}
|
|
5341
5535
|
}
|
|
5342
5536
|
}
|
|
5343
5537
|
async function installApp(client, tenantId, projectId, name, input) {
|
|
@@ -5376,7 +5570,8 @@ async function installApp(client, tenantId, projectId, name, input) {
|
|
|
5376
5570
|
settings: input.settings,
|
|
5377
5571
|
placementFieldChoices: toPlacementFieldChoices(
|
|
5378
5572
|
input.mappings?.placementFields ?? {}
|
|
5379
|
-
)
|
|
5573
|
+
),
|
|
5574
|
+
updatePushSnapshot: true
|
|
5380
5575
|
});
|
|
5381
5576
|
}
|
|
5382
5577
|
function toSourceMappings(input) {
|
|
@@ -5466,7 +5661,11 @@ function printSummary(summary) {
|
|
|
5466
5661
|
}
|
|
5467
5662
|
}
|
|
5468
5663
|
function registerPushCommand(program2, globalOpts) {
|
|
5469
|
-
program2.command("push").description("Push foir.config.ts to the platform").option("--config <path>", "Path to config file (default: auto-discover)").option(
|
|
5664
|
+
program2.command("push").description("Push foir.config.ts to the platform").option("--config <path>", "Path to config file (default: auto-discover)").option(
|
|
5665
|
+
"--force",
|
|
5666
|
+
"Overwrite platform state on three-way-merge conflicts with admin-UI edits. Without this flag, conflicts abort the push.",
|
|
5667
|
+
false
|
|
5668
|
+
).option(
|
|
5470
5669
|
"--rotate-keys",
|
|
5471
5670
|
"Rotate existing API keys and rewrite their values in .env",
|
|
5472
5671
|
false
|
|
@@ -5507,11 +5706,38 @@ function registerPushCommand(program2, globalOpts) {
|
|
|
5507
5706
|
}
|
|
5508
5707
|
const configId = applyResult.id;
|
|
5509
5708
|
console.log(chalk6.dim("Reconciling resources..."));
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5709
|
+
let summary;
|
|
5710
|
+
try {
|
|
5711
|
+
summary = await reconcileConfig(client, configId, config2, {
|
|
5712
|
+
rotateKeys: opts.rotateKeys ?? false,
|
|
5713
|
+
tenantId: resolved?.project.tenantId,
|
|
5714
|
+
projectId: resolved?.project.id,
|
|
5715
|
+
force: opts.force ?? false
|
|
5716
|
+
});
|
|
5717
|
+
} catch (e) {
|
|
5718
|
+
if (e instanceof PushConflictError) {
|
|
5719
|
+
console.error();
|
|
5720
|
+
console.error(chalk6.red("\u2716 Push aborted \u2014 three-way-merge conflicts detected"));
|
|
5721
|
+
console.error();
|
|
5722
|
+
console.error(
|
|
5723
|
+
chalk6.dim(
|
|
5724
|
+
" 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"
|
|
5725
|
+
)
|
|
5726
|
+
);
|
|
5727
|
+
for (const c of e.conflicts) {
|
|
5728
|
+
console.error(chalk6.yellow(` ${c.modelKey} \xB7 ${c.fieldKey}`) + chalk6.dim(` (${c.kind})`));
|
|
5729
|
+
console.error(chalk6.dim(" snapshot: ") + JSON.stringify(c.snapshot));
|
|
5730
|
+
console.error(chalk6.dim(" platform: ") + JSON.stringify(c.platform));
|
|
5731
|
+
console.error(chalk6.dim(" config: ") + JSON.stringify(c.config));
|
|
5732
|
+
}
|
|
5733
|
+
console.error();
|
|
5734
|
+
console.error(chalk6.dim(" Resolution:"));
|
|
5735
|
+
console.error(chalk6.dim(" 1. `foir pull` to sync platform state into local config, OR"));
|
|
5736
|
+
console.error(chalk6.dim(" 2. re-run with --force to overwrite platform with local config"));
|
|
5737
|
+
process.exit(1);
|
|
5738
|
+
}
|
|
5739
|
+
throw e;
|
|
5740
|
+
}
|
|
5515
5741
|
console.log();
|
|
5516
5742
|
console.log(chalk6.green("\u2713 Config applied successfully"));
|
|
5517
5743
|
console.log(` Config ID: ${chalk6.cyan(configId)}`);
|
|
@@ -5671,6 +5897,39 @@ export default defineConfig(${jsonContent});
|
|
|
5671
5897
|
}
|
|
5672
5898
|
writeFileSync3(outPath, formatted, "utf-8");
|
|
5673
5899
|
console.log(chalk7.green(`\u2713 Exported to ${outPath}`));
|
|
5900
|
+
try {
|
|
5901
|
+
const { items: platformModels } = await client.models.listModels({ limit: 200 });
|
|
5902
|
+
for (const pm of platformModels) {
|
|
5903
|
+
await client.models.updateModel({
|
|
5904
|
+
id: pm.id,
|
|
5905
|
+
fields: pm.fields ?? [],
|
|
5906
|
+
snapshotOnly: true,
|
|
5907
|
+
updatePushSnapshot: true
|
|
5908
|
+
});
|
|
5909
|
+
}
|
|
5910
|
+
if (resolved && apps) {
|
|
5911
|
+
const tenantId = resolved.project.tenantId;
|
|
5912
|
+
const projectId = resolved.project.id;
|
|
5913
|
+
for (const [name, input] of Object.entries(apps)) {
|
|
5914
|
+
await client.apps.setAppMapping({
|
|
5915
|
+
tenantId,
|
|
5916
|
+
projectId,
|
|
5917
|
+
name,
|
|
5918
|
+
sourceMappings: input.mappings?.sources ?? {},
|
|
5919
|
+
sinkMappings: input.mappings?.sinks ?? {},
|
|
5920
|
+
placementFieldChoices: input.mappings?.placementFields ?? {},
|
|
5921
|
+
snapshotOnly: true
|
|
5922
|
+
});
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
console.log(chalk7.dim(" Synced push snapshots; next push will be a no-op."));
|
|
5926
|
+
} catch (e) {
|
|
5927
|
+
console.warn(
|
|
5928
|
+
chalk7.yellow(
|
|
5929
|
+
` Warning: failed to refresh push snapshots (${e.message}). Next push may flag spurious conflicts; re-run with --force if needed.`
|
|
5930
|
+
)
|
|
5931
|
+
);
|
|
5932
|
+
}
|
|
5674
5933
|
const models = configData.models ?? [];
|
|
5675
5934
|
const operations = configData.operations ?? [];
|
|
5676
5935
|
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.1",
|
|
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",
|