@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 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 import_node_fs4 = require("fs");
29
- var import_node_path3 = require("path");
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(path) {
501
- const segments = path.split("/");
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 path = buildServiceOutputPath(def.name, body, nested, parentPath);
694
- refsByPath.set(path, {
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((resolve4) => {
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
- resolve4(normalized === "" || normalized === "y" || normalized === "yes");
790
+ resolve5(normalized === "" || normalized === "y" || normalized === "yes");
791
791
  });
792
792
  });
793
793
  }
794
794
  function promptInput(message, defaultValue) {
795
- return new Promise((resolve4) => {
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
- resolve4(answer.trim() || defaultValue);
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((resolve4, reject) => {
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", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
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, import_node_path3.resolve)(__dirname, "../../package.json");
1167
- const packageJson = JSON.parse((0, import_node_fs4.readFileSync)(packageJsonPath, "utf-8"));
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();