@eventcatalog/cli 0.4.10 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/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,381 @@ 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 configPath = import_node_path4.default.join(catalogDir, "governance.yaml");
1257
+ if (!import_node_fs5.default.existsSync(configPath)) {
1258
+ return { rules: [] };
1259
+ }
1260
+ const content = import_node_fs5.default.readFileSync(configPath, "utf-8");
1261
+ const parsed = import_js_yaml.default.load(content);
1262
+ return { rules: parsed?.rules || [] };
1263
+ };
1264
+ var TRIGGER_FILTERS = {
1265
+ consumer_added: (c2) => c2.direction === "receives" && c2.changeType === "added",
1266
+ consumer_removed: (c2) => c2.direction === "receives" && c2.changeType === "removed",
1267
+ producer_added: (c2) => c2.direction === "sends" && c2.changeType === "added",
1268
+ producer_removed: (c2) => c2.direction === "sends" && c2.changeType === "removed"
1269
+ };
1270
+ var buildServiceMessageSets = (snapshot2) => {
1271
+ const produces = /* @__PURE__ */ new Map();
1272
+ const consumes = /* @__PURE__ */ new Map();
1273
+ for (const service of snapshot2.resources.services) {
1274
+ const serviceId = service.id;
1275
+ if (service.sends) {
1276
+ const ids = /* @__PURE__ */ new Set();
1277
+ for (const s of service.sends) ids.add(s.id);
1278
+ produces.set(serviceId, ids);
1279
+ }
1280
+ if (service.receives) {
1281
+ const ids = /* @__PURE__ */ new Set();
1282
+ for (const r of service.receives) ids.add(r.id);
1283
+ consumes.set(serviceId, ids);
1284
+ }
1285
+ }
1286
+ return { produces, consumes };
1287
+ };
1288
+ var matchesResource = (change, resources, messageSets) => {
1289
+ return resources.some((r) => {
1290
+ if (r === "*") return true;
1291
+ if (r.startsWith("service:")) return change.serviceId === r.slice(8);
1292
+ if (r.startsWith("message:")) return change.resourceId === r.slice(8);
1293
+ if (r.startsWith("produces:")) return messageSets?.produces.get(r.slice(9))?.has(change.resourceId) ?? false;
1294
+ if (r.startsWith("consumes:")) return messageSets?.consumes.get(r.slice(9))?.has(change.resourceId) ?? false;
1295
+ return false;
1296
+ });
1297
+ };
1298
+ var REMOVED_TRIGGERS = /* @__PURE__ */ new Set(["consumer_removed", "producer_removed"]);
1299
+ var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
1300
+ const results = [];
1301
+ const targetMessageSets = targetSnapshot ? buildServiceMessageSets(targetSnapshot) : void 0;
1302
+ const baseMessageSets = baseSnapshot ? buildServiceMessageSets(baseSnapshot) : void 0;
1303
+ for (const rule of config.rules) {
1304
+ for (const trigger of rule.when) {
1305
+ const filter = TRIGGER_FILTERS[trigger];
1306
+ if (!filter) continue;
1307
+ const messageSets = REMOVED_TRIGGERS.has(trigger) && baseMessageSets ? baseMessageSets : targetMessageSets;
1308
+ const matchedChanges = diff.relationships.filter((c2) => filter(c2) && matchesResource(c2, rule.resources, messageSets));
1309
+ if (matchedChanges.length > 0) {
1310
+ results.push({ rule, trigger, matchedChanges });
1311
+ }
1312
+ }
1313
+ }
1314
+ return results;
1315
+ };
1316
+ var PRODUCER_TRIGGERS = /* @__PURE__ */ new Set(["producer_added", "producer_removed"]);
1317
+ var isProducerTrigger = (trigger) => PRODUCER_TRIGGERS.has(trigger);
1318
+ var getChangeVerb = (trigger, changeType) => {
1319
+ const producer = isProducerTrigger(trigger);
1320
+ return changeType === "added" ? producer ? "now producing" : "now consuming" : producer ? "no longer producing" : "no longer consuming";
1321
+ };
1322
+ var resolveEnvVars = (value) => {
1323
+ return value.replace(/\$([A-Z_][A-Z0-9_]*)/g, (match, varName) => {
1324
+ const envValue = process.env[varName];
1325
+ if (envValue === void 0) {
1326
+ throw new Error(`Environment variable ${varName} is not set`);
1327
+ }
1328
+ return envValue;
1329
+ });
1330
+ };
1331
+
1332
+ // src/cli/governance/actions.ts
1333
+ var import_node_crypto2 = require("crypto");
1334
+ var buildMessageTypeMap = (snapshot2) => {
1335
+ const map = /* @__PURE__ */ new Map();
1336
+ for (const event of snapshot2.resources.messages.events) {
1337
+ map.set(event.id, "event");
1338
+ }
1339
+ for (const command of snapshot2.resources.messages.commands) {
1340
+ map.set(command.id, "command");
1341
+ }
1342
+ for (const query of snapshot2.resources.messages.queries) {
1343
+ map.set(query.id, "query");
1344
+ }
1345
+ return map;
1346
+ };
1347
+ var buildServiceOwnersMap = (snapshot2) => {
1348
+ const map = /* @__PURE__ */ new Map();
1349
+ for (const service of snapshot2.resources.services) {
1350
+ if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) {
1351
+ map.set(service.id, service.owners);
1352
+ }
1353
+ }
1354
+ return map;
1355
+ };
1356
+ var executeGovernanceActions = async (results, opts = {}) => {
1357
+ const { messageTypes, status, serviceOwners } = opts;
1358
+ const webhookCalls = [];
1359
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1360
+ for (const result of results) {
1361
+ for (const action of result.rule.actions) {
1362
+ if (action.type !== "webhook") continue;
1363
+ const url = resolveEnvVars(action.url);
1364
+ const headers = { "Content-Type": "application/json" };
1365
+ if (action.headers) {
1366
+ for (const [key, value] of Object.entries(action.headers)) {
1367
+ headers[key] = resolveEnvVars(value);
1368
+ }
1369
+ }
1370
+ for (const change of result.matchedChanges) {
1371
+ const verb = getChangeVerb(result.trigger, change.changeType);
1372
+ const messageType = messageTypes?.get(change.resourceId) || "message";
1373
+ const serviceRole = isProducerTrigger(result.trigger) ? "producer" : "consumer";
1374
+ const payload = {
1375
+ specversion: "1.0",
1376
+ type: `eventcatalog.governance.${result.trigger}`,
1377
+ source: "eventcatalog/governance",
1378
+ id: (0, import_node_crypto2.randomUUID)(),
1379
+ time: now,
1380
+ datacontenttype: "application/json",
1381
+ data: {
1382
+ schemaVersion: 1,
1383
+ ...status && { status },
1384
+ summary: `${change.serviceId} is ${verb} the ${messageType} ${change.resourceId}`,
1385
+ [serviceRole]: {
1386
+ id: change.serviceId,
1387
+ version: change.serviceVersion,
1388
+ ...serviceOwners?.get(change.serviceId) && { owners: serviceOwners.get(change.serviceId) }
1389
+ },
1390
+ message: {
1391
+ id: change.resourceId,
1392
+ version: change.resourceVersion,
1393
+ type: messageType
1394
+ }
1395
+ }
1396
+ };
1397
+ webhookCalls.push({
1398
+ urlTemplate: action.url,
1399
+ request: fetch(url, { method: "POST", headers, body: JSON.stringify(payload) })
1400
+ });
1401
+ }
1402
+ }
1403
+ }
1404
+ const settled = await Promise.allSettled(webhookCalls.map((c2) => c2.request));
1405
+ return settled.map((result, i) => {
1406
+ const url = webhookCalls[i].urlTemplate;
1407
+ if (result.status === "fulfilled") {
1408
+ const res = result.value;
1409
+ if (!res.ok) {
1410
+ return ` Webhook failed: ${url} \u2717 (HTTP ${res.status})`;
1411
+ }
1412
+ return ` Webhook sent: ${url} \u2713`;
1413
+ }
1414
+ return ` Webhook failed: ${url} \u2717 (${result.reason instanceof Error ? result.reason.message : String(result.reason)})`;
1415
+ });
1416
+ };
1417
+
1418
+ // src/cli/governance/format.ts
1419
+ var formatGovernanceOutput = (results) => {
1420
+ if (results.length === 0) {
1421
+ return "No governance rules triggered. Catalog is compliant.";
1422
+ }
1423
+ const lines = ["Governance:", ""];
1424
+ for (const result of results) {
1425
+ lines.push(` Rule "${result.rule.name}" triggered (${result.trigger}):`);
1426
+ for (const change of result.matchedChanges) {
1427
+ const prefix = change.changeType === "added" ? "+" : "-";
1428
+ const verb = getChangeVerb(result.trigger, change.changeType);
1429
+ lines.push(` ${prefix} ${change.serviceId} is ${verb} ${change.resourceId}`);
1430
+ }
1431
+ lines.push("");
1432
+ }
1433
+ return lines.join("\n");
1434
+ };
1435
+
1436
+ // src/cli/governance/check.ts
1437
+ var import_node_path5 = __toESM(require("path"));
1438
+ var import_node_child_process = require("child_process");
1439
+ var import_node_fs6 = require("fs");
1440
+ var import_node_os = require("os");
1441
+ var import_dotenv = __toESM(require("dotenv"));
1442
+ var import_sdk6 = __toESM(require("@eventcatalog/sdk"));
1443
+ var import_license = require("@eventcatalog/license");
1444
+ var BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/;
1445
+ var extractBranchToTempDir = (branch, catalogDir, tempDirs) => {
1446
+ if (!BRANCH_NAME_RE.test(branch)) {
1447
+ throw new Error(`Invalid branch name: "${branch}"`);
1448
+ }
1449
+ const tmpDir = (0, import_node_fs6.mkdtempSync)(import_node_path5.default.join((0, import_node_os.tmpdir)(), "ec-governance-"));
1450
+ tempDirs.push(tmpDir);
1451
+ try {
1452
+ (0, import_node_child_process.execSync)(`git archive ${branch} | tar -x -C ${tmpDir}`, { cwd: catalogDir, stdio: "pipe" });
1453
+ } catch {
1454
+ throw new Error(`Failed to extract branch "${branch}". Is it a valid git branch?`);
1455
+ }
1456
+ return tmpDir;
1457
+ };
1458
+ var governanceCheck = async (opts) => {
1459
+ const dir = import_node_path5.default.resolve(opts.dir);
1460
+ import_dotenv.default.config({ path: import_node_path5.default.join(dir, ".env") });
1461
+ const isScale = await (0, import_license.isEventCatalogScaleEnabled)();
1462
+ if (!isScale) {
1463
+ throw new Error("Governance requires an EventCatalog Scale plan. Learn more at https://eventcatalog.dev/pricing");
1464
+ }
1465
+ const baseBranch = opts.base || "main";
1466
+ const tempDirs = [];
1467
+ const trackTempDir = (prefix) => {
1468
+ const d = (0, import_node_fs6.mkdtempSync)(import_node_path5.default.join((0, import_node_os.tmpdir)(), prefix));
1469
+ tempDirs.push(d);
1470
+ return d;
1471
+ };
1472
+ try {
1473
+ const baseTmpDir = extractBranchToTempDir(baseBranch, dir, tempDirs);
1474
+ const baseSnapshotDir = trackTempDir("ec-snap-base-");
1475
+ const targetSnapshotDir = trackTempDir("ec-snap-target-");
1476
+ const baseSDK = (0, import_sdk6.default)(baseTmpDir);
1477
+ const baseResult = await baseSDK.createSnapshot({ label: `base-${baseBranch}`, outputDir: baseSnapshotDir });
1478
+ let targetResult;
1479
+ if (opts.target) {
1480
+ const targetTmpDir = extractBranchToTempDir(opts.target, dir, tempDirs);
1481
+ const targetSDK = (0, import_sdk6.default)(targetTmpDir);
1482
+ targetResult = await targetSDK.createSnapshot({ label: `target-${opts.target}`, outputDir: targetSnapshotDir });
1483
+ } else {
1484
+ const targetSDK = (0, import_sdk6.default)(dir);
1485
+ targetResult = await targetSDK.createSnapshot({ label: "current", outputDir: targetSnapshotDir });
1486
+ }
1487
+ const diff = await baseSDK.diffSnapshots(baseResult.filePath, targetResult.filePath);
1488
+ const config = loadGovernanceConfig(dir);
1489
+ if (config.rules.length === 0) {
1490
+ return "No governance.yaml found or no rules defined.";
1491
+ }
1492
+ const results = evaluateGovernanceRules(diff, config, targetResult.snapshot, baseResult.snapshot);
1493
+ const messageTypes = buildMessageTypeMap(targetResult.snapshot);
1494
+ const serviceOwners = buildServiceOwnersMap(targetResult.snapshot);
1495
+ const actionOutput = await executeGovernanceActions(results, {
1496
+ messageTypes,
1497
+ status: opts.status,
1498
+ serviceOwners
1499
+ });
1500
+ if (opts.format === "json") {
1501
+ return JSON.stringify({ baseBranch, target: opts.target || "working directory", results, diff: diff.summary }, null, 2);
1502
+ }
1503
+ const targetLabel = opts.target || "working directory";
1504
+ const lines = [`Governance check: comparing ${targetLabel} against ${baseBranch}`, ""];
1505
+ lines.push(formatGovernanceOutput(results));
1506
+ if (actionOutput.length > 0) {
1507
+ lines.push("");
1508
+ lines.push(...actionOutput);
1509
+ }
1510
+ if (results.length > 0) {
1511
+ const webhookCount = actionOutput.filter((l) => l.includes("Webhook sent")).length;
1512
+ const parts = [`${results.length} rule${results.length === 1 ? "" : "s"} triggered`];
1513
+ if (webhookCount > 0) parts.push(`${webhookCount} webhook${webhookCount === 1 ? "" : "s"} sent`);
1514
+ lines.push("");
1515
+ lines.push(parts.join(", ") + ".");
1516
+ }
1517
+ return lines.join("\n");
1518
+ } finally {
1519
+ for (const d of tempDirs) {
1520
+ (0, import_node_fs6.rmSync)(d, { recursive: true, force: true });
1521
+ }
1522
+ }
1523
+ };
1524
+
1163
1525
  // src/cli/index.ts
