edsger 0.81.1 → 0.82.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 +27 -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 +268 -37
- 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 pc9 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
|
|
|
@@ -490,6 +490,30 @@ function summarizeTool(name = "tool", input = {}) {
|
|
|
490
490
|
}
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
+
// src/core/git.ts
|
|
494
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
495
|
+
function tryGit(args, cwd) {
|
|
496
|
+
try {
|
|
497
|
+
return execFileSync2("git", args, {
|
|
498
|
+
cwd,
|
|
499
|
+
encoding: "utf8",
|
|
500
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
501
|
+
}).trim();
|
|
502
|
+
} catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function gitInfo(cwd) {
|
|
507
|
+
const commit = tryGit(["rev-parse", "--short", "HEAD"], cwd);
|
|
508
|
+
const branch = tryGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
509
|
+
const status = tryGit(["status", "--porcelain"], cwd);
|
|
510
|
+
return {
|
|
511
|
+
commit,
|
|
512
|
+
branch: branch && branch !== "HEAD" ? branch : null,
|
|
513
|
+
dirty: Boolean(status && status.length > 0)
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
493
517
|
// src/core/report.ts
|
|
494
518
|
function scoreBenchmark(standard, result) {
|
|
495
519
|
const scale = standard.scoreScale || 4;
|
|
@@ -715,6 +739,7 @@ async function runBenchmark(opts) {
|
|
|
715
739
|
const scored = scoreBenchmark(standard, result);
|
|
716
740
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
717
741
|
const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
742
|
+
const git = gitInfo(opts.cwd);
|
|
718
743
|
const md = renderBenchmarkMarkdown(scored, {
|
|
719
744
|
repo: basename(opts.cwd),
|
|
720
745
|
languages: resolved.languages,
|
|
@@ -728,6 +753,7 @@ async function runBenchmark(opts) {
|
|
|
728
753
|
generatedAt,
|
|
729
754
|
model: opts.model,
|
|
730
755
|
repo: basename(opts.cwd),
|
|
756
|
+
git,
|
|
731
757
|
languages: resolved.languages,
|
|
732
758
|
standard: { name: standard.name, version: standard.version, layers: resolved.layers },
|
|
733
759
|
overall: scored.overall,
|
|
@@ -739,6 +765,19 @@ async function runBenchmark(opts) {
|
|
|
739
765
|
recommendations: scored.recommendations,
|
|
740
766
|
costUsd
|
|
741
767
|
};
|
|
768
|
+
const historyRecord = {
|
|
769
|
+
runId,
|
|
770
|
+
generatedAt,
|
|
771
|
+
commit: git.commit,
|
|
772
|
+
branch: git.branch,
|
|
773
|
+
dirty: git.dirty,
|
|
774
|
+
model: opts.model,
|
|
775
|
+
standardVersion: standard.version,
|
|
776
|
+
overall: scored.overall,
|
|
777
|
+
grade: scored.grade,
|
|
778
|
+
categories: Object.fromEntries(scored.categories.map((c) => [c.id, c.fraction])),
|
|
779
|
+
costUsd
|
|
780
|
+
};
|
|
742
781
|
if (!opts.noWrite && !opts.dryRun) {
|
|
743
782
|
const baseDir = join3(opts.cwd, opts.out);
|
|
744
783
|
const mdOut = md + "\n";
|
|
@@ -749,6 +788,7 @@ async function runBenchmark(opts) {
|
|
|
749
788
|
writeFileSync(join3(runDir, "report.json"), jsonOut, "utf8");
|
|
750
789
|
writeFileSync(join3(baseDir, "report.md"), mdOut, "utf8");
|
|
751
790
|
writeFileSync(join3(baseDir, "report.json"), jsonOut, "utf8");
|
|
791
|
+
appendFileSync(join3(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
|
|
752
792
|
log.success(
|
|
753
793
|
`Report written to ${pc3.bold(join3(opts.out, "report.md"))} ${pc3.dim(`(run archived at ${join3(opts.out, "runs", runId)})`)}`
|
|
754
794
|
);
|
|
@@ -1221,49 +1261,212 @@ function buildPrompt3(ctx, feedback, instruction) {
|
|
|
1221
1261
|
].join("\n");
|
|
1222
1262
|
}
|
|
1223
1263
|
|
|
1264
|
+
// src/commands/serve.ts
|
|
1265
|
+
import { createReadStream, existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
1266
|
+
import { createServer } from "http";
|
|
1267
|
+
import { extname, join as join6, normalize } from "path";
|
|
1268
|
+
import pc6 from "picocolors";
|
|
1269
|
+
|
|
1270
|
+
// src/core/dashboard.ts
|
|
1271
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
1272
|
+
import { join as join5 } from "path";
|
|
1273
|
+
function assembleData(benchmarkDir) {
|
|
1274
|
+
const latest = readJson(join5(benchmarkDir, "report.json"));
|
|
1275
|
+
const history = readJsonl(join5(benchmarkDir, "history.jsonl"));
|
|
1276
|
+
const runs = {};
|
|
1277
|
+
const runsDir = join5(benchmarkDir, "runs");
|
|
1278
|
+
if (existsSync3(runsDir)) {
|
|
1279
|
+
for (const entry of readdirSync2(runsDir)) {
|
|
1280
|
+
const report = readJson(join5(runsDir, entry, "report.json"));
|
|
1281
|
+
if (report) runs[entry] = report;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const repo = latest && typeof latest === "object" && "repo" in latest ? latest.repo : void 0;
|
|
1285
|
+
return { repo, latest, history, runs };
|
|
1286
|
+
}
|
|
1287
|
+
function hasData(data) {
|
|
1288
|
+
return Boolean(data.latest) || data.history.length > 0;
|
|
1289
|
+
}
|
|
1290
|
+
function findViewerDir() {
|
|
1291
|
+
const dir = join5(findBundledAssetsDir(), "viewer");
|
|
1292
|
+
if (!existsSync3(join5(dir, "index.html"))) {
|
|
1293
|
+
throw new Error(
|
|
1294
|
+
"Dashboard viewer assets not found. Reinstall edsger, or run `npm run sync-viewer` in a dev checkout."
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
return dir;
|
|
1298
|
+
}
|
|
1299
|
+
function readViewerSingleTemplate() {
|
|
1300
|
+
const file = join5(findBundledAssetsDir(), "viewer-single.html");
|
|
1301
|
+
if (!existsSync3(file)) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
"Single-file viewer template not found. Reinstall edsger, or run `npm run sync-viewer`."
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
return readFileSync2(file, "utf8");
|
|
1307
|
+
}
|
|
1308
|
+
function renderSingleFile(template, data) {
|
|
1309
|
+
const json = JSON.stringify(data).replace(/<\//g, "<\\/");
|
|
1310
|
+
const tag = `<script>window.__EDSGER_DATA__=${json}</script>`;
|
|
1311
|
+
if (template.includes("</head>")) return template.replace("</head>", `${tag}</head>`);
|
|
1312
|
+
return template.replace("<body>", `<body>${tag}`);
|
|
1313
|
+
}
|
|
1314
|
+
function readJson(path) {
|
|
1315
|
+
try {
|
|
1316
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
1317
|
+
} catch {
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
function readJsonl(path) {
|
|
1322
|
+
if (!existsSync3(path)) return [];
|
|
1323
|
+
const out = [];
|
|
1324
|
+
for (const line of readFileSync2(path, "utf8").split("\n")) {
|
|
1325
|
+
const t = line.trim();
|
|
1326
|
+
if (!t) continue;
|
|
1327
|
+
try {
|
|
1328
|
+
out.push(JSON.parse(t));
|
|
1329
|
+
} catch {
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return out;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/core/open.ts
|
|
1336
|
+
import { spawn } from "child_process";
|
|
1337
|
+
function openInBrowser(target) {
|
|
1338
|
+
const platform = process.platform;
|
|
1339
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
1340
|
+
const args = platform === "win32" ? ["/c", "start", "", target] : [target];
|
|
1341
|
+
try {
|
|
1342
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
1343
|
+
} catch {
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/commands/serve.ts
|
|
1348
|
+
var MIME = {
|
|
1349
|
+
".html": "text/html; charset=utf-8",
|
|
1350
|
+
".js": "text/javascript; charset=utf-8",
|
|
1351
|
+
".css": "text/css; charset=utf-8",
|
|
1352
|
+
".json": "application/json; charset=utf-8",
|
|
1353
|
+
".svg": "image/svg+xml",
|
|
1354
|
+
".map": "application/json",
|
|
1355
|
+
".ico": "image/x-icon",
|
|
1356
|
+
".woff2": "font/woff2",
|
|
1357
|
+
".woff": "font/woff"
|
|
1358
|
+
};
|
|
1359
|
+
async function runServe(opts) {
|
|
1360
|
+
const benchmarkDir = join6(opts.cwd, opts.dir);
|
|
1361
|
+
const viewerDir = findViewerDir();
|
|
1362
|
+
const server = createServer((req, res) => {
|
|
1363
|
+
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
1364
|
+
if (url === "/edsger-data.json") {
|
|
1365
|
+
res.writeHead(200, {
|
|
1366
|
+
"content-type": "application/json; charset=utf-8",
|
|
1367
|
+
"cache-control": "no-store"
|
|
1368
|
+
});
|
|
1369
|
+
res.end(JSON.stringify(assembleData(benchmarkDir)));
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const rel = url === "/" ? "index.html" : decodeURIComponent(url.replace(/^\/+/, ""));
|
|
1373
|
+
const safe = normalize(rel).replace(/^(\.\.(?:[/\\]|$))+/, "");
|
|
1374
|
+
let file = join6(viewerDir, safe);
|
|
1375
|
+
if (!file.startsWith(viewerDir)) {
|
|
1376
|
+
res.writeHead(403);
|
|
1377
|
+
res.end("forbidden");
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (!existsSync4(file) || statSync2(file).isDirectory()) file = join6(viewerDir, "index.html");
|
|
1381
|
+
res.writeHead(200, { "content-type": MIME[extname(file)] ?? "application/octet-stream" });
|
|
1382
|
+
createReadStream(file).pipe(res);
|
|
1383
|
+
});
|
|
1384
|
+
return new Promise((resolve3, reject) => {
|
|
1385
|
+
server.on("error", reject);
|
|
1386
|
+
server.listen(opts.port, opts.host, () => {
|
|
1387
|
+
const addr = server.address();
|
|
1388
|
+
const port = typeof addr === "object" && addr ? addr.port : opts.port;
|
|
1389
|
+
const shownHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
|
|
1390
|
+
const url = `http://${shownHost}:${port}/`;
|
|
1391
|
+
if (!hasData(assembleData(benchmarkDir))) {
|
|
1392
|
+
log.warn(`No benchmark data in ${opts.dir} yet \u2014 run \`edsger benchmark\` first.`);
|
|
1393
|
+
}
|
|
1394
|
+
log.success(`Dashboard at ${pc6.bold(pc6.cyan(url))} ${pc6.dim("(Ctrl-C to stop)")}`);
|
|
1395
|
+
if (opts.open) openInBrowser(url);
|
|
1396
|
+
});
|
|
1397
|
+
const shutdown = () => {
|
|
1398
|
+
server.close(() => resolve3(0));
|
|
1399
|
+
};
|
|
1400
|
+
process.on("SIGINT", shutdown);
|
|
1401
|
+
process.on("SIGTERM", shutdown);
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/commands/report.ts
|
|
1406
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1407
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join7 } from "path";
|
|
1408
|
+
import pc7 from "picocolors";
|
|
1409
|
+
async function runReport(opts) {
|
|
1410
|
+
const benchmarkDir = join7(opts.cwd, opts.dir);
|
|
1411
|
+
const data = assembleData(benchmarkDir);
|
|
1412
|
+
if (!hasData(data)) {
|
|
1413
|
+
log.error(`No benchmark data in ${opts.dir}. Run \`edsger benchmark\` first.`);
|
|
1414
|
+
return 1;
|
|
1415
|
+
}
|
|
1416
|
+
const html = renderSingleFile(readViewerSingleTemplate(), data);
|
|
1417
|
+
const out = isAbsolute2(opts.html) ? opts.html : join7(opts.cwd, opts.html);
|
|
1418
|
+
mkdirSync2(dirname2(out), { recursive: true });
|
|
1419
|
+
writeFileSync3(out, html, "utf8");
|
|
1420
|
+
log.success(
|
|
1421
|
+
`Wrote self-contained report to ${pc7.bold(opts.html)} ${pc7.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
|
|
1422
|
+
);
|
|
1423
|
+
if (opts.open) openInBrowser(out);
|
|
1424
|
+
return 0;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1224
1427
|
// src/commands/skills.ts
|
|
1225
1428
|
import {
|
|
1226
1429
|
cpSync,
|
|
1227
|
-
existsSync as
|
|
1228
|
-
mkdirSync as
|
|
1229
|
-
readFileSync as
|
|
1230
|
-
readdirSync as
|
|
1430
|
+
existsSync as existsSync5,
|
|
1431
|
+
mkdirSync as mkdirSync3,
|
|
1432
|
+
readFileSync as readFileSync3,
|
|
1433
|
+
readdirSync as readdirSync3,
|
|
1231
1434
|
rmSync as rmSync2,
|
|
1232
|
-
statSync as
|
|
1233
|
-
writeFileSync as
|
|
1435
|
+
statSync as statSync3,
|
|
1436
|
+
writeFileSync as writeFileSync4
|
|
1234
1437
|
} from "fs";
|
|
1235
1438
|
import { homedir as homedir2 } from "os";
|
|
1236
|
-
import { join as
|
|
1237
|
-
import
|
|
1439
|
+
import { join as join8 } from "path";
|
|
1440
|
+
import pc8 from "picocolors";
|
|
1238
1441
|
var MANAGED = ["benchmark", "pr-review", "pr-resolve"];
|
|
1239
1442
|
function targetSkillsDir(scope) {
|
|
1240
|
-
return scope.global ?
|
|
1443
|
+
return scope.global ? join8(homedir2(), ".claude", "skills") : join8(scope.cwd, ".claude", "skills");
|
|
1241
1444
|
}
|
|
1242
1445
|
function sourceSkillsDir() {
|
|
1243
|
-
return
|
|
1446
|
+
return join8(findBundledAssetsDir(), "skills");
|
|
1244
1447
|
}
|
|
1245
1448
|
async function runSkillsInstall(opts) {
|
|
1246
1449
|
const assetsDir = findBundledAssetsDir();
|
|
1247
1450
|
const src = sourceSkillsDir();
|
|
1248
|
-
if (!
|
|
1451
|
+
if (!existsSync5(src)) {
|
|
1249
1452
|
log.error(`Bundled skills not found at ${src}. Reinstall edsger.`);
|
|
1250
1453
|
return 1;
|
|
1251
1454
|
}
|
|
1252
1455
|
const dest = targetSkillsDir(opts);
|
|
1253
|
-
|
|
1254
|
-
const available =
|
|
1456
|
+
mkdirSync3(dest, { recursive: true });
|
|
1457
|
+
const available = readdirSync3(src).filter((n) => existsSync5(join8(src, n, "SKILL.md")));
|
|
1255
1458
|
let installed = 0;
|
|
1256
1459
|
for (const name of available) {
|
|
1257
|
-
const outDir =
|
|
1258
|
-
if (
|
|
1460
|
+
const outDir = join8(dest, name);
|
|
1461
|
+
if (existsSync5(outDir) && !opts.force) {
|
|
1259
1462
|
log.warn(`skill "${name}" already exists \u2014 use --force to overwrite`);
|
|
1260
1463
|
continue;
|
|
1261
1464
|
}
|
|
1262
1465
|
rmSync2(outDir, { recursive: true, force: true });
|
|
1263
|
-
cpSync(
|
|
1264
|
-
const skillFile =
|
|
1265
|
-
const rewritten =
|
|
1266
|
-
|
|
1466
|
+
cpSync(join8(src, name), outDir, { recursive: true });
|
|
1467
|
+
const skillFile = join8(outDir, "SKILL.md");
|
|
1468
|
+
const rewritten = readFileSync3(skillFile, "utf8").split("${CLAUDE_PLUGIN_ROOT}").join(assetsDir);
|
|
1469
|
+
writeFileSync4(skillFile, rewritten, "utf8");
|
|
1267
1470
|
installed++;
|
|
1268
1471
|
log.success(`installed /${name}`);
|
|
1269
1472
|
}
|
|
@@ -1271,7 +1474,7 @@ async function runSkillsInstall(opts) {
|
|
|
1271
1474
|
log.info("Nothing installed.");
|
|
1272
1475
|
return 0;
|
|
1273
1476
|
}
|
|
1274
|
-
log.heading(`Installed ${installed} skill(s) into ${
|
|
1477
|
+
log.heading(`Installed ${installed} skill(s) into ${pc8.bold(dest)}`);
|
|
1275
1478
|
log.step(`scope: ${opts.global ? "user (all projects)" : "this project"}`);
|
|
1276
1479
|
log.step(`standards: ${assetsDir}`);
|
|
1277
1480
|
log.step("Open Claude Code and run /benchmark, /pr-review, or /pr-resolve.");
|
|
@@ -1281,8 +1484,8 @@ async function runSkillsUninstall(opts) {
|
|
|
1281
1484
|
const dest = targetSkillsDir(opts);
|
|
1282
1485
|
let removed = 0;
|
|
1283
1486
|
for (const name of MANAGED) {
|
|
1284
|
-
const dir =
|
|
1285
|
-
if (
|
|
1487
|
+
const dir = join8(dest, name);
|
|
1488
|
+
if (existsSync5(dir)) {
|
|
1286
1489
|
rmSync2(dir, { recursive: true, force: true });
|
|
1287
1490
|
removed++;
|
|
1288
1491
|
log.success(`removed /${name}`);
|
|
@@ -1298,20 +1501,20 @@ async function runSkillsList(opts) {
|
|
|
1298
1501
|
]) {
|
|
1299
1502
|
const dir = targetSkillsDir(scope);
|
|
1300
1503
|
const present = MANAGED.filter((n) => {
|
|
1301
|
-
const p =
|
|
1302
|
-
return
|
|
1504
|
+
const p = join8(dir, n, "SKILL.md");
|
|
1505
|
+
return existsSync5(p) && statSync3(p).isFile();
|
|
1303
1506
|
});
|
|
1304
1507
|
log.heading(`${label}: ${dir}`);
|
|
1305
|
-
if (present.length) for (const n of present) process.stderr.write(` ${
|
|
1508
|
+
if (present.length) for (const n of present) process.stderr.write(` ${pc8.green("\u2713")} /${n}
|
|
1306
1509
|
`);
|
|
1307
|
-
else process.stderr.write(` ${
|
|
1510
|
+
else process.stderr.write(` ${pc8.dim("(none installed)")}
|
|
1308
1511
|
`);
|
|
1309
1512
|
}
|
|
1310
1513
|
return 0;
|
|
1311
1514
|
}
|
|
1312
1515
|
|
|
1313
1516
|
// src/index.ts
|
|
1314
|
-
var pkg = JSON.parse(
|
|
1517
|
+
var pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
|
|
1315
1518
|
var VERSION = pkg.version;
|
|
1316
1519
|
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1317
1520
|
function toGlobal(raw) {
|
|
@@ -1349,10 +1552,38 @@ program.name("edsger").description(
|
|
|
1349
1552
|
"Agentic CLI to benchmark repositories and review/resolve pull requests\nagainst industrial-grade standards (from edsger-assets, fully overridable)."
|
|
1350
1553
|
).version(VERSION, "-V, --version");
|
|
1351
1554
|
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")
|
|
1555
|
+
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
1556
|
).action(async (opts) => {
|
|
1557
|
+
await guard(async () => {
|
|
1558
|
+
const g = toGlobal(opts);
|
|
1559
|
+
const code = await runBenchmark({ ...g, out: opts.out, noWrite: opts.write === false });
|
|
1560
|
+
if (code === 0 && opts.serve) {
|
|
1561
|
+
return runServe({ cwd: g.cwd, dir: opts.out, port: 4577, host: "127.0.0.1", open: true });
|
|
1562
|
+
}
|
|
1563
|
+
return code;
|
|
1564
|
+
});
|
|
1565
|
+
});
|
|
1566
|
+
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(
|
|
1567
|
+
async (opts) => {
|
|
1568
|
+
await guard(
|
|
1569
|
+
() => runServe({
|
|
1570
|
+
cwd: resolve2(opts.cwd ?? process.cwd()),
|
|
1571
|
+
dir: opts.dir,
|
|
1572
|
+
port: Number.parseInt(opts.port, 10) || 4577,
|
|
1573
|
+
host: opts.host,
|
|
1574
|
+
open: opts.open !== false
|
|
1575
|
+
})
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
);
|
|
1579
|
+
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
1580
|
await guard(
|
|
1355
|
-
() =>
|
|
1581
|
+
() => runReport({
|
|
1582
|
+
cwd: resolve2(opts.cwd ?? process.cwd()),
|
|
1583
|
+
dir: opts.dir,
|
|
1584
|
+
html: opts.html,
|
|
1585
|
+
open: Boolean(opts.open)
|
|
1586
|
+
})
|
|
1356
1587
|
);
|
|
1357
1588
|
});
|
|
1358
1589
|
addGlobalOptions(
|
|
@@ -1403,11 +1634,11 @@ program.addHelpText(
|
|
|
1403
1634
|
"after",
|
|
1404
1635
|
`
|
|
1405
1636
|
Examples:
|
|
1406
|
-
${
|
|
1407
|
-
${
|
|
1408
|
-
${
|
|
1409
|
-
${
|
|
1410
|
-
${
|
|
1637
|
+
${pc9.dim("$")} edsger benchmark
|
|
1638
|
+
${pc9.dim("$")} edsger benchmark -C ./my-repo --json
|
|
1639
|
+
${pc9.dim("$")} edsger pr-review 123 --post
|
|
1640
|
+
${pc9.dim("$")} edsger pr-resolve 123 --instruction "fix the failing test" --push
|
|
1641
|
+
${pc9.dim("$")} edsger skills install ${pc9.dim("# enable /benchmark etc. in Claude Code")}
|
|
1411
1642
|
|
|
1412
1643
|
Auth: the agent uses your Claude Code login (run \`claude\` once to sign in); run \`gh auth login\` for PR commands.
|
|
1413
1644
|
`
|