edsger 0.82.0 → 0.83.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.
Files changed (81) hide show
  1. package/README.md +36 -0
  2. package/assets/README.md +21 -18
  3. package/assets/diagrams/base.yaml +351 -0
  4. package/assets/diagrams/csharp.yaml +48 -0
  5. package/assets/diagrams/dart.yaml +41 -0
  6. package/assets/diagrams/go.yaml +44 -0
  7. package/assets/diagrams/java.yaml +43 -0
  8. package/assets/diagrams/python.yaml +41 -0
  9. package/assets/diagrams/typescript.yaml +45 -0
  10. package/assets/diagrams-viewer/assets/abnfDiagram-VRR7QNED-BuBTl7CD.js +1 -0
  11. package/assets/diagrams-viewer/assets/arc-OMyuAfpK.js +1 -0
  12. package/assets/diagrams-viewer/assets/architectureDiagram-ZJ3FMSHR-BtZuD4Oc.js +36 -0
  13. package/assets/diagrams-viewer/assets/blockDiagram-677ZJIJ3-BXUV3Kw8.js +132 -0
  14. package/assets/diagrams-viewer/assets/c4Diagram-LMCZKHZV-CB6FLms7.js +10 -0
  15. package/assets/diagrams-viewer/assets/channel-DfIqGUHQ.js +1 -0
  16. package/assets/diagrams-viewer/assets/chunk-2Q5K7J3B-mPN4SRuj.js +1 -0
  17. package/assets/diagrams-viewer/assets/chunk-32BRIVSS-DVv1-dcH.js +1 -0
  18. package/assets/diagrams-viewer/assets/chunk-5VM5RSS4-CV1HWp5i.js +15 -0
  19. package/assets/diagrams-viewer/assets/chunk-EX3LRPZG-Dka-2CXP.js +231 -0
  20. package/assets/diagrams-viewer/assets/chunk-JWPE2WC7-D18DLD3z.js +1 -0
  21. package/assets/diagrams-viewer/assets/chunk-MOJQB5TN-DZ8recee.js +88 -0
  22. package/assets/diagrams-viewer/assets/chunk-RYQCIY6F-D-T7U7G_.js +1 -0
  23. package/assets/diagrams-viewer/assets/chunk-V7JOEXUC-CycRbumY.js +206 -0
  24. package/assets/diagrams-viewer/assets/chunk-VR4S4FIN-CgtEJekm.js +1 -0
  25. package/assets/diagrams-viewer/assets/chunk-XXDRQBXY-N1fSDz2n.js +1 -0
  26. package/assets/diagrams-viewer/assets/classDiagram-OUVF2IWQ-D7S3taPH.js +1 -0
  27. package/assets/diagrams-viewer/assets/classDiagram-v2-EOCWNBFH-D7S3taPH.js +1 -0
  28. package/assets/diagrams-viewer/assets/cose-bilkent-JH36ORCC-x2DnNSLL.js +1 -0
  29. package/assets/diagrams-viewer/assets/cynefin-VYW2F7L2-CWCJCs-i.js +178 -0
  30. package/assets/diagrams-viewer/assets/cynefinDiagram-TSTJHNR4-mn39JWqG.js +62 -0
  31. package/assets/diagrams-viewer/assets/cytoscape.esm-CUqq0XTU.js +331 -0
  32. package/assets/diagrams-viewer/assets/dagre-VKFMJZFB-iOjB5Pax.js +4 -0
  33. package/assets/diagrams-viewer/assets/defaultLocale-DX6XiGOO.js +1 -0
  34. package/assets/diagrams-viewer/assets/diagram-FQU43EPY-DIFXEG6K.js +3 -0
  35. package/assets/diagrams-viewer/assets/diagram-G47NLZAW-CUFys0Kk.js +24 -0
  36. package/assets/diagrams-viewer/assets/diagram-NH7WQ7WH-ByXjnNJR.js +24 -0
  37. package/assets/diagrams-viewer/assets/diagram-OA4YK3LP-B7VCnnms.js +30 -0
  38. package/assets/diagrams-viewer/assets/diagram-WEI45ONY-DdmQeP2M.js +41 -0
  39. package/assets/diagrams-viewer/assets/diagrams-DFlErBCY.js +314 -0
  40. package/assets/diagrams-viewer/assets/diagrams-hdYg_gGB.css +1 -0
  41. package/assets/diagrams-viewer/assets/ebnfDiagram-CCIWWBDH-B54UVMil.js +1 -0
  42. package/assets/diagrams-viewer/assets/erDiagram-Q63AITRT-zkOgYlyz.js +85 -0
  43. package/assets/diagrams-viewer/assets/flowDiagram-23GEKE2U-CCNIfraz.js +156 -0
  44. package/assets/diagrams-viewer/assets/ganttDiagram-NO4QXBWP-DCn2zVSV.js +292 -0
  45. package/assets/diagrams-viewer/assets/gitGraphDiagram-IHSO6WYX-lB6SW9xm.js +106 -0
  46. package/assets/diagrams-viewer/assets/graph-C2PyVmSI.js +1 -0
  47. package/assets/diagrams-viewer/assets/infoDiagram-FWYZ7A6U-CvGfGSmH.js +2 -0
  48. package/assets/diagrams-viewer/assets/init-Gi6I4Gst.js +1 -0
  49. package/assets/diagrams-viewer/assets/ishikawaDiagram-FXEZZL3T-DnwhjgE5.js +70 -0
  50. package/assets/diagrams-viewer/assets/journeyDiagram-5HDEW3XC-C8LVhqZM.js +139 -0
  51. package/assets/diagrams-viewer/assets/kanban-definition-HUTT4EX6-PYOrsIQc.js +89 -0
  52. package/assets/diagrams-viewer/assets/katex-C5jXJg4s.js +257 -0
  53. package/assets/diagrams-viewer/assets/layout-HEHzrzvT.js +1 -0
  54. package/assets/diagrams-viewer/assets/linear-CIU4B3PA.js +1 -0
  55. package/assets/diagrams-viewer/assets/map-BQHxRNoK.js +1 -0
  56. package/assets/diagrams-viewer/assets/mindmap-definition-LN4V7U3C-B-HjOMSw.js +96 -0
  57. package/assets/diagrams-viewer/assets/ordinal-Cboi1Yqb.js +1 -0
  58. package/assets/diagrams-viewer/assets/pegDiagram-2B236MQR-dAvsUVxy.js +1 -0
  59. package/assets/diagrams-viewer/assets/pieDiagram-ENE6RG2P-W-6EgmyD.js +39 -0
  60. package/assets/diagrams-viewer/assets/quadrantDiagram-ABIIQ3AL-SlUlWOO8.js +7 -0
  61. package/assets/diagrams-viewer/assets/railroadDiagram-RFXS5EU6-KjtAtTVy.js +1 -0
  62. package/assets/diagrams-viewer/assets/requirementDiagram-TGXJPOKE-DZtlwaER.js +84 -0
  63. package/assets/diagrams-viewer/assets/sankeyDiagram-HTMAVEWB-1Ae4oRc3.js +40 -0
  64. package/assets/diagrams-viewer/assets/sequenceDiagram-DBY2YBRQ-DU-x0cLP.js +162 -0
  65. package/assets/diagrams-viewer/assets/sizeCapture-X5ZJPWSS-Cemh16BZ.js +1 -0
  66. package/assets/diagrams-viewer/assets/stateDiagram-2N3HPSRC-CtfrA6gP.js +1 -0
  67. package/assets/diagrams-viewer/assets/stateDiagram-v2-6OUMAXLB-BU13FASH.js +1 -0
  68. package/assets/diagrams-viewer/assets/swimlanes-5IMT3BWC-BkZY9GJ7.js +2 -0
  69. package/assets/diagrams-viewer/assets/swimlanesDiagram-G3AALYLV-03GXtgC-.js +8 -0
  70. package/assets/diagrams-viewer/assets/timeline-definition-FHXFAJF6-DikyXYB3.js +120 -0
  71. package/assets/diagrams-viewer/assets/vennDiagram-L72KCM5P-C_7veK57.js +34 -0
  72. package/assets/diagrams-viewer/assets/wardleyDiagram-EHGQE667-Das_YJGv.js +78 -0
  73. package/assets/diagrams-viewer/assets/xychartDiagram-FW5EYKEG-B5fwef3a.js +7 -0
  74. package/assets/diagrams-viewer/diagrams.html +14 -0
  75. package/assets/diagrams-viewer-single.html +3576 -0
  76. package/assets/manifest.yaml +5 -1
  77. package/assets/schema/diagrams.schema.json +100 -0
  78. package/assets/skills/diagram/SKILL.md +70 -0
  79. package/dist/index.js +569 -62
  80. package/dist/index.js.map +1 -1
  81. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { readFileSync as readFileSync4 } from "fs";
