edsger 0.81.1 → 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 (85) hide show
  1. package/README.md +63 -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/assets/viewer/assets/index-C7hwGkuU.js +3 -0
  80. package/assets/viewer/assets/index-Dhc-7q5o.css +1 -0
  81. package/assets/viewer/index.html +14 -0
  82. package/assets/viewer-single.html +16 -0
  83. package/dist/index.js +776 -38
  84. package/dist/index.js.map +1 -1
  85. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync3 } from "fs";
4
+ import { readFileSync as readFileSync4 } from "fs";
5
5
  import { resolve as resolve2 } from "path";
6
6
  import { Command, Option } from "commander";
7
- import pc7 from "picocolors";
7
+ import pc10 from "picocolors";
8
8
 
9
9
  // src/commands/benchmark.ts
10
- import { mkdirSync, writeFileSync } from "fs";
10
+ import { appendFileSync, mkdirSync, writeFileSync } from "fs";
11
11
  import { basename, join as join3 } from "path";
12
12
  import pc3 from "picocolors";
13
13
 
@@ -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 ?? [],
@@ -490,6 +518,30 @@ function summarizeTool(name = "tool", input = {}) {
490
518
  }
491
519
  }
492
520
 
521
+ // src/core/git.ts
522
+ import { execFileSync as execFileSync2 } from "child_process";
523
+ function tryGit(args, cwd) {
524
+ try {
525
+ return execFileSync2("git", args, {
526
+ cwd,
527
+ encoding: "utf8",
528
+ stdio: ["ignore", "pipe", "ignore"]
529
+ }).trim();
530
+ } catch {
531
+ return null;
532
+ }
533
+ }
534
+ function gitInfo(cwd) {
535
+ const commit = tryGit(["rev-parse", "--short", "HEAD"], cwd);
536
+ const branch = tryGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
537
+ const status = tryGit(["status", "--porcelain"], cwd);
538
+ return {
539
+ commit,
540
+ branch: branch && branch !== "HEAD" ? branch : null,
541
+ dirty: Boolean(status && status.length > 0)
542
+ };
543
+ }
544
+
493
545
  // src/core/report.ts
