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