5
5
  import { resolve as resolve2 } from "path";
6
6
  import { Command, Option } from "commander";
7
- import pc9 from "picocolors";
7
+ import pc10 from "picocolors";
8
8
 
9
9
  // src/commands/benchmark.ts
10
10
  import { appendFileSync, mkdirSync, writeFileSync } from "fs";
@@ -127,7 +127,8 @@ function resolveStandard(kind, opts, config, configDir, forcedLanguages) {
127
127
  const roots = [
128
128
  { path: bundled, label: opts.assetsPath ? `assets:${opts.assetsPath}` : "bundled" }
129
129
  ];
130
- const overrideSpec = (kind === "benchmark" ? config.benchmark?.standards : config.review?.standards) ?? config.standards;
130
+ const perKind = kind === "benchmark" ? config.benchmark?.standards : kind === "diagrams" ? config.diagrams?.standards : config.review?.standards;
131
+ const overrideSpec = perKind ?? config.standards;
131
132
  if (overrideSpec && !opts.assetsPath) {
132
133
  roots.push({ path: resolveRootPath(overrideSpec, configDir), label: `config:${overrideSpec}` });
133
134
  }
@@ -175,11 +176,38 @@ function mergeLayers(kind, layers) {
175
176
  {}
176
177
  );
177
178
  }
179
+ if (kind === "diagrams") {
180
+ return layers.reduce(
181
+ (acc, layer) => mergeDiagrams(acc, layer),
182
+ {}
183
+ );
184
+ }
178
185
  return layers.reduce(
179
186
  (acc, layer) => mergeReview(acc, layer),
180
187
  {}
181
188
  );
182
189
  }
