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.
- package/README.md +63 -0
- package/assets/README.md +21 -18
- package/assets/diagrams/base.yaml +351 -0
- package/assets/diagrams/csharp.yaml +48 -0
- package/assets/diagrams/dart.yaml +41 -0
- package/assets/diagrams/go.yaml +44 -0
- package/assets/diagrams/java.yaml +43 -0
- package/assets/diagrams/python.yaml +41 -0
- package/assets/diagrams/typescript.yaml +45 -0
- package/assets/diagrams-viewer/assets/abnfDiagram-VRR7QNED-BuBTl7CD.js +1 -0
- package/assets/diagrams-viewer/assets/arc-OMyuAfpK.js +1 -0
- package/assets/diagrams-viewer/assets/architectureDiagram-ZJ3FMSHR-BtZuD4Oc.js +36 -0
- package/assets/diagrams-viewer/assets/blockDiagram-677ZJIJ3-BXUV3Kw8.js +132 -0
- package/assets/diagrams-viewer/assets/c4Diagram-LMCZKHZV-CB6FLms7.js +10 -0
- package/assets/diagrams-viewer/assets/channel-DfIqGUHQ.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-2Q5K7J3B-mPN4SRuj.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-32BRIVSS-DVv1-dcH.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-5VM5RSS4-CV1HWp5i.js +15 -0
- package/assets/diagrams-viewer/assets/chunk-EX3LRPZG-Dka-2CXP.js +231 -0
- package/assets/diagrams-viewer/assets/chunk-JWPE2WC7-D18DLD3z.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-MOJQB5TN-DZ8recee.js +88 -0
- package/assets/diagrams-viewer/assets/chunk-RYQCIY6F-D-T7U7G_.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-V7JOEXUC-CycRbumY.js +206 -0
- package/assets/diagrams-viewer/assets/chunk-VR4S4FIN-CgtEJekm.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-XXDRQBXY-N1fSDz2n.js +1 -0
- package/assets/diagrams-viewer/assets/classDiagram-OUVF2IWQ-D7S3taPH.js +1 -0
- package/assets/diagrams-viewer/assets/classDiagram-v2-EOCWNBFH-D7S3taPH.js +1 -0
- package/assets/diagrams-viewer/assets/cose-bilkent-JH36ORCC-x2DnNSLL.js +1 -0
- package/assets/diagrams-viewer/assets/cynefin-VYW2F7L2-CWCJCs-i.js +178 -0
- package/assets/diagrams-viewer/assets/cynefinDiagram-TSTJHNR4-mn39JWqG.js +62 -0
- package/assets/diagrams-viewer/assets/cytoscape.esm-CUqq0XTU.js +331 -0
- package/assets/diagrams-viewer/assets/dagre-VKFMJZFB-iOjB5Pax.js +4 -0
- package/assets/diagrams-viewer/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/assets/diagrams-viewer/assets/diagram-FQU43EPY-DIFXEG6K.js +3 -0
- package/assets/diagrams-viewer/assets/diagram-G47NLZAW-CUFys0Kk.js +24 -0
- package/assets/diagrams-viewer/assets/diagram-NH7WQ7WH-ByXjnNJR.js +24 -0
- package/assets/diagrams-viewer/assets/diagram-OA4YK3LP-B7VCnnms.js +30 -0
- package/assets/diagrams-viewer/assets/diagram-WEI45ONY-DdmQeP2M.js +41 -0
- package/assets/diagrams-viewer/assets/diagrams-DFlErBCY.js +314 -0
- package/assets/diagrams-viewer/assets/diagrams-hdYg_gGB.css +1 -0
- package/assets/diagrams-viewer/assets/ebnfDiagram-CCIWWBDH-B54UVMil.js +1 -0
- package/assets/diagrams-viewer/assets/erDiagram-Q63AITRT-zkOgYlyz.js +85 -0
- package/assets/diagrams-viewer/assets/flowDiagram-23GEKE2U-CCNIfraz.js +156 -0
- package/assets/diagrams-viewer/assets/ganttDiagram-NO4QXBWP-DCn2zVSV.js +292 -0
- package/assets/diagrams-viewer/assets/gitGraphDiagram-IHSO6WYX-lB6SW9xm.js +106 -0
- package/assets/diagrams-viewer/assets/graph-C2PyVmSI.js +1 -0
- package/assets/diagrams-viewer/assets/infoDiagram-FWYZ7A6U-CvGfGSmH.js +2 -0
- package/assets/diagrams-viewer/assets/init-Gi6I4Gst.js +1 -0
- package/assets/diagrams-viewer/assets/ishikawaDiagram-FXEZZL3T-DnwhjgE5.js +70 -0
- package/assets/diagrams-viewer/assets/journeyDiagram-5HDEW3XC-C8LVhqZM.js +139 -0
- package/assets/diagrams-viewer/assets/kanban-definition-HUTT4EX6-PYOrsIQc.js +89 -0
- package/assets/diagrams-viewer/assets/katex-C5jXJg4s.js +257 -0
- package/assets/diagrams-viewer/assets/layout-HEHzrzvT.js +1 -0
- package/assets/diagrams-viewer/assets/linear-CIU4B3PA.js +1 -0
- package/assets/diagrams-viewer/assets/map-BQHxRNoK.js +1 -0
- package/assets/diagrams-viewer/assets/mindmap-definition-LN4V7U3C-B-HjOMSw.js +96 -0
- package/assets/diagrams-viewer/assets/ordinal-Cboi1Yqb.js +1 -0
- package/assets/diagrams-viewer/assets/pegDiagram-2B236MQR-dAvsUVxy.js +1 -0
- package/assets/diagrams-viewer/assets/pieDiagram-ENE6RG2P-W-6EgmyD.js +39 -0
- package/assets/diagrams-viewer/assets/quadrantDiagram-ABIIQ3AL-SlUlWOO8.js +7 -0
- package/assets/diagrams-viewer/assets/railroadDiagram-RFXS5EU6-KjtAtTVy.js +1 -0
- package/assets/diagrams-viewer/assets/requirementDiagram-TGXJPOKE-DZtlwaER.js +84 -0
- package/assets/diagrams-viewer/assets/sankeyDiagram-HTMAVEWB-1Ae4oRc3.js +40 -0
- package/assets/diagrams-viewer/assets/sequenceDiagram-DBY2YBRQ-DU-x0cLP.js +162 -0
- package/assets/diagrams-viewer/assets/sizeCapture-X5ZJPWSS-Cemh16BZ.js +1 -0
- package/assets/diagrams-viewer/assets/stateDiagram-2N3HPSRC-CtfrA6gP.js +1 -0
- package/assets/diagrams-viewer/assets/stateDiagram-v2-6OUMAXLB-BU13FASH.js +1 -0
- package/assets/diagrams-viewer/assets/swimlanes-5IMT3BWC-BkZY9GJ7.js +2 -0
- package/assets/diagrams-viewer/assets/swimlanesDiagram-G3AALYLV-03GXtgC-.js +8 -0
- package/assets/diagrams-viewer/assets/timeline-definition-FHXFAJF6-DikyXYB3.js +120 -0
- package/assets/diagrams-viewer/assets/vennDiagram-L72KCM5P-C_7veK57.js +34 -0
- package/assets/diagrams-viewer/assets/wardleyDiagram-EHGQE667-Das_YJGv.js +78 -0
- package/assets/diagrams-viewer/assets/xychartDiagram-FW5EYKEG-B5fwef3a.js +7 -0
- package/assets/diagrams-viewer/diagrams.html +14 -0
- package/assets/diagrams-viewer-single.html +3576 -0
- package/assets/manifest.yaml +5 -1
- package/assets/schema/diagrams.schema.json +100 -0
- package/assets/skills/diagram/SKILL.md +70 -0
- package/assets/viewer/assets/index-C7hwGkuU.js +3 -0
- package/assets/viewer/assets/index-Dhc-7q5o.css +1 -0
- package/assets/viewer/index.html +14 -0
- package/assets/viewer-single.html +16 -0
- package/dist/index.js +776 -38
- package/dist/index.js.map +1 -1
- 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
|
|
4
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
5
5
|
import { resolve as resolve2 } from "path";
|
|
6
6
|
import { Command, Option } from "commander";
|
|
7
|
-
import
|
|
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
|
|
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
|
|
1228
|
-
mkdirSync as
|
|
1229
|
-
readFileSync as
|
|
1230
|
-
readdirSync as
|
|
1884
|
+
existsSync as existsSync5,
|
|
1885
|
+
mkdirSync as mkdirSync4,
|
|
1886
|
+
readFileSync as readFileSync3,
|
|
1887
|
+
readdirSync as readdirSync3,
|
|
1231
1888
|
rmSync as rmSync2,
|
|
1232
|
-
statSync as
|
|
1233
|
-
writeFileSync as
|
|
1889
|
+
statSync as statSync3,
|
|
1890
|
+
writeFileSync as writeFileSync5
|
|
1234
1891
|
} from "fs";
|
|
1235
1892
|
import { homedir as homedir2 } from "os";
|
|
1236
|
-
import { join as
|
|
1237
|
-
import
|
|
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 ?
|
|
1897
|
+
return scope.global ? join10(homedir2(), ".claude", "skills") : join10(scope.cwd, ".claude", "skills");
|
|
1241
1898
|
}
|
|
1242
1899
|
function sourceSkillsDir() {
|
|
1243
|
-
return
|
|
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 (!
|
|
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
|
-
|
|
1254
|
-
const available =
|
|
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 =
|
|
1258
|
-
if (
|
|
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(
|
|
1264
|
-
const skillFile =
|
|
1265
|
-
const rewritten =
|
|
1266
|
-
|
|
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 ${
|
|
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 =
|
|
1285
|
-
if (
|
|
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 =
|
|
1302
|
-
return
|
|
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(` ${
|
|
1962
|
+
if (present.length) for (const n of present) process.stderr.write(` ${pc9.green("\u2713")} /${n}
|
|
1306
1963
|
`);
|
|
1307
|
-
else process.stderr.write(` ${
|
|
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(
|
|
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
|
-
() =>
|
|
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
|
-
${
|
|
1407
|
-
${
|
|
1408
|
-
${
|
|
1409
|
-
${
|
|
1410
|
-
${
|
|
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
|
`
|