494
546
  function scoreBenchmark(standard, result) {
495
547
  const scale = standard.scoreScale || 4;
@@ -683,6 +735,28 @@ var ResolveResultSchema = z.object({
683
735
  testsRun: z.string().nullable().default(null).describe("Command used to verify, and its outcome"),
684
736
  remaining: z.array(z.string()).default([]).describe("Anything still unresolved")
685
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
+ });
686
760
 
687
761
  // src/commands/benchmark.ts
688
762
  async function runBenchmark(opts) {
@@ -715,6 +789,7 @@ async function runBenchmark(opts) {
715
789
  const scored = scoreBenchmark(standard, result);
716
790
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
717
791
  const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
792
+ const git = gitInfo(opts.cwd);
718
793
  const md = renderBenchmarkMarkdown(scored, {
719
794
  repo: basename(opts.cwd),
720
795
  languages: resolved.languages,
@@ -728,6 +803,7 @@ async function runBenchmark(opts) {
728
803
  generatedAt,
729
804
  model: opts.model,
730
805
  repo: basename(opts.cwd),
806
+ git,
731
807
  languages: resolved.languages,
732
808
  standard: { name: standard.name, version: standard.version, layers: resolved.layers },
733
809
  overall: scored.overall,
@@ -739,6 +815,19 @@ async function runBenchmark(opts) {
739
815
  recommendations: scored.recommendations,
740
816
  costUsd
741
817
  };
818
+ const historyRecord = {
819
+ runId,
820
+ generatedAt,
821
+ commit: git.commit,
822
+ branch: git.branch,
823
+ dirty: git.dirty,
824
+ model: opts.model,
825
+ standardVersion: standard.version,
826
+ overall: scored.overall,
827
+ grade: scored.grade,
828
+ categories: Object.fromEntries(scored.categories.map((c) => [c.id, c.fraction])),
829
+ costUsd
830
+ };
742
831
  if (!opts.noWrite && !opts.dryRun) {
743
832
  const baseDir = join3(opts.cwd, opts.out);
744
833
  const mdOut = md + "\n";
@@ -749,6 +838,7 @@ async function runBenchmark(opts) {
749
838
  writeFileSync(join3(runDir, "report.json"), jsonOut, "utf8");
750
839
  writeFileSync(join3(baseDir, "report.md"), mdOut, "utf8");
751
840
  writeFileSync(join3(baseDir, "report.json"), jsonOut, "utf8");
841
+ appendFileSync(join3(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
752
842
  log.success(
753
843
  `Report written to ${pc3.bold(join3(opts.out, "report.md"))} ${pc3.dim(`(run archived at ${join3(opts.out, "runs", runId)})`)}`
754
844
  );
@@ -1221,49 +1311,616 @@ function buildPrompt3(ctx, feedback, instruction) {
1221
1311
  ].join("\n");
1222
1312
  }
1223
1313
 
1314
+ // src/commands/serve.ts
1315
+ import { join as join7 } from "path";
1316
+
1317
+ // src/core/dashboard.ts
1318
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
1319
+ import { join as join5 } from "path";
1320
+ function assembleFrom(dir, latestFile) {
1321
+ const latest = readJson(join5(dir, latestFile));
1322
+ const history = readJsonl(join5(dir, "history.jsonl"));
1323
+ const runs = {};
1324
+ const runsDir = join5(dir, "runs");
1325
+ if (existsSync3(runsDir)) {
1326
+ for (const entry of readdirSync2(runsDir)) {
1327
+ const report = readJson(join5(runsDir, entry, latestFile));
1328
+ if (report) runs[entry] = report;
1329
+ }
1330
+ }
1331
+ const repo = latest && typeof latest === "object" && "repo" in latest ? latest.repo : void 0;
1332
+ return { repo, latest, history, runs };
1333
+ }
1334
+ function assembleData(benchmarkDir) {
1335
+ return assembleFrom(benchmarkDir, "report.json");
1336
+ }
1337
+ function assembleDiagramData(diagramsDir) {
1338
+ return assembleFrom(diagramsDir, "diagrams.json");
1339
+ }
1340
+ function hasData(data) {
1341
+ return Boolean(data.latest) || data.history.length > 0;
1342
+ }
1343
+ function findViewerDir(subdir = "viewer", index = "index.html") {
1344
+ const dir = join5(findBundledAssetsDir(), subdir);
1345
+ if (!existsSync3(join5(dir, index))) {
1346
+ throw new Error(
1347
+ `Dashboard viewer assets not found (${subdir}/${index}). Reinstall edsger, or run \`npm run sync-viewer\` in a dev checkout.`
1348
+ );
1349
+ }
1350
+ return dir;
1351
+ }
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)) {
1358
+ throw new Error(
1359
+ `Single-file viewer template not found (${file}). Reinstall edsger, or run \`npm run sync-viewer\`.`
1360
+ );
1361
+ }
1362
+ return readFileSync2(path, "utf8");
1363
+ }
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) {
1371
+ const json = JSON.stringify(data).replace(/<\//g, "<\\/");
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__");
1386
+ }
1387
+ function readJson(path) {
1388
+ try {
1389
+ return JSON.parse(readFileSync2(path, "utf8"));
1390
+ } catch {
1391
+ return null;
1392
+ }
1393
+ }
1394
+ function readJsonl(path) {
1395
+ if (!existsSync3(path)) return [];
1396
+ const out = [];
1397
+ for (const line of readFileSync2(path, "utf8").split("\n")) {
1398
+ const t = line.trim();
1399
+ if (!t) continue;
1400
+ try {
1401
+ out.push(JSON.parse(t));
1402
+ } catch {
1403
+ }
1404
+ }
1405
+ return out;
1406
+ }
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
+
1414
+ // src/core/open.ts
1415
+ import { spawn } from "child_process";
1416
+ function openInBrowser(target) {
1417
+ const platform = process.platform;
1418
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
1419
+ const args = platform === "win32" ? ["/c", "start", "", target] : [target];
1420
+ try {
1421
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
1422
+ } catch {
1423
+ }
1424
+ }
1425
+
1426
+ // src/core/viewer-server.ts
1427
+ var MIME = {
1428
+ ".html": "text/html; charset=utf-8",
1429
+ ".js": "text/javascript; charset=utf-8",
1430
+ ".css": "text/css; charset=utf-8",
1431
+ ".json": "application/json; charset=utf-8",
1432
+ ".svg": "image/svg+xml",
1433
+ ".map": "application/json",
1434
+ ".ico": "image/x-icon",
1435
+ ".woff2": "font/woff2",
1436
+ ".woff": "font/woff"
1437
+ };
1438
+ async function startViewerServer(opts) {
1439
+ const server = createServer((req, res) => {
1440
+ const url = (req.url ?? "/").split("?")[0] ?? "/";
1441
+ if (url === opts.dataRoute) {
1442
+ res.writeHead(200, {
1443
+ "content-type": "application/json; charset=utf-8",
1444
+ "cache-control": "no-store"
1445
+ });
1446
+ res.end(JSON.stringify(opts.getData()));
1447
+ return;
1448
+ }
1449
+ const rel = url === "/" ? opts.indexFile : decodeURIComponent(url.replace(/^\/+/, ""));
1450
+ const safe = normalize(rel).replace(/^(\.\.(?:[/\\]|$))+/, "");
1451
+ let file = join6(opts.viewerDir, safe);
1452
+ if (!file.startsWith(opts.viewerDir)) {
1453
+ res.writeHead(403);
1454
+ res.end("forbidden");
1455
+ return;
1456
+ }
1457
+ if (!existsSync4(file) || statSync2(file).isDirectory())
1458
+ file = join6(opts.viewerDir, opts.indexFile);
1459
+ res.writeHead(200, { "content-type": MIME[extname(file)] ?? "application/octet-stream" });
1460
+ createReadStream(file).pipe(res);
1461
+ });
1462
+ return new Promise((resolve3, reject) => {
1463
+ server.on("error", reject);
1464
+ server.listen(opts.port, opts.host, () => {
1465
+ const addr = server.address();
1466
+ const port = typeof addr === "object" && addr ? addr.port : opts.port;
1467
+ const shownHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
1468
+ const url = `http://${shownHost}:${port}/`;
1469
+ if (opts.emptyHint) log.warn(opts.emptyHint);
1470
+ log.success(`Dashboard at ${pc6.bold(pc6.cyan(url))} ${pc6.dim("(Ctrl-C to stop)")}`);
1471
+ if (opts.open) openInBrowser(url);
1472
+ });
1473
+ const shutdown = () => {
1474
+ server.close(() => resolve3(0));
1475
+ };
1476
+ process.on("SIGINT", shutdown);
1477
+ process.on("SIGTERM", shutdown);
1478
+ });
1479
+ }
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
+
1497
+ // src/commands/report.ts
1498
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
1499
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join8 } from "path";
1500
+ import pc7 from "picocolors";
1501
+ async function runReport(opts) {
1502
+ const benchmarkDir = join8(opts.cwd, opts.dir);
1503
+ const data = assembleData(benchmarkDir);
1504
+ if (!hasData(data)) {
1505
+ log.error(`No benchmark data in ${opts.dir}. Run \`edsger benchmark\` first.`);
1506
+ return 1;
1507
+ }
1508
+ const html = renderSingleFile(readViewerSingleTemplate(), data);
1509
+ const out = isAbsolute2(opts.html) ? opts.html : join8(opts.cwd, opts.html);
1510
+ mkdirSync2(dirname2(out), { recursive: true });
1511
+ writeFileSync3(out, html, "utf8");
1512
+ log.success(
1513
+ `Wrote self-contained report to ${pc7.bold(opts.html)} ${pc7.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
1514
+ );
1515
+ if (opts.open) openInBrowser(out);
1516
+ return 0;
1517
+ }
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
+
1224
1881
  // src/commands/skills.ts
1225
1882
  import {
1226
1883
  cpSync,
1227
- existsSync as existsSync3,
1228
- mkdirSync as mkdirSync2,
1229
- readFileSync as readFileSync2,
1230
- readdirSync as readdirSync2,
1884
+ existsSync as existsSync5,
1885
+ mkdirSync as mkdirSync4,
1886
+ readFileSync as readFileSync3,
1887
+ readdirSync as readdirSync3,
1231
1888
  rmSync as rmSync2,
1232
- statSync as statSync2,
1233
- writeFileSync as writeFileSync3
1889
+ statSync as statSync3,
1890
+ writeFileSync as writeFileSync5
1234
1891
  } from "fs";
1235
1892
  import { homedir as homedir2 } from "os";
1236
- import { join as join5 } from "path";
1237
- import pc6 from "picocolors";
1893
+ import { join as join10 } from "path";
1894
+ import pc9 from "picocolors";
1238
1895
  var MANAGED = ["benchmark", "pr-review", "pr-resolve"];
1239
1896
  function targetSkillsDir(scope) {
1240
- return scope.global ? join5(homedir2(), ".claude", "skills") : join5(scope.cwd, ".claude", "skills");
1897
+ return scope.global ? join10(homedir2(), ".claude", "skills") : join10(scope.cwd, ".claude", "skills");
1241
1898
  }
1242
1899
  function sourceSkillsDir() {
1243
- return join5(findBundledAssetsDir(), "skills");
1900
+ return join10(findBundledAssetsDir(), "skills");
1244
1901
  }
1245
1902
  async function runSkillsInstall(opts) {
1246
1903
  const assetsDir = findBundledAssetsDir();
1247
1904
  const src = sourceSkillsDir();
1248
- if (!existsSync3(src)) {
1905
+ if (!existsSync5(src)) {
1249
1906
  log.error(`Bundled skills not found at ${src}. Reinstall edsger.`);
1250
1907
  return 1;
1251
1908
  }
1252
1909
  const dest = targetSkillsDir(opts);
1253
- mkdirSync2(dest, { recursive: true });
1254
- const available = readdirSync2(src).filter((n) => existsSync3(join5(src, n, "SKILL.md")));
1910
+ mkdirSync4(dest, { recursive: true });
1911
+ const available = readdirSync3(src).filter((n) => existsSync5(join10(src, n, "SKILL.md")));
1255
1912
  let installed = 0;
1256
1913
  for (const name of available) {
1257
- const outDir = join5(dest, name);
1258
- if (existsSync3(outDir) && !opts.force) {
1914
+ const outDir = join10(dest, name);
1915
+ if (existsSync5(outDir) && !opts.force) {
1259
1916
  log.warn(`skill "${name}" already exists \u2014 use --force to overwrite`);
1260
1917
  continue;
1261
1918
  }
1262
1919
  rmSync2(outDir, { recursive: true, force: true });
1263
- cpSync(join5(src, name), outDir, { recursive: true });
1264
- const skillFile = join5(outDir, "SKILL.md");
1265
- const rewritten = readFileSync2(skillFile, "utf8").split("${CLAUDE_PLUGIN_ROOT}").join(assetsDir);
1266
- writeFileSync3(skillFile, rewritten, "utf8");
1920
+ cpSync(join10(src, name), outDir, { recursive: true });
1921
+ const skillFile = join10(outDir, "SKILL.md");
1922
+ const rewritten = readFileSync3(skillFile, "utf8").split("${CLAUDE_PLUGIN_ROOT}").join(assetsDir);
1923
+ writeFileSync5(skillFile, rewritten, "utf8");
1267
1924
  installed++;
1268
1925
  log.success(`installed /${name}`);
1269
1926
  }
@@ -1271,7 +1928,7 @@ async function runSkillsInstall(opts) {
1271
1928
  log.info("Nothing installed.");
1272
1929
  return 0;
1273
1930
  }
1274
- log.heading(`Installed ${installed} skill(s) into ${pc6.bold(dest)}`);
1931
+ log.heading(`Installed ${installed} skill(s) into ${pc9.bold(dest)}`);
1275
1932
  log.step(`scope: ${opts.global ? "user (all projects)" : "this project"}`);
1276
1933
  log.step(`standards: ${assetsDir}`);
1277
1934
  log.step("Open Claude Code and run /benchmark, /pr-review, or /pr-resolve.");
@@ -1281,8 +1938,8 @@ async function runSkillsUninstall(opts) {
1281
1938
  const dest = targetSkillsDir(opts);
1282
1939
  let removed = 0;
1283
1940
  for (const name of MANAGED) {
1284
- const dir = join5(dest, name);
1285
- if (existsSync3(dir)) {
1941
+ const dir = join10(dest, name);
1942
+ if (existsSync5(dir)) {
1286
1943
  rmSync2(dir, { recursive: true, force: true });
1287
1944
  removed++;
1288
1945
  log.success(`removed /${name}`);
@@ -1298,20 +1955,20 @@ async function runSkillsList(opts) {
1298
1955
  ]) {
1299
1956
  const dir = targetSkillsDir(scope);
1300
1957
  const present = MANAGED.filter((n) => {
1301
- const p = join5(dir, n, "SKILL.md");
1302
- return existsSync3(p) && statSync2(p).isFile();
1958
+ const p = join10(dir, n, "SKILL.md");
1959
+ return existsSync5(p) && statSync3(p).isFile();
1303
1960
  });
1304
1961
  log.heading(`${label}: ${dir}`);
1305
- if (present.length) for (const n of present) process.stderr.write(` ${pc6.green("\u2713")} /${n}
1962
+ if (present.length) for (const n of present) process.stderr.write(` ${pc9.green("\u2713")} /${n}
1306
1963
  `);
1307
- else process.stderr.write(` ${pc6.dim("(none installed)")}
1964
+ else process.stderr.write(` ${pc9.dim("(none installed)")}
1308
1965
  `);
1309
1966
  }
1310
1967
  return 0;
1311
1968
  }
1312
1969
 
1313
1970
  // src/index.ts
1314
- var pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
1971
+ var pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
1315
1972
  var VERSION = pkg.version;
1316
1973
  var DEFAULT_MODEL = "claude-sonnet-4-6";
1317
1974
  function toGlobal(raw) {
@@ -1349,10 +2006,88 @@ program.name("edsger").description(
1349
2006
  "Agentic CLI to benchmark repositories and review/resolve pull requests\nagainst industrial-grade standards (from edsger-assets, fully overridable)."
1350
2007
  ).version(VERSION, "-V, --version");
1351
2008
  addGlobalOptions(
1352
- program.command("benchmark").description("Score a repository against a benchmark rubric and write a report").option("--out <dir>", "report output directory (relative to repo)", ".edsger/benchmark").option("--no-write", "do not write report files into the repo")
2009
+ program.command("benchmark").description("Score a repository against a benchmark rubric and write a report").option("--out <dir>", "report output directory (relative to repo)", ".edsger/benchmark").option("--no-write", "do not write report files into the repo").option("--serve", "open the dashboard after benchmarking")
1353
2010
  ).action(async (opts) => {
2011
+ await guard(async () => {
2012
+ const g = toGlobal(opts);
2013
+ const code = await runBenchmark({ ...g, out: opts.out, noWrite: opts.write === false });
2014
+ if (code === 0 && opts.serve) {
2015
+ return runServe({ cwd: g.cwd, dir: opts.out, port: 4577, host: "127.0.0.1", open: true });
2016
+ }
2017
+ return code;
2018
+ });
2019
+ });
2020
+ program.command("serve").description("Serve the benchmark dashboard (current, history, trends) locally").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "benchmark output directory (relative to repo)", ".edsger/benchmark").option("--port <n>", "port to listen on", "4577").option("--host <host>", "host to bind", "127.0.0.1").option("--no-open", "do not open the browser automatically").action(
2021
+ async (opts) => {
2022
+ await guard(
2023
+ () => runServe({
2024
+ cwd: resolve2(opts.cwd ?? process.cwd()),
2025
+ dir: opts.dir,
2026
+ port: Number.parseInt(opts.port, 10) || 4577,
2027
+ host: opts.host,
2028
+ open: opts.open !== false
2029
+ })
2030
+ );
2031
+ }
2032
+ );
2033
+ program.command("report").description("Export a self-contained HTML dashboard (data inlined) for sharing").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "benchmark output directory (relative to repo)", ".edsger/benchmark").option("--html <path>", "output HTML file path", "edsger-report.html").option("--open", "open the exported file after writing").action(async (opts) => {
1354
2034
  await guard(
1355
- () => runBenchmark({ ...toGlobal(opts), out: opts.out, noWrite: opts.write === false })
2035
+ () => runReport({
2036
+ cwd: resolve2(opts.cwd ?? process.cwd()),
2037
+ dir: opts.dir,
2038
+ html: opts.html,
2039
+ open: Boolean(opts.open)
2040
+ })
2041
+ );
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
+ })
1356
2091
  );
1357
2092
  });
1358
2093
  addGlobalOptions(
@@ -1403,11 +2138,14 @@ program.addHelpText(
1403
2138
  "after",
1404
2139
  `
1405
2140
  Examples:
1406
- ${pc7.dim("$")} edsger benchmark
1407
- ${pc7.dim("$")} edsger benchmark -C ./my-repo --json
1408
- ${pc7.dim("$")} edsger pr-review 123 --post
1409
- ${pc7.dim("$")} edsger pr-resolve 123 --instruction "fix the failing test" --push
1410
- ${pc7.dim("$")} edsger skills install ${pc7.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")}
1411
2149
 
1412
2150
  Auth: the agent uses your Claude Code login (run \`claude\` once to sign in); run \`gh auth login\` for PR commands.
1413
2151
  `