1164
1526
  var version = "1.0.0";
1165
1527
  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"));
1528
+ const packageJsonPath = (0, import_node_path6.resolve)(__dirname, "../../package.json");
1529
+ const packageJson = JSON.parse((0, import_node_fs7.readFileSync)(packageJsonPath, "utf-8"));
1168
1530
  version = packageJson.version;
1169
1531
  } catch {
1170
1532
  }
@@ -1232,6 +1594,58 @@ import_commander.program.command("import [files...]").description("Import EventC
1232
1594
  process.exit(1);
1233
1595
  }
1234
1596
  });
1597
+ var snapshot = import_commander.program.command("snapshot").description("Create, diff, and list catalog snapshots");
1598
+ 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) => {
1599
+ try {
1600
+ const globalOpts = import_commander.program.opts();
1601
+ const dir = globalOpts.dir || ".";
1602
+ const result = await snapshotCreate({ label: opts.label, output: opts.output, stdout: opts.stdout, dir });
1603
+ console.log(result);
1604
+ } catch (error) {
1605
+ console.error(error instanceof Error ? error.message : String(error));
1606
+ process.exit(1);
1607
+ }
1608
+ });
1609
+ 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) => {
1610
+ try {
1611
+ const globalOpts = import_commander.program.opts();
1612
+ const dir = globalOpts.dir || ".";
1613
+ const result = await snapshotDiff({ fileA, fileB, format: opts.format, dir });
1614
+ console.log(result);
1615
+ } catch (error) {
1616
+ console.error(error instanceof Error ? error.message : String(error));
1617
+ process.exit(1);
1618
+ }
1619
+ });
1620
+ snapshot.command("list").description("List all snapshots in the catalog").option("--format <format>", "Output format: text or json", "text").action(async (opts) => {
1621
+ try {
1622
+ const globalOpts = import_commander.program.opts();
1623
+ const dir = globalOpts.dir || ".";
1624
+ const result = await snapshotList({ format: opts.format, dir });
1625
+ console.log(result);
1626
+ } catch (error) {
1627
+ console.error(error instanceof Error ? error.message : String(error));
1628
+ process.exit(1);
1629
+ }
1630
+ });
1631
+ var governance = import_commander.program.command("governance").description("Run governance rules against catalog changes");
1632
+ 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) => {
1633
+ try {
1634
+ const globalOpts = import_commander.program.opts();
1635
+ const dir = globalOpts.dir || ".";
1636
+ const result = await governanceCheck({
1637
+ base: opts.base,
1638
+ target: opts.target,
1639
+ format: opts.format,
1640
+ status: opts.status,
1641
+ dir
1642
+ });
1643
+ console.log(result);
1644
+ } catch (error) {
1645
+ console.error(error instanceof Error ? error.message : String(error));
1646
+ process.exit(1);
1647
+ }
1648
+ });
1235
1649
  import_commander.program.arguments("<function> [args...]").action(async (functionName, args) => {
1236
1650
  try {
1237
1651
  const options = import_commander.program.opts();