@eventcatalog/cli 0.4.11 → 0.5.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/index.js +543 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +541 -12
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli-docs.js +109 -0
- package/dist/cli-docs.js.map +1 -1
- package/dist/cli-docs.mjs +109 -0
- package/dist/cli-docs.mjs.map +1 -1
- package/package.json +7 -3
package/dist/cli/index.js
CHANGED
|
@@ -25,8 +25,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/cli/index.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
|
-
var
|
|
29
|
-
var
|
|
28
|
+
var import_node_fs7 = require("fs");
|
|
29
|
+
var import_node_path6 = require("path");
|
|
30
30
|
|
|
31
31
|
// src/cli/executor.ts
|
|
32
32
|
var import_node_fs = require("fs");
|
|
@@ -497,8 +497,8 @@ ${messages.join("\n")}`);
|
|
|
497
497
|
}
|
|
498
498
|
return { outputs, program: program2 };
|
|
499
499
|
}
|
|
500
|
-
function extractResourceTypeFolder(
|
|
501
|
-
const segments =
|
|
500
|
+
function extractResourceTypeFolder(path3) {
|
|
501
|
+
const segments = path3.split("/");
|
|
502
502
|
let lastTypeFolder = segments[0];
|
|
503
503
|
for (const seg of segments) {
|
|
504
504
|
if (RESOURCE_TYPE_FROM_FOLDER[seg]) {
|
|
@@ -690,8 +690,8 @@ function extractServiceContainerRefs(program2, nested = false) {
|
|
|
690
690
|
...stmt.ref.version ? { version: stmt.ref.version } : {}
|
|
691
691
|
}));
|
|
692
692
|
if (writesTo.length === 0 && readsFrom.length === 0) continue;
|
|
693
|
-
const
|
|
694
|
-
refsByPath.set(
|
|
693
|
+
const path3 = buildServiceOutputPath(def.name, body, nested, parentPath);
|
|
694
|
+
refsByPath.set(path3, {
|
|
695
695
|
...writesTo.length > 0 ? { writesTo } : {},
|
|
696
696
|
...readsFrom.length > 0 ? { readsFrom } : {}
|
|
697
697
|
});
|
|
@@ -782,21 +782,21 @@ function getReader(sdk, type) {
|
|
|
782
782
|
}
|
|
783
783
|
}
|
|
784
784
|
function promptConfirm(message) {
|
|
785
|
-
return new Promise((
|
|
785
|
+
return new Promise((resolve5) => {
|
|
786
786
|
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
787
787
|
rl.question(`${message} `, (answer) => {
|
|
788
788
|
rl.close();
|
|
789
789
|
const normalized = answer.trim().toLowerCase();
|
|
790
|
-
|
|
790
|
+
resolve5(normalized === "" || normalized === "y" || normalized === "yes");
|
|
791
791
|
});
|
|
792
792
|
});
|
|
793
793
|
}
|
|
794
794
|
function promptInput(message, defaultValue) {
|
|
795
|
-
return new Promise((
|
|
795
|
+
return new Promise((resolve5) => {
|
|
796
796
|
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
797
797
|
rl.question(`${message} `, (answer) => {
|
|
798
798
|
rl.close();
|
|
799
|
-
|
|
799
|
+
resolve5(answer.trim() || defaultValue);
|
|
800
800
|
});
|
|
801
801
|
});
|
|
802
802
|
}
|
|
@@ -1152,19 +1152,496 @@ function formatResult(result, dryRun) {
|
|
|
1152
1152
|
return lines.join("\n");
|
|
1153
1153
|
}
|
|
1154
1154
|
function readStdin() {
|
|
1155
|
-
return new Promise((
|
|
1155
|
+
return new Promise((resolve5, reject) => {
|
|
1156
1156
|
const chunks = [];
|
|
1157
1157
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
1158
|
-
process.stdin.on("end", () =>
|
|
1158
|
+
process.stdin.on("end", () => resolve5(Buffer.concat(chunks).toString("utf-8")));
|
|
1159
1159
|
process.stdin.on("error", reject);
|
|
1160
1160
|
});
|
|
1161
1161
|
}
|
|
1162
1162
|
|
|
1163
|
+
// src/cli/snapshot.ts
|
|
1164
|
+
var import_node_path3 = require("path");
|
|
1165
|
+
var import_node_fs4 = require("fs");
|
|
1166
|
+
var import_sdk5 = __toESM(require("@eventcatalog/sdk"));
|
|
1167
|
+
var snapshotCreate = async (opts) => {
|
|
1168
|
+
const dir = (0, import_node_path3.resolve)(opts.dir);
|
|
1169
|
+
const sdk = (0, import_sdk5.default)(dir);
|
|
1170
|
+
const result = await sdk.createSnapshot({
|
|
1171
|
+
label: opts.label,
|
|
1172
|
+
outputDir: opts.output ? (0, import_node_path3.resolve)(opts.output) : void 0
|
|
1173
|
+
});
|
|
1174
|
+
if (opts.stdout) {
|
|
1175
|
+
(0, import_node_fs4.rmSync)(result.filePath, { force: true });
|
|
1176
|
+
return JSON.stringify(result.snapshot, null, 2);
|
|
1177
|
+
}
|
|
1178
|
+
const resources = result.snapshot.resources;
|
|
1179
|
+
const counts = [
|
|
1180
|
+
resources.services.length && `${resources.services.length} services`,
|
|
1181
|
+
resources.messages.events.length && `${resources.messages.events.length} events`,
|
|
1182
|
+
resources.messages.commands.length && `${resources.messages.commands.length} commands`,
|
|
1183
|
+
resources.messages.queries.length && `${resources.messages.queries.length} queries`,
|
|
1184
|
+
resources.domains.length && `${resources.domains.length} domains`,
|
|
1185
|
+
resources.channels.length && `${resources.channels.length} channels`
|
|
1186
|
+
].filter(Boolean).join(", ");
|
|
1187
|
+
return `Snapshot created: ${result.filePath}
|
|
1188
|
+
Resources: ${counts}`;
|
|
1189
|
+
};
|
|
1190
|
+
var formatDiffText = (diff) => {
|
|
1191
|
+
const lines = [];
|
|
1192
|
+
const labelA = diff.snapshotA.label;
|
|
1193
|
+
const labelB = diff.snapshotB.label;
|
|
1194
|
+
lines.push(`Comparing: ${labelA} vs ${labelB}`);
|
|
1195
|
+
lines.push("");
|
|
1196
|
+
if (diff.resources.length > 0) {
|
|
1197
|
+
lines.push(`Resources (${diff.resources.length} changes):`);
|
|
1198
|
+
for (const r of diff.resources) {
|
|
1199
|
+
const prefix = r.changeType === "added" ? "+" : r.changeType === "removed" ? "-" : r.changeType === "versioned" ? "^" : "~";
|
|
1200
|
+
const version2 = r.changeType === "versioned" ? `${r.previousVersion} -> ${r.newVersion}` : r.version;
|
|
1201
|
+
const fields = r.changedFields ? ` (${r.changedFields.join(", ")})` : "";
|
|
1202
|
+
lines.push(` ${prefix} ${r.resourceId}@${version2} [${r.type}] ${r.changeType}${fields}`);
|
|
1203
|
+
}
|
|
1204
|
+
lines.push("");
|
|
1205
|
+
}
|
|
1206
|
+
if (diff.relationships.length > 0) {
|
|
1207
|
+
lines.push(`Relationships (${diff.relationships.length} changes):`);
|
|
1208
|
+
for (const r of diff.relationships) {
|
|
1209
|
+
const prefix = r.changeType === "added" ? "+" : "-";
|
|
1210
|
+
lines.push(
|
|
1211
|
+
` ${prefix} ${r.serviceId} --${r.direction}--> ${r.resourceId}${r.resourceVersion ? `@${r.resourceVersion}` : ""}`
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
lines.push("");
|
|
1215
|
+
}
|
|
1216
|
+
if (diff.summary.totalChanges === 0) {
|
|
1217
|
+
lines.push("No changes detected.");
|
|
1218
|
+
} else {
|
|
1219
|
+
lines.push(`Summary: ${diff.resources.length} resource changes, ${diff.relationships.length} relationship changes`);
|
|
1220
|
+
}
|
|
1221
|
+
return lines.join("\n");
|
|
1222
|
+
};
|
|
1223
|
+
var snapshotDiff = async (opts) => {
|
|
1224
|
+
const dir = (0, import_node_path3.resolve)(opts.dir);
|
|
1225
|
+
const sdk = (0, import_sdk5.default)(dir);
|
|
1226
|
+
const diff = await sdk.diffSnapshots((0, import_node_path3.resolve)(opts.fileA), (0, import_node_path3.resolve)(opts.fileB));
|
|
1227
|
+
if (opts.format === "json") {
|
|
1228
|
+
return JSON.stringify(diff, null, 2);
|
|
1229
|
+
}
|
|
1230
|
+
return formatDiffText(diff);
|
|
1231
|
+
};
|
|
1232
|
+
var snapshotList = async (opts) => {
|
|
1233
|
+
const dir = (0, import_node_path3.resolve)(opts.dir);
|
|
1234
|
+
const sdk = (0, import_sdk5.default)(dir);
|
|
1235
|
+
const snapshots = await sdk.listSnapshots();
|
|
1236
|
+
if (opts.format === "json") {
|
|
1237
|
+
return JSON.stringify(snapshots, null, 2);
|
|
1238
|
+
}
|
|
1239
|
+
if (snapshots.length === 0) {
|
|
1240
|
+
return "No snapshots found.";
|
|
1241
|
+
}
|
|
1242
|
+
const lines = ["Snapshots:", ""];
|
|
1243
|
+
for (const s of snapshots) {
|
|
1244
|
+
const git = s.git ? ` (${s.git.branch} ${s.git.commit})` : "";
|
|
1245
|
+
lines.push(` ${s.label} ${s.createdAt}${git}`);
|
|
1246
|
+
lines.push(` ${s.filePath}`);
|
|
1247
|
+
}
|
|
1248
|
+
return lines.join("\n");
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
// src/cli/governance/rules.ts
|
|
1252
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
1253
|
+
var import_node_path4 = __toESM(require("path"));
|
|
1254
|
+
var import_js_yaml = __toESM(require("js-yaml"));
|
|
1255
|
+
var loadGovernanceConfig = (catalogDir) => {
|
|
1256
|
+
const yamlPath = import_node_path4.default.join(catalogDir, "governance.yaml");
|
|
1257
|
+
const ymlPath = import_node_path4.default.join(catalogDir, "governance.yml");
|
|
1258
|
+
const configPath = import_node_fs5.default.existsSync(yamlPath) ? yamlPath : import_node_fs5.default.existsSync(ymlPath) ? ymlPath : null;
|
|
1259
|
+
if (!configPath) {
|
|
1260
|
+
return { rules: [] };
|
|
1261
|
+
}
|
|
1262
|
+
const content = import_node_fs5.default.readFileSync(configPath, "utf-8");
|
|
1263
|
+
const parsed = import_js_yaml.default.load(content);
|
|
1264
|
+
return { rules: parsed?.rules || [] };
|
|
1265
|
+
};
|
|
1266
|
+
var TRIGGER_FILTERS = {
|
|
1267
|
+
consumer_added: (c2) => c2.direction === "receives" && c2.changeType === "added",
|
|
1268
|
+
consumer_removed: (c2) => c2.direction === "receives" && c2.changeType === "removed",
|
|
1269
|
+
producer_added: (c2) => c2.direction === "sends" && c2.changeType === "added",
|
|
1270
|
+
producer_removed: (c2) => c2.direction === "sends" && c2.changeType === "removed"
|
|
1271
|
+
};
|
|
1272
|
+
var buildServiceMessageSets = (snapshot2) => {
|
|
1273
|
+
const produces = /* @__PURE__ */ new Map();
|
|
1274
|
+
const consumes = /* @__PURE__ */ new Map();
|
|
1275
|
+
for (const service of snapshot2.resources.services) {
|
|
1276
|
+
const serviceId = service.id;
|
|
1277
|
+
if (service.sends) {
|
|
1278
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1279
|
+
for (const s of service.sends) ids.add(s.id);
|
|
1280
|
+
produces.set(serviceId, ids);
|
|
1281
|
+
}
|
|
1282
|
+
if (service.receives) {
|
|
1283
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1284
|
+
for (const r of service.receives) ids.add(r.id);
|
|
1285
|
+
consumes.set(serviceId, ids);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return { produces, consumes };
|
|
1289
|
+
};
|
|
1290
|
+
var matchesResourceId = (resourceId, serviceId, resources, messageSets) => {
|
|
1291
|
+
return resources.some((r) => {
|
|
1292
|
+
if (r === "*") return true;
|
|
1293
|
+
if (r.startsWith("service:")) {
|
|
1294
|
+
if (serviceId) return serviceId === r.slice(8);
|
|
1295
|
+
return messageSets?.produces.get(r.slice(8))?.has(resourceId) ?? false;
|
|
1296
|
+
}
|
|
1297
|
+
if (r.startsWith("message:")) return resourceId === r.slice(8);
|
|
1298
|
+
if (r.startsWith("produces:")) return messageSets?.produces.get(r.slice(9))?.has(resourceId) ?? false;
|
|
1299
|
+
if (r.startsWith("consumes:")) return messageSets?.consumes.get(r.slice(9))?.has(resourceId) ?? false;
|
|
1300
|
+
return false;
|
|
1301
|
+
});
|
|
1302
|
+
};
|
|
1303
|
+
var REMOVED_TRIGGERS = /* @__PURE__ */ new Set(["consumer_removed", "producer_removed"]);
|
|
1304
|
+
var MESSAGE_RESOURCE_TYPES = /* @__PURE__ */ new Set(["event", "command", "query"]);
|
|
1305
|
+
var buildMessageMap = (snapshot2) => {
|
|
1306
|
+
const map = /* @__PURE__ */ new Map();
|
|
1307
|
+
for (const msg of snapshot2.resources.messages.events) map.set(msg.id, msg);
|
|
1308
|
+
for (const msg of snapshot2.resources.messages.commands) map.set(msg.id, msg);
|
|
1309
|
+
for (const msg of snapshot2.resources.messages.queries) map.set(msg.id, msg);
|
|
1310
|
+
return map;
|
|
1311
|
+
};
|
|
1312
|
+
var buildProducerIndex = (snapshot2) => {
|
|
1313
|
+
const index = /* @__PURE__ */ new Map();
|
|
1314
|
+
for (const service of snapshot2.resources.services) {
|
|
1315
|
+
if (!service.sends) continue;
|
|
1316
|
+
for (const s of service.sends) {
|
|
1317
|
+
let producers = index.get(s.id);
|
|
1318
|
+
if (!producers) {
|
|
1319
|
+
producers = [];
|
|
1320
|
+
index.set(s.id, producers);
|
|
1321
|
+
}
|
|
1322
|
+
const entry = {
|
|
1323
|
+
id: service.id,
|
|
1324
|
+
version: service.version
|
|
1325
|
+
};
|
|
1326
|
+
if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) {
|
|
1327
|
+
entry.owners = service.owners;
|
|
1328
|
+
}
|
|
1329
|
+
producers.push(entry);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return index;
|
|
1333
|
+
};
|
|
1334
|
+
var evaluateDeprecationRules = (diff, config, targetSnapshot, targetMessageSets, baseSnapshot) => {
|
|
1335
|
+
const deprecationRules = config.rules.filter((rule) => rule.when.includes("message_deprecated"));
|
|
1336
|
+
if (deprecationRules.length === 0) return [];
|
|
1337
|
+
const targetMessages = buildMessageMap(targetSnapshot);
|
|
1338
|
+
const baseMessages = baseSnapshot ? buildMessageMap(baseSnapshot) : void 0;
|
|
1339
|
+
const producerIndex = buildProducerIndex(targetSnapshot);
|
|
1340
|
+
const deprecatedResources = diff.resources.filter((rc) => {
|
|
1341
|
+
if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false;
|
|
1342
|
+
if (!rc.changedFields?.includes("deprecated")) return false;
|
|
1343
|
+
const targetMessage = targetMessages.get(rc.resourceId);
|
|
1344
|
+
if (!targetMessage || !targetMessage.deprecated) return false;
|
|
1345
|
+
if (baseMessages) {
|
|
1346
|
+
const baseMessage = baseMessages.get(rc.resourceId);
|
|
1347
|
+
if (baseMessage && baseMessage.deprecated) return false;
|
|
1348
|
+
}
|
|
1349
|
+
return true;
|
|
1350
|
+
});
|
|
1351
|
+
if (deprecatedResources.length === 0) return [];
|
|
1352
|
+
const results = [];
|
|
1353
|
+
for (const rule of deprecationRules) {
|
|
1354
|
+
const matched = [];
|
|
1355
|
+
for (const rc of deprecatedResources) {
|
|
1356
|
+
if (!matchesResourceId(rc.resourceId, void 0, rule.resources, targetMessageSets)) continue;
|
|
1357
|
+
const producers = producerIndex.get(rc.resourceId) || [];
|
|
1358
|
+
matched.push({ resourceChange: rc, producerServices: producers });
|
|
1359
|
+
}
|
|
1360
|
+
if (matched.length > 0) {
|
|
1361
|
+
results.push({ rule, trigger: "message_deprecated", matchedChanges: [], deprecationChanges: matched });
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return results;
|
|
1365
|
+
};
|
|
1366
|
+
var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
|
|
1367
|
+
const results = [];
|
|
1368
|
+
const targetMessageSets = targetSnapshot ? buildServiceMessageSets(targetSnapshot) : void 0;
|
|
1369
|
+
const baseMessageSets = baseSnapshot ? buildServiceMessageSets(baseSnapshot) : void 0;
|
|
1370
|
+
for (const rule of config.rules) {
|
|
1371
|
+
for (const trigger of rule.when) {
|
|
1372
|
+
const filter = TRIGGER_FILTERS[trigger];
|
|
1373
|
+
if (!filter) continue;
|
|
1374
|
+
const messageSets = REMOVED_TRIGGERS.has(trigger) && baseMessageSets ? baseMessageSets : targetMessageSets;
|
|
1375
|
+
const matchedChanges = diff.relationships.filter(
|
|
1376
|
+
(c2) => filter(c2) && matchesResourceId(c2.resourceId, c2.serviceId, rule.resources, messageSets)
|
|
1377
|
+
);
|
|
1378
|
+
if (matchedChanges.length > 0) {
|
|
1379
|
+
results.push({ rule, trigger, matchedChanges });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (targetSnapshot && targetMessageSets) {
|
|
1384
|
+
results.push(...evaluateDeprecationRules(diff, config, targetSnapshot, targetMessageSets, baseSnapshot));
|
|
1385
|
+
}
|
|
1386
|
+
return results;
|
|
1387
|
+
};
|
|
1388
|
+
var PRODUCER_TRIGGERS = /* @__PURE__ */ new Set(["producer_added", "producer_removed"]);
|
|
1389
|
+
var isProducerTrigger = (trigger) => PRODUCER_TRIGGERS.has(trigger);
|
|
1390
|
+
var getChangeVerb = (trigger, changeType) => {
|
|
1391
|
+
const producer = isProducerTrigger(trigger);
|
|
1392
|
+
return changeType === "added" ? producer ? "now producing" : "now consuming" : producer ? "no longer producing" : "no longer consuming";
|
|
1393
|
+
};
|
|
1394
|
+
var resolveEnvVars = (value) => {
|
|
1395
|
+
return value.replace(/\$([A-Z_][A-Z0-9_]*)/g, (match, varName) => {
|
|
1396
|
+
const envValue = process.env[varName];
|
|
1397
|
+
if (envValue === void 0) {
|
|
1398
|
+
throw new Error(`Environment variable ${varName} is not set`);
|
|
1399
|
+
}
|
|
1400
|
+
return envValue;
|
|
1401
|
+
});
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
// src/cli/governance/actions.ts
|
|
1405
|
+
var import_node_crypto2 = require("crypto");
|
|
1406
|
+
var buildMessageTypeMap = (snapshot2) => {
|
|
1407
|
+
const map = /* @__PURE__ */ new Map();
|
|
1408
|
+
for (const event of snapshot2.resources.messages.events) {
|
|
1409
|
+
map.set(event.id, "event");
|
|
1410
|
+
}
|
|
1411
|
+
for (const command of snapshot2.resources.messages.commands) {
|
|
1412
|
+
map.set(command.id, "command");
|
|
1413
|
+
}
|
|
1414
|
+
for (const query of snapshot2.resources.messages.queries) {
|
|
1415
|
+
map.set(query.id, "query");
|
|
1416
|
+
}
|
|
1417
|
+
return map;
|
|
1418
|
+
};
|
|
1419
|
+
var buildServiceOwnersMap = (snapshot2) => {
|
|
1420
|
+
const map = /* @__PURE__ */ new Map();
|
|
1421
|
+
for (const service of snapshot2.resources.services) {
|
|
1422
|
+
if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) {
|
|
1423
|
+
map.set(service.id, service.owners);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
return map;
|
|
1427
|
+
};
|
|
1428
|
+
var executeGovernanceActions = async (results, opts = {}) => {
|
|
1429
|
+
const { messageTypes, status, serviceOwners } = opts;
|
|
1430
|
+
const webhookCalls = [];
|
|
1431
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1432
|
+
for (const result of results) {
|
|
1433
|
+
for (const action of result.rule.actions) {
|
|
1434
|
+
if (action.type !== "webhook") continue;
|
|
1435
|
+
const url = resolveEnvVars(action.url);
|
|
1436
|
+
const headers = { "Content-Type": "application/json" };
|
|
1437
|
+
if (action.headers) {
|
|
1438
|
+
for (const [key, value] of Object.entries(action.headers)) {
|
|
1439
|
+
headers[key] = resolveEnvVars(value);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (result.deprecationChanges && result.deprecationChanges.length > 0) {
|
|
1443
|
+
for (const dc of result.deprecationChanges) {
|
|
1444
|
+
const messageType = messageTypes?.get(dc.resourceChange.resourceId) || "message";
|
|
1445
|
+
const producers = dc.producerServices.length > 0 ? dc.producerServices : [{ id: "unknown", version: "unknown" }];
|
|
1446
|
+
for (const producer of producers) {
|
|
1447
|
+
const payload = {
|
|
1448
|
+
specversion: "1.0",
|
|
1449
|
+
type: `eventcatalog.governance.message_deprecated`,
|
|
1450
|
+
source: "eventcatalog/governance",
|
|
1451
|
+
id: (0, import_node_crypto2.randomUUID)(),
|
|
1452
|
+
time: now,
|
|
1453
|
+
datacontenttype: "application/json",
|
|
1454
|
+
data: {
|
|
1455
|
+
schemaVersion: 1,
|
|
1456
|
+
...status && { status },
|
|
1457
|
+
summary: `${dc.resourceChange.resourceId} (${messageType}) has been deprecated by ${producer.id}`,
|
|
1458
|
+
producer: {
|
|
1459
|
+
id: producer.id,
|
|
1460
|
+
version: producer.version,
|
|
1461
|
+
...producer.owners && { owners: producer.owners }
|
|
1462
|
+
},
|
|
1463
|
+
message: {
|
|
1464
|
+
id: dc.resourceChange.resourceId,
|
|
1465
|
+
version: dc.resourceChange.version,
|
|
1466
|
+
type: messageType
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
webhookCalls.push({
|
|
1471
|
+
urlTemplate: action.url,
|
|
1472
|
+
request: fetch(url, { method: "POST", headers, body: JSON.stringify(payload) })
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
for (const change of result.matchedChanges) {
|
|
1479
|
+
const verb = getChangeVerb(result.trigger, change.changeType);
|
|
1480
|
+
const messageType = messageTypes?.get(change.resourceId) || "message";
|
|
1481
|
+
const serviceRole = isProducerTrigger(result.trigger) ? "producer" : "consumer";
|
|
1482
|
+
const payload = {
|
|
1483
|
+
specversion: "1.0",
|
|
1484
|
+
type: `eventcatalog.governance.${result.trigger}`,
|
|
1485
|
+
source: "eventcatalog/governance",
|
|
1486
|
+
id: (0, import_node_crypto2.randomUUID)(),
|
|
1487
|
+
time: now,
|
|
1488
|
+
datacontenttype: "application/json",
|
|
1489
|
+
data: {
|
|
1490
|
+
schemaVersion: 1,
|
|
1491
|
+
...status && { status },
|
|
1492
|
+
summary: `${change.serviceId} is ${verb} the ${messageType} ${change.resourceId}`,
|
|
1493
|
+
[serviceRole]: {
|
|
1494
|
+
id: change.serviceId,
|
|
1495
|
+
version: change.serviceVersion,
|
|
1496
|
+
...serviceOwners?.get(change.serviceId) && { owners: serviceOwners.get(change.serviceId) }
|
|
1497
|
+
},
|
|
1498
|
+
message: {
|
|
1499
|
+
id: change.resourceId,
|
|
1500
|
+
version: change.resourceVersion,
|
|
1501
|
+
type: messageType
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
webhookCalls.push({
|
|
1506
|
+
urlTemplate: action.url,
|
|
1507
|
+
request: fetch(url, { method: "POST", headers, body: JSON.stringify(payload) })
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
const settled = await Promise.allSettled(webhookCalls.map((c2) => c2.request));
|
|
1513
|
+
return settled.map((result, i) => {
|
|
1514
|
+
const url = webhookCalls[i].urlTemplate;
|
|
1515
|
+
if (result.status === "fulfilled") {
|
|
1516
|
+
const res = result.value;
|
|
1517
|
+
if (!res.ok) {
|
|
1518
|
+
return ` Webhook failed: ${url} \u2717 (HTTP ${res.status})`;
|
|
1519
|
+
}
|
|
1520
|
+
return ` Webhook sent: ${url} \u2713`;
|
|
1521
|
+
}
|
|
1522
|
+
return ` Webhook failed: ${url} \u2717 (${result.reason instanceof Error ? result.reason.message : String(result.reason)})`;
|
|
1523
|
+
});
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
// src/cli/governance/format.ts
|
|
1527
|
+
var formatGovernanceOutput = (results) => {
|
|
1528
|
+
if (results.length === 0) {
|
|
1529
|
+
return "No governance rules triggered. Catalog is compliant.";
|
|
1530
|
+
}
|
|
1531
|
+
const lines = ["Governance:", ""];
|
|
1532
|
+
for (const result of results) {
|
|
1533
|
+
lines.push(` Rule "${result.rule.name}" triggered (${result.trigger}):`);
|
|
1534
|
+
if (result.deprecationChanges && result.deprecationChanges.length > 0) {
|
|
1535
|
+
for (const dc of result.deprecationChanges) {
|
|
1536
|
+
const producers = dc.producerServices.length > 0 ? dc.producerServices.map((p) => p.id).join(", ") : "unknown producer";
|
|
1537
|
+
lines.push(` ! ${dc.resourceChange.resourceId} (${dc.resourceChange.type}) deprecated by ${producers}`);
|
|
1538
|
+
}
|
|
1539
|
+
} else {
|
|
1540
|
+
for (const change of result.matchedChanges) {
|
|
1541
|
+
const prefix = change.changeType === "added" ? "+" : "-";
|
|
1542
|
+
const verb = getChangeVerb(result.trigger, change.changeType);
|
|
1543
|
+
lines.push(` ${prefix} ${change.serviceId} is ${verb} ${change.resourceId}`);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
lines.push("");
|
|
1547
|
+
}
|
|
1548
|
+
return lines.join("\n");
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
// src/cli/governance/check.ts
|
|
1552
|
+
var import_node_path5 = __toESM(require("path"));
|
|
1553
|
+
var import_node_child_process = require("child_process");
|
|
1554
|
+
var import_node_fs6 = require("fs");
|
|
1555
|
+
var import_node_os = require("os");
|
|
1556
|
+
var import_dotenv = __toESM(require("dotenv"));
|
|
1557
|
+
var import_sdk6 = __toESM(require("@eventcatalog/sdk"));
|
|
1558
|
+
var import_license = require("@eventcatalog/license");
|
|
1559
|
+
var BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/;
|
|
1560
|
+
var extractBranchToTempDir = (branch, catalogDir, tempDirs) => {
|
|
1561
|
+
if (!BRANCH_NAME_RE.test(branch)) {
|
|
1562
|
+
throw new Error(`Invalid branch name: "${branch}"`);
|
|
1563
|
+
}
|
|
1564
|
+
const tmpDir = (0, import_node_fs6.mkdtempSync)(import_node_path5.default.join((0, import_node_os.tmpdir)(), "ec-governance-"));
|
|
1565
|
+
tempDirs.push(tmpDir);
|
|
1566
|
+
try {
|
|
1567
|
+
(0, import_node_child_process.execSync)(`git archive ${branch} | tar -x -C ${tmpDir}`, { cwd: catalogDir, stdio: "pipe" });
|
|
1568
|
+
} catch {
|
|
1569
|
+
throw new Error(`Failed to extract branch "${branch}". Is it a valid git branch?`);
|
|
1570
|
+
}
|
|
1571
|
+
return tmpDir;
|
|
1572
|
+
};
|
|
1573
|
+
var governanceCheck = async (opts) => {
|
|
1574
|
+
const dir = import_node_path5.default.resolve(opts.dir);
|
|
1575
|
+
import_dotenv.default.config({ path: import_node_path5.default.join(dir, ".env") });
|
|
1576
|
+
const isScale = await (0, import_license.isEventCatalogScaleEnabled)();
|
|
1577
|
+
if (!isScale) {
|
|
1578
|
+
throw new Error("Governance requires an EventCatalog Scale plan. Learn more at https://eventcatalog.dev/pricing");
|
|
1579
|
+
}
|
|
1580
|
+
const baseBranch = opts.base || "main";
|
|
1581
|
+
const tempDirs = [];
|
|
1582
|
+
const trackTempDir = (prefix) => {
|
|
1583
|
+
const d = (0, import_node_fs6.mkdtempSync)(import_node_path5.default.join((0, import_node_os.tmpdir)(), prefix));
|
|
1584
|
+
tempDirs.push(d);
|
|
1585
|
+
return d;
|
|
1586
|
+
};
|
|
1587
|
+
try {
|
|
1588
|
+
const baseTmpDir = extractBranchToTempDir(baseBranch, dir, tempDirs);
|
|
1589
|
+
const baseSnapshotDir = trackTempDir("ec-snap-base-");
|
|
1590
|
+
const targetSnapshotDir = trackTempDir("ec-snap-target-");
|
|
1591
|
+
const baseSDK = (0, import_sdk6.default)(baseTmpDir);
|
|
1592
|
+
const baseResult = await baseSDK.createSnapshot({ label: `base-${baseBranch}`, outputDir: baseSnapshotDir });
|
|
1593
|
+
let targetResult;
|
|
1594
|
+
if (opts.target) {
|
|
1595
|
+
const targetTmpDir = extractBranchToTempDir(opts.target, dir, tempDirs);
|
|
1596
|
+
const targetSDK = (0, import_sdk6.default)(targetTmpDir);
|
|
1597
|
+
targetResult = await targetSDK.createSnapshot({ label: `target-${opts.target}`, outputDir: targetSnapshotDir });
|
|
1598
|
+
} else {
|
|
1599
|
+
const targetSDK = (0, import_sdk6.default)(dir);
|
|
1600
|
+
targetResult = await targetSDK.createSnapshot({ label: "current", outputDir: targetSnapshotDir });
|
|
1601
|
+
}
|
|
1602
|
+
const diff = await baseSDK.diffSnapshots(baseResult.filePath, targetResult.filePath);
|
|
1603
|
+
const config = loadGovernanceConfig(dir);
|
|
1604
|
+
if (config.rules.length === 0) {
|
|
1605
|
+
return "No governance.yaml (or governance.yml) found or no rules defined.";
|
|
1606
|
+
}
|
|
1607
|
+
const results = evaluateGovernanceRules(diff, config, targetResult.snapshot, baseResult.snapshot);
|
|
1608
|
+
const messageTypes = buildMessageTypeMap(targetResult.snapshot);
|
|
1609
|
+
const serviceOwners = buildServiceOwnersMap(targetResult.snapshot);
|
|
1610
|
+
const actionOutput = await executeGovernanceActions(results, {
|
|
1611
|
+
messageTypes,
|
|
1612
|
+
status: opts.status,
|
|
1613
|
+
serviceOwners
|
|
1614
|
+
});
|
|
1615
|
+
if (opts.format === "json") {
|
|
1616
|
+
return JSON.stringify({ baseBranch, target: opts.target || "working directory", results, diff: diff.summary }, null, 2);
|
|
1617
|
+
}
|
|
1618
|
+
const targetLabel = opts.target || "working directory";
|
|
1619
|
+
const lines = [`Governance check: comparing ${targetLabel} against ${baseBranch}`, ""];
|
|
1620
|
+
lines.push(formatGovernanceOutput(results));
|
|
1621
|
+
if (actionOutput.length > 0) {
|
|
1622
|
+
lines.push("");
|
|
1623
|
+
lines.push(...actionOutput);
|
|
1624
|
+
}
|
|
1625
|
+
if (results.length > 0) {
|
|
1626
|
+
const webhookCount = actionOutput.filter((l) => l.includes("Webhook sent")).length;
|
|
1627
|
+
const parts = [`${results.length} rule${results.length === 1 ? "" : "s"} triggered`];
|
|
1628
|
+
if (webhookCount > 0) parts.push(`${webhookCount} webhook${webhookCount === 1 ? "" : "s"} sent`);
|
|
1629
|
+
lines.push("");
|
|
1630
|
+
lines.push(parts.join(", ") + ".");
|
|
1631
|
+
}
|
|
1632
|
+
return lines.join("\n");
|
|
1633
|
+
} finally {
|
|
1634
|
+
for (const d of tempDirs) {
|
|
1635
|
+
(0, import_node_fs6.rmSync)(d, { recursive: true, force: true });
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1163
1640
|
// src/cli/index.ts
|
|
1164
1641
|
var version = "1.0.0";
|
|
1165
1642
|
try {
|
|
1166
|
-
const packageJsonPath = (0,
|
|
1167
|
-
const packageJson = JSON.parse((0,
|
|
1643
|
+
const packageJsonPath = (0, import_node_path6.resolve)(__dirname, "../../package.json");
|
|
1644
|
+
const packageJson = JSON.parse((0, import_node_fs7.readFileSync)(packageJsonPath, "utf-8"));
|
|
1168
1645
|
version = packageJson.version;
|
|
1169
1646
|
} catch {
|
|
1170
1647
|
}
|
|
@@ -1232,6 +1709,58 @@ import_commander.program.command("import [files...]").description("Import EventC
|
|
|
1232
1709
|
process.exit(1);
|
|
1233
1710
|
}
|
|
1234
1711
|
});
|
|
1712
|
+
var snapshot = import_commander.program.command("snapshot").description("Create, diff, and list catalog snapshots");
|
|
1713
|
+
snapshot.command("create").description("Take a point-in-time snapshot of the catalog").option("--label <label>", "Human-readable label (default: ISO timestamp)").option("-o, --output <path>", "Output directory for the snapshot file").option("--stdout", "Print JSON to stdout instead of writing a file", false).action(async (opts) => {
|
|
1714
|
+
try {
|
|
1715
|
+
const globalOpts = import_commander.program.opts();
|
|
1716
|
+
const dir = globalOpts.dir || ".";
|
|
1717
|
+
const result = await snapshotCreate({ label: opts.label, output: opts.output, stdout: opts.stdout, dir });
|
|
1718
|
+
console.log(result);
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1721
|
+
process.exit(1);
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
snapshot.command("diff <fileA> <fileB>").description("Compare two snapshot files and output a structured diff").option("--format <format>", "Output format: text or json", "text").action(async (fileA, fileB, opts) => {
|
|
1725
|
+
try {
|
|
1726
|
+
const globalOpts = import_commander.program.opts();
|
|
1727
|
+
const dir = globalOpts.dir || ".";
|
|
1728
|
+
const result = await snapshotDiff({ fileA, fileB, format: opts.format, dir });
|
|
1729
|
+
console.log(result);
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1732
|
+
process.exit(1);
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
snapshot.command("list").description("List all snapshots in the catalog").option("--format <format>", "Output format: text or json", "text").action(async (opts) => {
|
|
1736
|
+
try {
|
|
1737
|
+
const globalOpts = import_commander.program.opts();
|
|
1738
|
+
const dir = globalOpts.dir || ".";
|
|
1739
|
+
const result = await snapshotList({ format: opts.format, dir });
|
|
1740
|
+
console.log(result);
|
|
1741
|
+
} catch (error) {
|
|
1742
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1743
|
+
process.exit(1);
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
var governance = import_commander.program.command("governance").description("Run governance rules against catalog changes");
|
|
1747
|
+
governance.command("check").description("Compare catalog against a base branch and evaluate governance rules").option("--base <branch>", "Base branch to compare against (default: main)").option("--target <branch>", "Target branch to compare (default: current working directory)").option("--format <format>", "Output format: text or json", "text").option("--status <status>", "Status label to include in webhook payloads (e.g. proposed, approved)").action(async (opts) => {
|
|
1748
|
+
try {
|
|
1749
|
+
const globalOpts = import_commander.program.opts();
|
|
1750
|
+
const dir = globalOpts.dir || ".";
|
|
1751
|
+
const result = await governanceCheck({
|
|
1752
|
+
base: opts.base,
|
|
1753
|
+
target: opts.target,
|
|
1754
|
+
format: opts.format,
|
|
1755
|
+
status: opts.status,
|
|
1756
|
+
dir
|
|
1757
|
+
});
|
|
1758
|
+
console.log(result);
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1761
|
+
process.exit(1);
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1235
1764
|
import_commander.program.arguments("<function> [args...]").action(async (functionName, args) => {
|
|
1236
1765
|
try {
|
|
1237
1766
|
const options = import_commander.program.opts();
|