190
+ function mergeDiagrams(base, overlay) {
191
+ const groups = mergeById(base.groups ?? [], overlay.groups ?? [], (b, o) => ({
192
+ ...b,
193
+ ...o,
194
+ // Diagrams with the same id are overridden; new ids are appended.
195
+ diagrams: mergeById(b.diagrams ?? [], o.diagrams ?? [], (bd, od) => ({
196
+ ...bd,
197
+ ...od
198
+ }))
199
+ }));
200
+ return {
201
+ ...base,
202
+ ...overlay,
203
+ // Preserve the base layer's identity rather than the last overlay's.
204
+ id: base.id ?? overlay.id,
205
+ name: base.name ?? overlay.name,
206
+ version: base.version ?? overlay.version,
207
+ kind: "diagrams",
208
+ groups
209
+ };
210
+ }
183
211
  function mergeBenchmark(base, overlay) {
184
212
  const categories = mergeById(
185
213
  base.categories ?? [],
@@ -707,6 +735,28 @@ var ResolveResultSchema = z.object({
707
735
  testsRun: z.string().nullable().default(null).describe("Command used to verify, and its outcome"),
708
736
  remaining: z.array(z.string()).default([]).describe("Anything still unresolved")
709
737
  });
738
+ var GeneratedDiagramSchema = z.object({
739
+ id: z.string().describe("Diagram id from the catalog, or a descriptive slug if bespoke"),
740
+ groupId: z.string().describe("Catalog group id this diagram belongs to (e.g. architecture)"),
741
+ title: z.string().describe("Human-readable diagram title"),
742
+ type: z.string().describe(
743
+ "Mermaid diagram type used, e.g. flowchart, erDiagram, sequenceDiagram, classDiagram"
744
+ ),
745
+ mermaid: z.string().describe("Valid Mermaid source for the diagram (renders in HTML/Markdown)"),
746
+ description: z.string().describe("One or two sentences: what the diagram shows and why it applies to this repo"),
747
+ evidence: z.array(z.string()).default([]).describe("Repo files this diagram was derived from")
748
+ });
749
+ var DiagramsResultSchema = z.object({
750
+ profile: z.object({
751
+ summary: z.string().describe("2-4 sentence characterisation of the project"),
752
+ kinds: z.array(z.string()).default([]).describe("Project kind tags, e.g. cli, rest-api, spa, library, data-pipeline, monorepo"),
753
+ characteristics: z.array(z.string()).default([]).describe(
754
+ "Notable traits that drove diagram selection (owns a Postgres DB, deploys to k8s, \u2026)"
755
+ )
756
+ }).describe("The project profile that justifies which diagrams were chosen"),
757
+ diagrams: z.array(GeneratedDiagramSchema).describe("The diagrams selected for and generated from this repository"),
758
+ notes: z.string().nullable().default(null).describe("Optional notes, e.g. diagrams intentionally skipped and why")
759
+ });
710
760
 
711
761
  // src/commands/benchmark.ts
712
762
  async function runBenchmark(opts) {
@@ -1262,54 +1312,77 @@ function buildPrompt3(ctx, feedback, instruction) {
1262
1312
  }
1263
1313
 
1264
1314
  // src/commands/serve.ts
1265
- import { createReadStream, existsSync as existsSync4, statSync as statSync2 } from "fs";
1266
- import { createServer } from "http";
1267
- import { extname, join as join6, normalize } from "path";
1268
- import pc6 from "picocolors";
1315
+ import { join as join7 } from "path";
1269
1316
 
1270
1317
  // src/core/dashboard.ts
1271
1318
  import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
1272
1319
  import { join as join5 } from "path";
1273
- function assembleData(benchmarkDir) {
1274
- const latest = readJson(join5(benchmarkDir, "report.json"));
1275
- const history = readJsonl(join5(benchmarkDir, "history.jsonl"));
1320
+ function assembleFrom(dir, latestFile) {
1321
+ const latest = readJson(join5(dir, latestFile));
1322
+ const history = readJsonl(join5(dir, "history.jsonl"));
1276
1323
  const runs = {};
1277
- const runsDir = join5(benchmarkDir, "runs");
1324
+ const runsDir = join5(dir, "runs");
1278
1325
  if (existsSync3(runsDir)) {
1279
1326
  for (const entry of readdirSync2(runsDir)) {
1280
- const report = readJson(join5(runsDir, entry, "report.json"));
1327
+ const report = readJson(join5(runsDir, entry, latestFile));
1281
1328
  if (report) runs[entry] = report;
1282
1329
  }
1283
1330
  }
1284
1331
  const repo = latest && typeof latest === "object" && "repo" in latest ? latest.repo : void 0;
1285
1332
  return { repo, latest, history, runs };
1286
1333
  }
1334
+ function assembleData(benchmarkDir) {
1335
+ return assembleFrom(benchmarkDir, "report.json");
1336
+ }
1337
+ function assembleDiagramData(diagramsDir) {
1338
+ return assembleFrom(diagramsDir, "diagrams.json");
1339
+ }
1287
1340
  function hasData(data) {
1288
1341
  return Boolean(data.latest) || data.history.length > 0;
1289
1342
  }
1290
- function findViewerDir() {
1291
- const dir = join5(findBundledAssetsDir(), "viewer");
1292
- if (!existsSync3(join5(dir, "index.html"))) {
1343
+ function findViewerDir(subdir = "viewer", index = "index.html") {
1344
+ const dir = join5(findBundledAssetsDir(), subdir);
1345
+ if (!existsSync3(join5(dir, index))) {
1293
1346
  throw new Error(
1294
- "Dashboard viewer assets not found. Reinstall edsger, or run `npm run sync-viewer` in a dev checkout."
1347
+ `Dashboard viewer assets not found (${subdir}/${index}). Reinstall edsger, or run \`npm run sync-viewer\` in a dev checkout.`
1295
1348
  );
1296
1349
  }
1297
1350
  return dir;
1298
1351
  }
1299
- function readViewerSingleTemplate() {
1300
- const file = join5(findBundledAssetsDir(), "viewer-single.html");
1301
- if (!existsSync3(file)) {
1352
+ function findDiagramViewerDir() {
1353
+ return findViewerDir("diagrams-viewer", "diagrams.html");
1354
+ }
1355
+ function readSingleTemplate(file) {
1356
+ const path = join5(findBundledAssetsDir(), file);
1357
+ if (!existsSync3(path)) {
1302
1358
  throw new Error(
1303
- "Single-file viewer template not found. Reinstall edsger, or run `npm run sync-viewer`."
1359
+ `Single-file viewer template not found (${file}). Reinstall edsger, or run \`npm run sync-viewer\`.`
1304
1360
  );
1305
1361
  }
1306
- return readFileSync2(file, "utf8");
1362
+ return readFileSync2(path, "utf8");
1307
1363
  }
1308
- function renderSingleFile(template, data) {
1364
+ function readViewerSingleTemplate() {
1365
+ return readSingleTemplate("viewer-single.html");
1366
+ }
1367
+ function readDiagramViewerSingleTemplate() {
1368
+ return readSingleTemplate("diagrams-viewer-single.html");
1369
+ }
1370
+ function renderSingleFileWith(template, data, globalName) {
1309
1371
  const json = JSON.stringify(data).replace(/<\//g, "<\\/");
1310
- const tag = `<script>window.__EDSGER_DATA__=${json}</script>`;
1311
- if (template.includes("</head>")) return template.replace("</head>", `${tag}</head>`);
1312
- return template.replace("<body>", `<body>${tag}`);
1372
+ const tag = `<script>window.${globalName}=${json}</script>`;
1373
+ const placeholder = new RegExp(`<!--[^>]*${globalName}[^>]*-->`);
1374
+ if (placeholder.test(template)) return template.replace(placeholder, () => tag);
1375
+ const scriptIdx = template.indexOf("<script");
1376
+ if (scriptIdx !== -1) return template.slice(0, scriptIdx) + tag + template.slice(scriptIdx);
1377
+ const headIdx = template.lastIndexOf("</head>");
1378
+ if (headIdx !== -1) return template.slice(0, headIdx) + tag + template.slice(headIdx);
1379
+ return tag + template;
1380
+ }
1381
+ function renderSingleFile(template, data) {
1382
+ return renderSingleFileWith(template, data, "__EDSGER_DATA__");
1383
+ }
1384
+ function renderDiagramsSingleFile(template, data) {
1385
+ return renderSingleFileWith(template, data, "__EDSGER_DIAGRAMS__");
1313
1386
  }
1314
1387
  function readJson(path) {
1315
1388
  try {
@@ -1332,6 +1405,12 @@ function readJsonl(path) {
1332
1405
  return out;
1333
1406
  }
1334
1407
 
1408
+ // src/core/viewer-server.ts
1409
+ import { createReadStream, existsSync as existsSync4, statSync as statSync2 } from "fs";
1410
+ import { createServer } from "http";
1411
+ import { extname, join as join6, normalize } from "path";
1412
+ import pc6 from "picocolors";
1413
+
1335
1414
  // src/core/open.ts
1336
1415
  import { spawn } from "child_process";
1337
1416
  function openInBrowser(target) {
@@ -1344,7 +1423,7 @@ function openInBrowser(target) {
1344
1423
  }
1345
1424
  }
1346
1425
 
1347
- // src/commands/serve.ts
1426
+ // src/core/viewer-server.ts
1348
1427
  var MIME = {
1349
1428
  ".html": "text/html; charset=utf-8",
1350
1429
  ".js": "text/javascript; charset=utf-8",
@@ -1356,28 +1435,27 @@ var MIME = {
1356
1435
  ".woff2": "font/woff2",
1357
1436
  ".woff": "font/woff"
1358
1437
  };
1359
- async function runServe(opts) {
1360
- const benchmarkDir = join6(opts.cwd, opts.dir);
1361
- const viewerDir = findViewerDir();
1438
+ async function startViewerServer(opts) {
1362
1439
  const server = createServer((req, res) => {
1363
1440
  const url = (req.url ?? "/").split("?")[0] ?? "/";
1364
- if (url === "/edsger-data.json") {
1441
+ if (url === opts.dataRoute) {
1365
1442
  res.writeHead(200, {
1366
1443
  "content-type": "application/json; charset=utf-8",
1367
1444
  "cache-control": "no-store"
1368
1445
  });
1369
- res.end(JSON.stringify(assembleData(benchmarkDir)));
1446
+ res.end(JSON.stringify(opts.getData()));
1370
1447
  return;
1371
1448
  }
1372
- const rel = url === "/" ? "index.html" : decodeURIComponent(url.replace(/^\/+/, ""));
1449
+ const rel = url === "/" ? opts.indexFile : decodeURIComponent(url.replace(/^\/+/, ""));
1373
1450
  const safe = normalize(rel).replace(/^(\.\.(?:[/\\]|$))+/, "");
1374
- let file = join6(viewerDir, safe);
1375
- if (!file.startsWith(viewerDir)) {
1451
+ let file = join6(opts.viewerDir, safe);
1452
+ if (!file.startsWith(opts.viewerDir)) {
1376
1453
  res.writeHead(403);
1377
1454
  res.end("forbidden");
1378
1455
  return;
1379
1456
  }
1380
- if (!existsSync4(file) || statSync2(file).isDirectory()) file = join6(viewerDir, "index.html");
1457
+ if (!existsSync4(file) || statSync2(file).isDirectory())
1458
+ file = join6(opts.viewerDir, opts.indexFile);
1381
1459
  res.writeHead(200, { "content-type": MIME[extname(file)] ?? "application/octet-stream" });
1382
1460
  createReadStream(file).pipe(res);
1383
1461
  });
@@ -1388,9 +1466,7 @@ async function runServe(opts) {
1388
1466
  const port = typeof addr === "object" && addr ? addr.port : opts.port;
1389
1467
  const shownHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
1390
1468
  const url = `http://${shownHost}:${port}/`;
1391
- if (!hasData(assembleData(benchmarkDir))) {
1392
- log.warn(`No benchmark data in ${opts.dir} yet \u2014 run \`edsger benchmark\` first.`);
1393
- }
1469
+ if (opts.emptyHint) log.warn(opts.emptyHint);
1394
1470
  log.success(`Dashboard at ${pc6.bold(pc6.cyan(url))} ${pc6.dim("(Ctrl-C to stop)")}`);
1395
1471
  if (opts.open) openInBrowser(url);
1396
1472
  });
@@ -1402,19 +1478,35 @@ async function runServe(opts) {
1402
1478
  });
1403
1479
  }
1404
1480
 
1481
+ // src/commands/serve.ts
1482
+ async function runServe(opts) {
1483
+ const benchmarkDir = join7(opts.cwd, opts.dir);
1484
+ const viewerDir = findViewerDir();
1485
+ return startViewerServer({
1486
+ viewerDir,
1487
+ indexFile: "index.html",
1488
+ dataRoute: "/edsger-data.json",
1489
+ getData: () => assembleData(benchmarkDir),
1490
+ port: opts.port,
1491
+ host: opts.host,
1492
+ open: opts.open,
1493
+ emptyHint: hasData(assembleData(benchmarkDir)) ? void 0 : `No benchmark data in ${opts.dir} yet \u2014 run \`edsger benchmark\` first.`
1494
+ });
1495
+ }
1496
+
1405
1497
  // src/commands/report.ts
1406
1498
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
1407
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join7 } from "path";
1499
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join8 } from "path";
1408
1500
  import pc7 from "picocolors";
1409
1501
  async function runReport(opts) {
1410
- const benchmarkDir = join7(opts.cwd, opts.dir);
1502
+ const benchmarkDir = join8(opts.cwd, opts.dir);
1411
1503
  const data = assembleData(benchmarkDir);
1412
1504
  if (!hasData(data)) {
1413
1505
  log.error(`No benchmark data in ${opts.dir}. Run \`edsger benchmark\` first.`);
1414
1506
  return 1;
1415
1507
  }
1416
1508
  const html = renderSingleFile(readViewerSingleTemplate(), data);
1417
- const out = isAbsolute2(opts.html) ? opts.html : join7(opts.cwd, opts.html);
1509
+ const out = isAbsolute2(opts.html) ? opts.html : join8(opts.cwd, opts.html);
1418
1510
  mkdirSync2(dirname2(out), { recursive: true });
1419
1511
  writeFileSync3(out, html, "utf8");
1420
1512
  log.success(
@@ -1424,26 +1516,388 @@ async function runReport(opts) {
1424
1516
  return 0;
1425
1517
  }
1426
1518
 
1519
+ // src/commands/diagram.ts
1520
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
1521
+ import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute3, join as join9 } from "path";
1522
+ import pc8 from "picocolors";
1523
+
1524
+ // src/core/diagrams.ts
1525
+ function buildDiagramsPrompt(catalog, cwd, only) {
1526
+ const onlySet = only && only.length ? new Set(only.map((s) => s.trim())) : null;
1527
+ const catalogText = catalog.groups.map((group) => {
1528
+ const diagrams = group.diagrams.filter((d) => !onlySet || onlySet.has(d.id)).map((d) => {
1529
+ const detect = d.detect?.length ? `
1530
+ detect: ${d.detect.join(", ")}` : "";
1531
+ return ` - id: ${d.id}
1532
+ title: ${d.title}
1533
+ mermaidType: ${d.type}
1534
+ appliesWhen: ${d.appliesWhen.trim()}` + detect + `
1535
+ guidance: ${d.guidance.trim()}`;
1536
+ }).join("\n");
1537
+ if (!diagrams) return "";
1538
+ return `- group: ${group.id} (${group.name})
1539
+ ${diagrams}`;
1540
+ }).filter(Boolean).join("\n");
1541
+ return [
1542
+ `Produce a set of professional software diagrams for the repository at: ${cwd}`,
1543
+ ``,
1544
+ `You have a catalog of candidate diagrams below. Your job is to pick the ones`,
1545
+ `that genuinely fit THIS project and generate each as valid Mermaid.`,
1546
+ ``,
1547
+ `Method:`,
1548
+ `1. Explore the repository (Read/Grep/Glob and read-only Bash like ls, cat, git log).`,
1549
+ ` Characterise the project: what kind is it (service, library, CLI, SPA, data`,
1550
+ ` pipeline, monorepo, \u2026), what does it integrate with, does it own a database,`,
1551
+ ` how is it deployed?`,
1552
+ `2. For each catalog diagram, use its appliesWhen/detect to decide whether it adds`,
1553
+ ` value for this repo. Only include diagrams the evidence supports. A library`,
1554
+ ` needs different diagrams than a microservice. Do NOT invent structure that is`,
1555
+ ` not in the code. Aim for the ${onlySet ? "requested" : "5-9 most valuable"} diagrams.`,
1556
+ `3. For each selected diagram, follow its guidance and emit VALID Mermaid of the`,
1557
+ ` given mermaidType, grounded in real files. Rules for the mermaid field:`,
1558
+ ` - Put ONLY the Mermaid source (starting with the diagram keyword, e.g.`,
1559
+ ` "flowchart TD" or "erDiagram"). Do NOT wrap it in \`\`\`mermaid fences and`,
1560
+ ` do NOT add any prose/markdown outside the diagram.`,
1561
+ ` - It MUST parse as-is. Use simple alphanumeric node/participant ids and put`,
1562
+ ` human text in quoted labels, e.g. A["Order Service"] not A[Order Service].`,
1563
+ ` Quote any label containing spaces, punctuation, parentheses or slashes.`,
1564
+ ` - Avoid Mermaid reserved words as bare node ids (especially "end" in`,
1565
+ ` flowcharts \u2014 write End or "end" instead).`,
1566
+ ` - Set "type" to the exact keyword the source starts with.`,
1567
+ ` - Keep each diagram to roughly one screen and legible.`,
1568
+ ` - Prefer faithful over exhaustive; record the files you used as evidence.`,
1569
+ `4. When finished, call submit_result exactly once with the project profile and`,
1570
+ ` the diagrams (each with id, groupId, title, type, mermaid, description, evidence).`,
1571
+ ``,
1572
+ `Diagram catalog:`,
1573
+ catalogText
1574
+ ].join("\n");
1575
+ }
1576
+ function sanitizeMermaid(src) {
1577
+ let s = src.trim();
1578
+ const fence = s.match(/^```(?:mermaid)?\s*\n([\s\S]*?)\n```$/);
1579
+ if (fence && fence[1] !== void 0) s = fence[1].trim();
1580
+ return s;
1581
+ }
1582
+ var KNOWN_MERMAID_TYPES = /* @__PURE__ */ new Set([
1583
+ "flowchart",
1584
+ "graph",
1585
+ "sequenceDiagram",
1586
+ "classDiagram",
1587
+ "stateDiagram",
1588
+ "stateDiagram-v2",
1589
+ "erDiagram",
1590
+ "journey",
1591
+ "gantt",
1592
+ "gitGraph",
1593
+ "mindmap",
1594
+ "timeline",
1595
+ "requirementDiagram",
1596
+ "quadrantChart",
1597
+ "pie",
1598
+ "sankey-beta",
1599
+ "xychart-beta",
1600
+ "block-beta",
1601
+ "packet-beta",
1602
+ "kanban",
1603
+ "zenuml",
1604
+ "C4Context",
1605
+ "C4Container",
1606
+ "C4Component",
1607
+ "C4Deployment",
1608
+ "C4Dynamic",
1609
+ "architecture-beta"
1610
+ ]);
1611
+ function firstKeyword(mermaid) {
1612
+ for (const raw of mermaid.split("\n")) {
1613
+ const line = raw.trim();
1614
+ if (!line || line.startsWith("%%")) continue;
1615
+ return line.split(/[\s({]/)[0] ?? line;
1616
+ }
1617
+ return "";
1618
+ }
1619
+ function normaliseType(keyword) {
1620
+ const k = keyword.trim();
1621
+ if (k === "graph") return "flowchart";
1622
+ if (k === "stateDiagram") return "stateDiagram-v2";
1623
+ return k;
1624
+ }
1625
+ function inferType(mermaid) {
1626
+ return firstKeyword(mermaid) || "flowchart";
1627
+ }
1628
+ function validateMermaid(declaredType, mermaid) {
1629
+ const warnings = [];
1630
+ if (mermaid.includes("```")) warnings.push("source still contains a ``` fence");
1631
+ const keyword = firstKeyword(mermaid);
1632
+ if (!keyword) {
1633
+ warnings.push("no diagram content");
1634
+ return warnings;
1635
+ }
1636
+ if (!KNOWN_MERMAID_TYPES.has(keyword)) {
1637
+ warnings.push(`unrecognised diagram keyword "${keyword}"`);
1638
+ }
1639
+ const body = mermaid.split("\n").filter((l) => {
1640
+ const t = l.trim();
1641
+ return t && !t.startsWith("%%");
1642
+ });
1643
+ if (body.length < 2) warnings.push("header only \u2014 the diagram has no body");
1644
+ if (declaredType && KNOWN_MERMAID_TYPES.has(keyword)) {
1645
+ if (normaliseType(declaredType) !== normaliseType(keyword)) {
1646
+ warnings.push(`declared type "${declaredType}" but source starts with "${keyword}"`);
1647
+ }
1648
+ }
1649
+ return warnings;
1650
+ }
1651
+ function prepareDiagrams(catalog, result) {
1652
+ const groupName = new Map(catalog.groups.map((g) => [g.id, g.name]));
1653
+ const groupOrder = new Map(catalog.groups.map((g, i) => [g.id, i]));
1654
+ const prepared = [];
1655
+ const seenIds = /* @__PURE__ */ new Map();
1656
+ for (const d of result.diagrams) {
1657
+ const mermaid = sanitizeMermaid(d.mermaid ?? "");
1658
+ if (!mermaid) {
1659
+ log.warn(`Skipping diagram "${d.id}" \u2014 it had no mermaid source.`);
1660
+ continue;
1661
+ }
1662
+ const baseId = d.id || inferType(mermaid);
1663
+ const count = seenIds.get(baseId) ?? 0;
1664
+ seenIds.set(baseId, count + 1);
1665
+ const id = count === 0 ? baseId : `${baseId}-${count + 1}`;
1666
+ const type = d.type?.trim() || inferType(mermaid);
1667
+ prepared.push({
1668
+ ...d,
1669
+ id,
1670
+ mermaid,
1671
+ type,
1672
+ evidence: d.evidence ?? [],
1673
+ groupName: groupName.get(d.groupId) ?? "Other",
1674
+ warnings: validateMermaid(type, mermaid)
1675
+ });
1676
+ }
1677
+ prepared.sort((a, b) => {
1678
+ const oa = groupOrder.get(a.groupId) ?? Number.MAX_SAFE_INTEGER;
1679
+ const ob = groupOrder.get(b.groupId) ?? Number.MAX_SAFE_INTEGER;
1680
+ return oa - ob;
1681
+ });
1682
+ if (prepared.length === 0) {
1683
+ log.warn("The agent returned no usable diagrams.");
1684
+ } else {
1685
+ const flagged = prepared.filter((d) => d.warnings.length);
1686
+ if (flagged.length) {
1687
+ log.warn(
1688
+ `${flagged.length}/${prepared.length} diagram(s) have Mermaid warnings \u2014 they will still render if valid, but check: ${flagged.map((d) => d.id).join(", ")}`
1689
+ );
1690
+ }
1691
+ }
1692
+ return { profile: result.profile, diagrams: prepared, notes: result.notes ?? null };
1693
+ }
1694
+ function renderDiagramsMarkdown(prepared, meta) {
1695
+ const lines = [];
1696
+ lines.push(`# Edsger Diagrams`);
1697
+ lines.push("");
1698
+ lines.push(`> ${prepared.profile.summary}`);
1699
+ lines.push("");
1700
+ lines.push(`| Field | Value |`);
1701
+ lines.push(`| --- | --- |`);
1702
+ lines.push(`| Repository | \`${meta.repo}\` |`);
1703
+ lines.push(`| Languages | ${meta.languages.join(", ") || "n/a"} |`);
1704
+ lines.push(`| Project kinds | ${prepared.profile.kinds.join(", ") || "n/a"} |`);
1705
+ lines.push(`| Catalog sources | ${meta.layers.join(" \u2192 ")} |`);
1706
+ lines.push(`| Model | ${meta.model} |`);
1707
+ lines.push(`| Generated | ${meta.generatedAt} |`);
1708
+ lines.push("");
1709
+ if (prepared.profile.characteristics.length) {
1710
+ lines.push(`**Characteristics:** ${prepared.profile.characteristics.join(" \xB7 ")}`);
1711
+ lines.push("");
1712
+ }
1713
+ let currentGroup = null;
1714
+ for (const d of prepared.diagrams) {
1715
+ if (d.groupName !== currentGroup) {
1716
+ currentGroup = d.groupName;
1717
+ lines.push(`## ${currentGroup}`);
1718
+ lines.push("");
1719
+ }
1720
+ lines.push(`### ${d.title}`);
1721
+ lines.push("");
1722
+ lines.push(`${d.description} _(${d.type})_`);
1723
+ lines.push("");
1724
+ if (d.warnings.length) {
1725
+ lines.push(`> \u26A0\uFE0F ${d.warnings.join("; ")}`);
1726
+ lines.push("");
1727
+ }
1728
+ lines.push("```mermaid");
1729
+ lines.push(d.mermaid);
1730
+ lines.push("```");
1731
+ lines.push("");
1732
+ if (d.evidence.length) {
1733
+ lines.push(`<sub>Evidence: ${d.evidence.map((e) => `\`${e}\``).join(", ")}</sub>`);
1734
+ lines.push("");
1735
+ }
1736
+ }
1737
+ if (prepared.notes) {
1738
+ lines.push(`## Notes`);
1739
+ lines.push("");
1740
+ lines.push(prepared.notes);
1741
+ lines.push("");
1742
+ }
1743
+ lines.push(`---`);
1744
+ lines.push(`<sub>Generated by [edsger](https://www.npmjs.com/package/edsger).</sub>`);
1745
+ return lines.join("\n");
1746
+ }
1747
+
1748
+ // src/commands/diagram.ts
1749
+ async function runDiagramGenerate(opts) {
1750
+ const { config, path: configPath } = loadConfig(opts.cwd);
1751
+ const configDir = configPath ? join9(configPath, "..") : opts.cwd;
1752
+ const resolved = resolveStandard("diagrams", opts, config, configDir);
1753
+ const catalog = resolved.standard;
1754
+ log.info(
1755
+ `Diagramming ${pc8.bold(basename2(opts.cwd))} with ${pc8.bold(catalog.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
1756
+ );
1757
+ log.debug(`catalog layers: ${resolved.layers.join(" \u2192 ")}`);
1758
+ const prompt = buildDiagramsPrompt(catalog, opts.cwd, opts.only);
1759
+ const { result, costUsd, numTurns } = await runAgent({
1760
+ prompt,
1761
+ cwd: opts.cwd,
1762
+ model: opts.model,
1763
+ maxTurns: opts.maxTurns,
1764
+ timeoutMs: opts.timeoutMs,
1765
+ allowWrite: false,
1766
+ verbose: opts.verbose,
1767
+ resultShape: DiagramsResultSchema.shape,
1768
+ systemPromptAppend: "You are Edsger, a staff-level software architect. You produce accurate, legible diagrams of a codebase as valid Mermaid, grounded strictly in the code you inspect. Never invent components, tables, or flows that are not in the repository. Do not modify any files."
1769
+ });
1770
+ if (!result) {
1771
+ log.error(
1772
+ "The agent did not produce a result. Try increasing --max-turns or check credentials."
1773
+ );
1774
+ return 1;
1775
+ }
1776
+ const prepared = prepareDiagrams(catalog, result);
1777
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1778
+ const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
1779
+ const git = gitInfo(opts.cwd);
1780
+ const md = renderDiagramsMarkdown(prepared, {
1781
+ repo: basename2(opts.cwd),
1782
+ languages: resolved.languages,
1783
+ layers: resolved.layers,
1784
+ generatedAt,
1785
+ model: opts.model
1786
+ });
1787
+ const jsonReport = {
1788
+ schema: "edsger.diagrams/1",
1789
+ runId,
1790
+ generatedAt,
1791
+ model: opts.model,
1792
+ repo: basename2(opts.cwd),
1793
+ git,
1794
+ languages: resolved.languages,
1795
+ catalog: { name: catalog.name, version: catalog.version, layers: resolved.layers },
1796
+ profile: prepared.profile,
1797
+ diagrams: prepared.diagrams,
1798
+ notes: prepared.notes,
1799
+ costUsd
1800
+ };
1801
+ const groupCounts = {};
1802
+ for (const d of prepared.diagrams) groupCounts[d.groupId] = (groupCounts[d.groupId] ?? 0) + 1;
1803
+ const historyRecord = {
1804
+ runId,
1805
+ generatedAt,
1806
+ commit: git.commit,
1807
+ branch: git.branch,
1808
+ dirty: git.dirty,
1809
+ model: opts.model,
1810
+ catalogVersion: catalog.version,
1811
+ diagramCount: prepared.diagrams.length,
1812
+ groups: groupCounts,
1813
+ costUsd
1814
+ };
1815
+ if (!opts.noWrite && !opts.dryRun) {
1816
+ const baseDir = join9(opts.cwd, opts.out);
1817
+ const mdOut = md + "\n";
1818
+ const jsonOut = JSON.stringify(jsonReport, null, 2) + "\n";
1819
+ const runDir = join9(baseDir, "runs", runId);
1820
+ mkdirSync3(runDir, { recursive: true });
1821
+ writeFileSync4(join9(runDir, "diagrams.json"), jsonOut, "utf8");
1822
+ writeFileSync4(join9(runDir, "diagrams.md"), mdOut, "utf8");
1823
+ writeFileSync4(join9(baseDir, "diagrams.json"), jsonOut, "utf8");
1824
+ writeFileSync4(join9(baseDir, "diagrams.md"), mdOut, "utf8");
1825
+ appendFileSync2(join9(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
1826
+ log.success(
1827
+ `Diagrams written to ${pc8.bold(join9(opts.out, "diagrams.md"))} ${pc8.dim(`(run archived at ${join9(opts.out, "runs", runId)})`)}`
1828
+ );
1829
+ }
1830
+ printSummary3(prepared, numTurns, costUsd);
1831
+ if (opts.json) process.stdout.write(JSON.stringify(jsonReport, null, 2) + "\n");
1832
+ return 0;
1833
+ }
1834
+ function printSummary3(prepared, numTurns, costUsd) {
1835
+ log.heading(`${prepared.diagrams.length} diagram(s)`);
1836
+ let currentGroup = null;
1837
+ for (const d of prepared.diagrams) {
1838
+ if (d.groupName !== currentGroup) {
1839
+ currentGroup = d.groupName;
1840
+ process.stderr.write(` ${pc8.bold(currentGroup)}
1841
+ `);
1842
+ }
1843
+ const warn = d.warnings.length ? ` ${pc8.yellow("\u26A0")}` : "";
1844
+ process.stderr.write(` ${pc8.cyan("\u25C6")} ${d.title} ${pc8.dim(`(${d.type})`)}${warn}
1845
+ `);
1846
+ }
1847
+ log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
1848
+ }
1849
+ async function runDiagramServe(opts) {
1850
+ const diagramsDir = join9(opts.cwd, opts.dir);
1851
+ const viewerDir = findDiagramViewerDir();
1852
+ return startViewerServer({
1853
+ viewerDir,
1854
+ indexFile: "diagrams.html",
1855
+ dataRoute: "/edsger-diagrams.json",
1856
+ getData: () => assembleDiagramData(diagramsDir),
1857
+ port: opts.port,
1858
+ host: opts.host,
1859
+ open: opts.open,
1860
+ emptyHint: hasData(assembleDiagramData(diagramsDir)) ? void 0 : `No diagram data in ${opts.dir} yet \u2014 run \`edsger diagram\` first.`
1861
+ });
1862
+ }
1863
+ async function runDiagramReport(opts) {
1864
+ const diagramsDir = join9(opts.cwd, opts.dir);
1865
+ const data = assembleDiagramData(diagramsDir);
1866
+ if (!hasData(data)) {
1867
+ log.error(`No diagram data in ${opts.dir}. Run \`edsger diagram\` first.`);
1868
+ return 1;
1869
+ }
1870
+ const html = renderDiagramsSingleFile(readDiagramViewerSingleTemplate(), data);
1871
+ const out = isAbsolute3(opts.html) ? opts.html : join9(opts.cwd, opts.html);
1872
+ mkdirSync3(dirname3(out), { recursive: true });
1873
+ writeFileSync4(out, html, "utf8");
1874
+ log.success(
1875
+ `Wrote self-contained diagram viewer to ${pc8.bold(opts.html)} ${pc8.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
1876
+ );
1877
+ if (opts.open) openInBrowser(out);
1878
+ return 0;
1879
+ }
1880
+
1427
1881
  // src/commands/skills.ts
1428
1882
  import {
1429
1883
  cpSync,
1430
1884
  existsSync as existsSync5,
1431
- mkdirSync as mkdirSync3,
1885
+ mkdirSync as mkdirSync4,
1432
1886
  readFileSync as readFileSync3,
1433
1887
  readdirSync as readdirSync3,
1434
1888
  rmSync as rmSync2,
1435
1889
  statSync as statSync3,
1436
- writeFileSync as writeFileSync4
1890
+ writeFileSync as writeFileSync5
1437
1891
  } from "fs";
1438
1892
  import { homedir as homedir2 } from "os";
1439
- import { join as join8 } from "path";
1440
- import pc8 from "picocolors";
1893
+ import { join as join10 } from "path";
1894
+ import pc9 from "picocolors";
1441
1895
  var MANAGED = ["benchmark", "pr-review", "pr-resolve"];
1442
1896
  function targetSkillsDir(scope) {
1443
- return scope.global ? join8(homedir2(), ".claude", "skills") : join8(scope.cwd, ".claude", "skills");
1897
+ return scope.global ? join10(homedir2(), ".claude", "skills") : join10(scope.cwd, ".claude", "skills");
1444
1898
  }
1445
1899
  function sourceSkillsDir() {
1446
- return join8(findBundledAssetsDir(), "skills");
1900
+ return join10(findBundledAssetsDir(), "skills");
1447
1901
  }
1448
1902
  async function runSkillsInstall(opts) {
1449
1903
  const assetsDir = findBundledAssetsDir();
@@ -1453,20 +1907,20 @@ async function runSkillsInstall(opts) {
1453
1907
  return 1;
1454
1908
  }
1455
1909
  const dest = targetSkillsDir(opts);
1456
- mkdirSync3(dest, { recursive: true });
1457
- const available = readdirSync3(src).filter((n) => existsSync5(join8(src, n, "SKILL.md")));
1910
+ mkdirSync4(dest, { recursive: true });
1911
+ const available = readdirSync3(src).filter((n) => existsSync5(join10(src, n, "SKILL.md")));
1458
1912
  let installed = 0;
1459
1913
  for (const name of available) {
1460
- const outDir = join8(dest, name);
1914
+ const outDir = join10(dest, name);
1461
1915
  if (existsSync5(outDir) && !opts.force) {
1462
1916
  log.warn(`skill "${name}" already exists \u2014 use --force to overwrite`);
1463
1917
  continue;
1464
1918
  }
1465
1919
  rmSync2(outDir, { recursive: true, force: true });
1466
- cpSync(join8(src, name), outDir, { recursive: true });
1467
- const skillFile = join8(outDir, "SKILL.md");
1920
+ cpSync(join10(src, name), outDir, { recursive: true });
1921
+ const skillFile = join10(outDir, "SKILL.md");
1468
1922
  const rewritten = readFileSync3(skillFile, "utf8").split("${CLAUDE_PLUGIN_ROOT}").join(assetsDir);
1469
- writeFileSync4(skillFile, rewritten, "utf8");
1923
+ writeFileSync5(skillFile, rewritten, "utf8");
1470
1924
  installed++;
1471
1925
  log.success(`installed /${name}`);
1472
1926
  }
@@ -1474,7 +1928,7 @@ async function runSkillsInstall(opts) {
1474
1928
  log.info("Nothing installed.");
1475
1929
  return 0;
1476
1930
  }
1477
- log.heading(`Installed ${installed} skill(s) into ${pc8.bold(dest)}`);
1931
+ log.heading(`Installed ${installed} skill(s) into ${pc9.bold(dest)}`);
1478
1932
  log.step(`scope: ${opts.global ? "user (all projects)" : "this project"}`);
1479
1933
  log.step(`standards: ${assetsDir}`);
1480
1934
  log.step("Open Claude Code and run /benchmark, /pr-review, or /pr-resolve.");
@@ -1484,7 +1938,7 @@ async function runSkillsUninstall(opts) {
1484
1938
  const dest = targetSkillsDir(opts);
1485
1939
  let removed = 0;
1486
1940
  for (const name of MANAGED) {
1487
- const dir = join8(dest, name);
1941
+ const dir = join10(dest, name);
1488
1942
  if (existsSync5(dir)) {
1489
1943
  rmSync2(dir, { recursive: true, force: true });
1490
1944
  removed++;
@@ -1501,13 +1955,13 @@ async function runSkillsList(opts) {
1501
1955
  ]) {
1502
1956
  const dir = targetSkillsDir(scope);
1503
1957
  const present = MANAGED.filter((n) => {
1504
- const p = join8(dir, n, "SKILL.md");
1958
+ const p = join10(dir, n, "SKILL.md");
1505
1959
  return existsSync5(p) && statSync3(p).isFile();
1506
1960
  });
1507
1961
  log.heading(`${label}: ${dir}`);
1508
- if (present.length) for (const n of present) process.stderr.write(` ${pc8.green("\u2713")} /${n}
1962
+ if (present.length) for (const n of present) process.stderr.write(` ${pc9.green("\u2713")} /${n}
1509
1963
  `);
1510
- else process.stderr.write(` ${pc8.dim("(none installed)")}
1964
+ else process.stderr.write(` ${pc9.dim("(none installed)")}
1511
1965
  `);
1512
1966
  }
1513
1967
  return 0;
@@ -1586,6 +2040,56 @@ program.command("report").description("Export a self-contained HTML dashboard (d
1586
2040
  })
1587
2041
  );
1588
2042
  });
2043
+ var diagram = program.command("diagram").description("Generate & view diagrams (architecture, ER, sequence, flow) for a repository");
2044
+ addGlobalOptions(
2045
+ diagram.command("generate", { isDefault: true }).description("Analyze the repository and generate diagrams as Mermaid").option("--out <dir>", "diagram output directory (relative to repo)", ".edsger/diagrams").option("--no-write", "do not write diagram files into the repo").option("--only <ids>", "comma-separated catalog diagram ids to generate").option("--serve", "open the diagram dashboard after generating")
2046
+ ).action(
2047
+ async (opts) => {
2048
+ await guard(async () => {
2049
+ const g = toGlobal(opts);
2050
+ const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
2051
+ const code = await runDiagramGenerate({
2052
+ ...g,
2053
+ out: opts.out,
2054
+ noWrite: opts.write === false,
2055
+ only
2056
+ });
2057
+ if (code === 0 && opts.serve) {
2058
+ return runDiagramServe({
2059
+ cwd: g.cwd,
2060
+ dir: opts.out,
2061
+ port: 4578,
2062
+ host: "127.0.0.1",
2063
+ open: true
2064
+ });
2065
+ }
2066
+ return code;
2067
+ });
2068
+ }
2069
+ );
2070
+ diagram.command("serve").description("Serve the diagram dashboard locally").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "diagram output directory (relative to repo)", ".edsger/diagrams").option("--port <n>", "port to listen on", "4578").option("--host <host>", "host to bind", "127.0.0.1").option("--no-open", "do not open the browser automatically").action(
2071
+ async (opts) => {
2072
+ await guard(
2073
+ () => runDiagramServe({
2074
+ cwd: resolve2(opts.cwd ?? process.cwd()),
2075
+ dir: opts.dir,
2076
+ port: Number.parseInt(opts.port, 10) || 4578,
2077
+ host: opts.host,
2078
+ open: opts.open !== false
2079
+ })
2080
+ );
2081
+ }
2082
+ );
2083
+ diagram.command("report").description("Export a self-contained HTML diagram viewer (data inlined) for sharing").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "diagram output directory (relative to repo)", ".edsger/diagrams").option("--html <path>", "output HTML file path", "edsger-diagrams.html").option("--open", "open the exported file after writing").action(async (opts) => {
2084
+ await guard(
2085
+ () => runDiagramReport({
2086
+ cwd: resolve2(opts.cwd ?? process.cwd()),
2087
+ dir: opts.dir,
2088
+ html: opts.html,
2089
+ open: Boolean(opts.open)
2090
+ })
2091
+ );
2092
+ });
1589
2093
  addGlobalOptions(
1590
2094
  program.command("pr-review").description("Review a pull request against the review standard (requires gh)").argument("<pr>", "PR number, URL, or branch").option("--repo <owner/repo>", "target repository (defaults to current)").option("--post", "submit the review to GitHub (otherwise preview only)")
1591
2095
  ).action(async (pr, opts) => {
@@ -1634,11 +2138,14 @@ program.addHelpText(
1634
2138
  "after",
1635
2139
  `
1636
2140
  Examples:
1637
- ${pc9.dim("$")} edsger benchmark
1638
- ${pc9.dim("$")} edsger benchmark -C ./my-repo --json
1639
- ${pc9.dim("$")} edsger pr-review 123 --post
1640
- ${pc9.dim("$")} edsger pr-resolve 123 --instruction "fix the failing test" --push
1641
- ${pc9.dim("$")} edsger skills install ${pc9.dim("# enable /benchmark etc. in Claude Code")}
2141
+ ${pc10.dim("$")} edsger benchmark
2142
+ ${pc10.dim("$")} edsger benchmark -C ./my-repo --json
2143
+ ${pc10.dim("$")} edsger diagram ${pc10.dim("# generate diagrams for the current repo")}
2144
+ ${pc10.dim("$")} edsger diagram --serve ${pc10.dim("# \u2026and open them in the browser")}
2145
+ ${pc10.dim("$")} edsger diagram report --html diagrams.html
2146
+ ${pc10.dim("$")} edsger pr-review 123 --post
2147
+ ${pc10.dim("$")} edsger pr-resolve 123 --instruction "fix the failing test" --push
2148
+ ${pc10.dim("$")} edsger skills install ${pc10.dim("# enable /benchmark etc. in Claude Code")}
1642
2149
 
1643
2150
  Auth: the agent uses your Claude Code login (run \`claude\` once to sign in); run \`gh auth login\` for PR commands.
1644
2151
  `