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