@doccov/cli 0.5.8 → 0.7.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/dist/cli.js +336 -845
- package/dist/config/index.d.ts +2 -20
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -168,8 +168,8 @@ ${formatIssues(issues)}`);
|
|
|
168
168
|
// src/config/index.ts
|
|
169
169
|
var defineConfig = (config) => config;
|
|
170
170
|
// src/cli.ts
|
|
171
|
-
import { readFileSync as
|
|
172
|
-
import * as
|
|
171
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
172
|
+
import * as path8 from "node:path";
|
|
173
173
|
import { fileURLToPath } from "node:url";
|
|
174
174
|
import { Command } from "commander";
|
|
175
175
|
|
|
@@ -181,11 +181,8 @@ import {
|
|
|
181
181
|
categorizeDrifts,
|
|
182
182
|
createSourceFile,
|
|
183
183
|
DocCov,
|
|
184
|
-
detectEntryPoint,
|
|
185
184
|
detectExampleAssertionFailures,
|
|
186
185
|
detectExampleRuntimeErrors,
|
|
187
|
-
detectMonorepo,
|
|
188
|
-
findPackageByName,
|
|
189
186
|
findJSDocLocation,
|
|
190
187
|
generateFixesForExport,
|
|
191
188
|
getDefaultConfig as getLintDefaultConfig,
|
|
@@ -195,6 +192,7 @@ import {
|
|
|
195
192
|
NodeFileSystem,
|
|
196
193
|
parseAssertions,
|
|
197
194
|
parseJSDocToPatch,
|
|
195
|
+
resolveTarget,
|
|
198
196
|
runExamplesWithPackage,
|
|
199
197
|
serializeJSDoc,
|
|
200
198
|
typecheckExamples
|
|
@@ -284,12 +282,6 @@ function collectDriftsFromExports(exports) {
|
|
|
284
282
|
}
|
|
285
283
|
return results;
|
|
286
284
|
}
|
|
287
|
-
function filterDriftsByType(drifts, onlyTypes) {
|
|
288
|
-
if (!onlyTypes)
|
|
289
|
-
return drifts;
|
|
290
|
-
const allowedTypes = new Set(onlyTypes.split(",").map((t) => t.trim()));
|
|
291
|
-
return drifts.filter((d) => allowedTypes.has(d.drift.type));
|
|
292
|
-
}
|
|
293
285
|
function groupByExport(drifts) {
|
|
294
286
|
const map = new Map;
|
|
295
287
|
for (const { export: exp, drift } of drifts) {
|
|
@@ -304,38 +296,20 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
304
296
|
...defaultDependencies,
|
|
305
297
|
...dependencies
|
|
306
298
|
};
|
|
307
|
-
program.command("check [entry]").description("Fail if documentation coverage falls below a threshold").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--min-coverage <percentage>", "Minimum docs coverage percentage (0-100)", (value) => Number(value)).option("--require-examples", "Require at least one @example for every export").option("--exec", "Execute @example blocks at runtime").option("--no-lint", "Skip lint checks").option("--no-typecheck", "Skip example type checking").option("--ignore-drift", "Do not fail on documentation drift").option("--skip-resolve", "Skip external type resolution from node_modules").option("--fix", "Auto-fix drift and lint issues").option("--write", "Alias for --fix").option("--
|
|
299
|
+
program.command("check [entry]").description("Fail if documentation coverage falls below a threshold").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--min-coverage <percentage>", "Minimum docs coverage percentage (0-100)", (value) => Number(value)).option("--require-examples", "Require at least one @example for every export").option("--exec", "Execute @example blocks at runtime").option("--no-lint", "Skip lint checks").option("--no-typecheck", "Skip example type checking").option("--ignore-drift", "Do not fail on documentation drift").option("--skip-resolve", "Skip external type resolution from node_modules").option("--fix", "Auto-fix drift and lint issues").option("--write", "Alias for --fix").option("--dry-run", "Preview fixes without writing (requires --fix)").action(async (entry, options) => {
|
|
308
300
|
try {
|
|
309
|
-
let targetDir = options.cwd;
|
|
310
|
-
let entryFile = entry;
|
|
311
301
|
const fileSystem = new NodeFileSystem(options.cwd);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
throw new Error(`Package "${options.package}" not found. Available: ${available}`);
|
|
321
|
-
}
|
|
322
|
-
targetDir = path2.join(options.cwd, pkg.path);
|
|
323
|
-
log(chalk.gray(`Found package at ${pkg.path}`));
|
|
302
|
+
const resolved = await resolveTarget(fileSystem, {
|
|
303
|
+
cwd: options.cwd,
|
|
304
|
+
package: options.package,
|
|
305
|
+
entry
|
|
306
|
+
});
|
|
307
|
+
const { targetDir, entryFile, packageInfo, entryPointInfo } = resolved;
|
|
308
|
+
if (packageInfo) {
|
|
309
|
+
log(chalk.gray(`Found package at ${packageInfo.path}`));
|
|
324
310
|
}
|
|
325
|
-
if (!
|
|
326
|
-
|
|
327
|
-
const detected = await detectEntryPoint(targetFs);
|
|
328
|
-
entryFile = path2.join(targetDir, detected.path);
|
|
329
|
-
log(chalk.gray(`Auto-detected entry point: ${detected.path} (from ${detected.source})`));
|
|
330
|
-
} else {
|
|
331
|
-
entryFile = path2.resolve(targetDir, entryFile);
|
|
332
|
-
if (fs.existsSync(entryFile) && fs.statSync(entryFile).isDirectory()) {
|
|
333
|
-
targetDir = entryFile;
|
|
334
|
-
const dirFs = new NodeFileSystem(entryFile);
|
|
335
|
-
const detected = await detectEntryPoint(dirFs);
|
|
336
|
-
entryFile = path2.join(entryFile, detected.path);
|
|
337
|
-
log(chalk.gray(`Auto-detected entry point: ${detected.path}`));
|
|
338
|
-
}
|
|
311
|
+
if (!entry) {
|
|
312
|
+
log(chalk.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
|
|
339
313
|
}
|
|
340
314
|
const minCoverage = clampCoverage(options.minCoverage ?? 80);
|
|
341
315
|
const resolveExternalTypes = !options.skipResolve;
|
|
@@ -401,7 +375,10 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
401
375
|
const allExamplesForTypecheck = [];
|
|
402
376
|
for (const exp of spec.exports ?? []) {
|
|
403
377
|
if (exp.examples && exp.examples.length > 0) {
|
|
404
|
-
allExamplesForTypecheck.push({
|
|
378
|
+
allExamplesForTypecheck.push({
|
|
379
|
+
exportName: exp.name,
|
|
380
|
+
examples: exp.examples
|
|
381
|
+
});
|
|
405
382
|
}
|
|
406
383
|
}
|
|
407
384
|
if (allExamplesForTypecheck.length > 0) {
|
|
@@ -534,11 +511,8 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
534
511
|
const fixedDriftKeys = new Set;
|
|
535
512
|
if (shouldFix && driftExports.length > 0) {
|
|
536
513
|
const allDrifts = collectDriftsFromExports(spec.exports ?? []);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
log(chalk.yellow("No matching drift issues for the specified types."));
|
|
540
|
-
} else if (filteredDrifts.length > 0) {
|
|
541
|
-
const { fixable, nonFixable } = categorizeDrifts(filteredDrifts.map((d) => d.drift));
|
|
514
|
+
if (allDrifts.length > 0) {
|
|
515
|
+
const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
|
|
542
516
|
if (fixable.length === 0) {
|
|
543
517
|
log(chalk.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
|
|
544
518
|
} else {
|
|
@@ -548,7 +522,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
548
522
|
log(chalk.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
|
|
549
523
|
}
|
|
550
524
|
log("");
|
|
551
|
-
const groupedDrifts = groupByExport(
|
|
525
|
+
const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
|
|
552
526
|
const edits = [];
|
|
553
527
|
const editsByFile = new Map;
|
|
554
528
|
for (const [exp, drifts] of groupedDrifts) {
|
|
@@ -889,7 +863,6 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
889
863
|
case "report":
|
|
890
864
|
log(generateHTMLReport(diff));
|
|
891
865
|
break;
|
|
892
|
-
case "text":
|
|
893
866
|
default:
|
|
894
867
|
printTextDiff(diff, log, error);
|
|
895
868
|
if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
|
|
@@ -1027,7 +1000,7 @@ function printAPIChanges(diff, log) {
|
|
|
1027
1000
|
}
|
|
1028
1001
|
const added = changes.filter((c) => c.changeType === "added");
|
|
1029
1002
|
if (added.length > 0) {
|
|
1030
|
-
const addedNames = added.map((a) => a.memberName
|
|
1003
|
+
const addedNames = added.map((a) => `${a.memberName}()`).join(", ");
|
|
1031
1004
|
log(chalk2.green(` + ${addedNames}`));
|
|
1032
1005
|
}
|
|
1033
1006
|
}
|
|
@@ -1361,59 +1334,207 @@ import * as fs3 from "node:fs";
|
|
|
1361
1334
|
import * as path4 from "node:path";
|
|
1362
1335
|
import {
|
|
1363
1336
|
DocCov as DocCov2,
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
findPackageByName as findPackageByName2,
|
|
1367
|
-
NodeFileSystem as NodeFileSystem2
|
|
1337
|
+
NodeFileSystem as NodeFileSystem2,
|
|
1338
|
+
resolveTarget as resolveTarget2
|
|
1368
1339
|
} from "@doccov/sdk";
|
|
1369
1340
|
import { normalize, validateSpec } from "@openpkg-ts/spec";
|
|
1370
1341
|
import chalk4 from "chalk";
|
|
1371
1342
|
|
|
1343
|
+
// src/reports/markdown.ts
|
|
1344
|
+
function bar(pct, width = 10) {
|
|
1345
|
+
const filled = Math.round(pct / 100 * width);
|
|
1346
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
1347
|
+
}
|
|
1348
|
+
function renderMarkdown(stats, options = {}) {
|
|
1349
|
+
const limit = options.limit ?? 20;
|
|
1350
|
+
const lines = [];
|
|
1351
|
+
lines.push(`# DocCov Report: ${stats.packageName}@${stats.version}`);
|
|
1352
|
+
lines.push("");
|
|
1353
|
+
lines.push(`**Coverage: ${stats.coverageScore}%** \`${bar(stats.coverageScore)}\``);
|
|
1354
|
+
lines.push("");
|
|
1355
|
+
lines.push("| Metric | Value |");
|
|
1356
|
+
lines.push("|--------|-------|");
|
|
1357
|
+
lines.push(`| Exports | ${stats.totalExports} |`);
|
|
1358
|
+
lines.push(`| Fully documented | ${stats.fullyDocumented} |`);
|
|
1359
|
+
lines.push(`| Partially documented | ${stats.partiallyDocumented} |`);
|
|
1360
|
+
lines.push(`| Undocumented | ${stats.undocumented} |`);
|
|
1361
|
+
lines.push(`| Drift issues | ${stats.driftCount} |`);
|
|
1362
|
+
lines.push("");
|
|
1363
|
+
lines.push("## Coverage by Signal");
|
|
1364
|
+
lines.push("");
|
|
1365
|
+
lines.push("| Signal | Coverage |");
|
|
1366
|
+
lines.push("|--------|----------|");
|
|
1367
|
+
for (const [sig, s] of Object.entries(stats.signalCoverage)) {
|
|
1368
|
+
lines.push(`| ${sig} | ${s.pct}% \`${bar(s.pct, 8)}\` |`);
|
|
1369
|
+
}
|
|
1370
|
+
if (stats.byKind.length > 0) {
|
|
1371
|
+
lines.push("");
|
|
1372
|
+
lines.push("## Coverage by Kind");
|
|
1373
|
+
lines.push("");
|
|
1374
|
+
lines.push("| Kind | Count | Avg Score |");
|
|
1375
|
+
lines.push("|------|-------|-----------|");
|
|
1376
|
+
for (const k of stats.byKind) {
|
|
1377
|
+
lines.push(`| ${k.kind} | ${k.count} | ${k.avgScore}% |`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
const lowExports = stats.exports.filter((e) => e.score < 100).slice(0, limit);
|
|
1381
|
+
if (lowExports.length > 0) {
|
|
1382
|
+
lines.push("");
|
|
1383
|
+
lines.push("## Lowest Coverage Exports");
|
|
1384
|
+
lines.push("");
|
|
1385
|
+
lines.push("| Export | Kind | Score | Missing |");
|
|
1386
|
+
lines.push("|--------|------|-------|---------|");
|
|
1387
|
+
for (const e of lowExports) {
|
|
1388
|
+
lines.push(`| \`${e.name}\` | ${e.kind} | ${e.score}% | ${e.missing.join(", ") || "-"} |`);
|
|
1389
|
+
}
|
|
1390
|
+
const totalLow = stats.exports.filter((e) => e.score < 100).length;
|
|
1391
|
+
if (totalLow > limit) {
|
|
1392
|
+
lines.push(`| ... | | | ${totalLow - limit} more |`);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (stats.driftIssues.length > 0) {
|
|
1396
|
+
lines.push("");
|
|
1397
|
+
lines.push("## Drift Issues");
|
|
1398
|
+
lines.push("");
|
|
1399
|
+
lines.push("| Export | Type | Issue |");
|
|
1400
|
+
lines.push("|--------|------|-------|");
|
|
1401
|
+
for (const d of stats.driftIssues.slice(0, limit)) {
|
|
1402
|
+
const hint = d.suggestion ? ` → ${d.suggestion}` : "";
|
|
1403
|
+
lines.push(`| \`${d.exportName}\` | ${d.type} | ${d.issue}${hint} |`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
lines.push("");
|
|
1407
|
+
lines.push("---");
|
|
1408
|
+
lines.push("*Generated by [DocCov](https://doccov.com)*");
|
|
1409
|
+
return lines.join(`
|
|
1410
|
+
`);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/reports/html.ts
|
|
1414
|
+
function escapeHtml(s) {
|
|
1415
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1416
|
+
}
|
|
1417
|
+
function renderHtml(stats, options = {}) {
|
|
1418
|
+
const md = renderMarkdown(stats, options);
|
|
1419
|
+
return `<!DOCTYPE html>
|
|
1420
|
+
<html lang="en">
|
|
1421
|
+
<head>
|
|
1422
|
+
<meta charset="UTF-8">
|
|
1423
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1424
|
+
<title>DocCov Report: ${escapeHtml(stats.packageName)}</title>
|
|
1425
|
+
<style>
|
|
1426
|
+
:root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; }
|
|
1427
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
|
|
1428
|
+
h1, h2 { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
|
|
1429
|
+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
1430
|
+
th, td { border: 1px solid var(--border); padding: 0.5rem 1rem; text-align: left; }
|
|
1431
|
+
th { background: #161b22; }
|
|
1432
|
+
code { background: #161b22; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
|
|
1433
|
+
a { color: var(--accent); }
|
|
1434
|
+
</style>
|
|
1435
|
+
</head>
|
|
1436
|
+
<body>
|
|
1437
|
+
<pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml(md)}</pre>
|
|
1438
|
+
</body>
|
|
1439
|
+
</html>`;
|
|
1440
|
+
}
|
|
1441
|
+
// src/reports/stats.ts
|
|
1442
|
+
function computeStats(spec) {
|
|
1443
|
+
const exports = spec.exports ?? [];
|
|
1444
|
+
const signals = {
|
|
1445
|
+
description: { covered: 0, total: 0 },
|
|
1446
|
+
params: { covered: 0, total: 0 },
|
|
1447
|
+
returns: { covered: 0, total: 0 },
|
|
1448
|
+
examples: { covered: 0, total: 0 }
|
|
1449
|
+
};
|
|
1450
|
+
const kindMap = new Map;
|
|
1451
|
+
const driftIssues = [];
|
|
1452
|
+
let fullyDocumented = 0;
|
|
1453
|
+
let partiallyDocumented = 0;
|
|
1454
|
+
let undocumented = 0;
|
|
1455
|
+
for (const exp of exports) {
|
|
1456
|
+
const score = exp.docs?.coverageScore ?? 0;
|
|
1457
|
+
const missing = exp.docs?.missing ?? [];
|
|
1458
|
+
for (const sig of ["description", "params", "returns", "examples"]) {
|
|
1459
|
+
signals[sig].total++;
|
|
1460
|
+
if (!missing.includes(sig))
|
|
1461
|
+
signals[sig].covered++;
|
|
1462
|
+
}
|
|
1463
|
+
const kindEntry = kindMap.get(exp.kind) ?? { count: 0, totalScore: 0 };
|
|
1464
|
+
kindEntry.count++;
|
|
1465
|
+
kindEntry.totalScore += score;
|
|
1466
|
+
kindMap.set(exp.kind, kindEntry);
|
|
1467
|
+
if (score === 100)
|
|
1468
|
+
fullyDocumented++;
|
|
1469
|
+
else if (score > 0)
|
|
1470
|
+
partiallyDocumented++;
|
|
1471
|
+
else
|
|
1472
|
+
undocumented++;
|
|
1473
|
+
for (const d of exp.docs?.drift ?? []) {
|
|
1474
|
+
driftIssues.push({
|
|
1475
|
+
exportName: exp.name,
|
|
1476
|
+
type: d.type,
|
|
1477
|
+
issue: d.issue,
|
|
1478
|
+
suggestion: d.suggestion
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
const signalCoverage = Object.fromEntries(Object.entries(signals).map(([k, v]) => [
|
|
1483
|
+
k,
|
|
1484
|
+
{ ...v, pct: v.total ? Math.round(v.covered / v.total * 100) : 0 }
|
|
1485
|
+
]));
|
|
1486
|
+
const byKind = Array.from(kindMap.entries()).map(([kind, { count, totalScore }]) => ({
|
|
1487
|
+
kind,
|
|
1488
|
+
count,
|
|
1489
|
+
avgScore: Math.round(totalScore / count)
|
|
1490
|
+
})).sort((a, b) => b.count - a.count);
|
|
1491
|
+
const sortedExports = exports.map((e) => ({
|
|
1492
|
+
name: e.name,
|
|
1493
|
+
kind: e.kind,
|
|
1494
|
+
score: e.docs?.coverageScore ?? 0,
|
|
1495
|
+
missing: e.docs?.missing ?? []
|
|
1496
|
+
})).sort((a, b) => a.score - b.score);
|
|
1497
|
+
return {
|
|
1498
|
+
packageName: spec.meta.name ?? "unknown",
|
|
1499
|
+
version: spec.meta.version ?? "0.0.0",
|
|
1500
|
+
coverageScore: spec.docs?.coverageScore ?? 0,
|
|
1501
|
+
totalExports: exports.length,
|
|
1502
|
+
fullyDocumented,
|
|
1503
|
+
partiallyDocumented,
|
|
1504
|
+
undocumented,
|
|
1505
|
+
driftCount: driftIssues.length,
|
|
1506
|
+
signalCoverage,
|
|
1507
|
+
byKind,
|
|
1508
|
+
exports: sortedExports,
|
|
1509
|
+
driftIssues
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1372
1512
|
// src/utils/filter-options.ts
|
|
1513
|
+
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|
|
1373
1514
|
import chalk3 from "chalk";
|
|
1374
|
-
var unique = (values) => Array.from(new Set(values));
|
|
1375
|
-
var parseListFlag = (value) => {
|
|
1376
|
-
if (!value) {
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
const rawItems = Array.isArray(value) ? value : [value];
|
|
1380
|
-
const normalized = rawItems.flatMap((item) => String(item).split(",")).map((item) => item.trim()).filter(Boolean);
|
|
1381
|
-
return normalized.length > 0 ? unique(normalized) : undefined;
|
|
1382
|
-
};
|
|
1383
1515
|
var formatList = (label, values) => `${label}: ${values.map((value) => chalk3.cyan(value)).join(", ")}`;
|
|
1384
1516
|
var mergeFilterOptions = (config, cliOptions) => {
|
|
1385
1517
|
const messages = [];
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
if (
|
|
1400
|
-
include = include ? include.filter((item) => cliInclude.includes(item)) : cliInclude;
|
|
1401
|
-
source = include ? "combined" : "cli";
|
|
1402
|
-
messages.push(formatList("apply include filters from CLI", cliInclude));
|
|
1403
|
-
}
|
|
1404
|
-
if (cliExclude) {
|
|
1405
|
-
exclude = exclude ? unique([...exclude, ...cliExclude]) : cliExclude;
|
|
1406
|
-
source = source ? "combined" : "cli";
|
|
1407
|
-
messages.push(formatList("apply exclude filters from CLI", cliExclude));
|
|
1408
|
-
}
|
|
1409
|
-
include = include ? unique(include) : undefined;
|
|
1410
|
-
exclude = exclude ? unique(exclude) : undefined;
|
|
1411
|
-
if (!include && !exclude) {
|
|
1518
|
+
if (config?.include) {
|
|
1519
|
+
messages.push(formatList("include filters from config", config.include));
|
|
1520
|
+
}
|
|
1521
|
+
if (config?.exclude) {
|
|
1522
|
+
messages.push(formatList("exclude filters from config", config.exclude));
|
|
1523
|
+
}
|
|
1524
|
+
if (cliOptions.include) {
|
|
1525
|
+
messages.push(formatList("apply include filters from CLI", cliOptions.include));
|
|
1526
|
+
}
|
|
1527
|
+
if (cliOptions.exclude) {
|
|
1528
|
+
messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
|
|
1529
|
+
}
|
|
1530
|
+
const resolved = mergeFilters(config, cliOptions);
|
|
1531
|
+
if (!resolved.include && !resolved.exclude) {
|
|
1412
1532
|
return { messages };
|
|
1413
1533
|
}
|
|
1534
|
+
const source = resolved.source === "override" ? "cli" : resolved.source;
|
|
1414
1535
|
return {
|
|
1415
|
-
include,
|
|
1416
|
-
exclude,
|
|
1536
|
+
include: resolved.include,
|
|
1537
|
+
exclude: resolved.exclude,
|
|
1417
1538
|
source,
|
|
1418
1539
|
messages
|
|
1419
1540
|
};
|
|
@@ -1451,37 +1572,20 @@ function registerGenerateCommand(program, dependencies = {}) {
|
|
|
1451
1572
|
...defaultDependencies3,
|
|
1452
1573
|
...dependencies
|
|
1453
1574
|
};
|
|
1454
|
-
program.command("generate [entry]").description("Generate OpenPkg specification for documentation coverage analysis").option("-o, --output <file>", "Output file", "openpkg.json").option("-p, --package <name>", "Target package name (for monorepos)").option("--cwd <dir>", "Working directory", process.cwd()).option("--skip-resolve", "Skip external type resolution from node_modules").option("--include <ids>", "Filter exports by identifier (comma-separated or repeated)").option("--exclude <ids>", "Exclude exports by identifier (comma-separated or repeated)").option("--show-diagnostics", "Print TypeScript diagnostics from analysis").option("--no-docs", "Omit docs coverage fields from output (pure structural spec)").option("-y, --yes", "Skip all prompts and use defaults").action(async (entry, options) => {
|
|
1575
|
+
program.command("generate [entry]").description("Generate OpenPkg specification for documentation coverage analysis").option("-o, --output <file>", "Output file", "openpkg.json").option("--format <format>", "Output format: json, markdown, html", "json").option("-p, --package <name>", "Target package name (for monorepos)").option("--cwd <dir>", "Working directory", process.cwd()).option("--skip-resolve", "Skip external type resolution from node_modules").option("--include <ids>", "Filter exports by identifier (comma-separated or repeated)").option("--exclude <ids>", "Exclude exports by identifier (comma-separated or repeated)").option("--show-diagnostics", "Print TypeScript diagnostics from analysis").option("--no-docs", "Omit docs coverage fields from output (pure structural spec)").option("--limit <n>", "Max exports to show in report tables (for markdown/html)", "20").option("-y, --yes", "Skip all prompts and use defaults").action(async (entry, options) => {
|
|
1455
1576
|
try {
|
|
1456
|
-
let targetDir = options.cwd;
|
|
1457
|
-
let entryFile = entry;
|
|
1458
1577
|
const fileSystem = new NodeFileSystem2(options.cwd);
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
throw new Error(`Package "${options.package}" not found. Available: ${available}`);
|
|
1468
|
-
}
|
|
1469
|
-
targetDir = path4.join(options.cwd, pkg.path);
|
|
1470
|
-
log(chalk4.gray(`Found package at ${pkg.path}`));
|
|
1578
|
+
const resolved = await resolveTarget2(fileSystem, {
|
|
1579
|
+
cwd: options.cwd,
|
|
1580
|
+
package: options.package,
|
|
1581
|
+
entry
|
|
1582
|
+
});
|
|
1583
|
+
const { targetDir, entryFile, packageInfo, entryPointInfo } = resolved;
|
|
1584
|
+
if (packageInfo) {
|
|
1585
|
+
log(chalk4.gray(`Found package at ${packageInfo.path}`));
|
|
1471
1586
|
}
|
|
1472
|
-
if (!
|
|
1473
|
-
|
|
1474
|
-
const detected = await detectEntryPoint2(targetFs);
|
|
1475
|
-
entryFile = path4.join(targetDir, detected.path);
|
|
1476
|
-
log(chalk4.gray(`Auto-detected entry point: ${detected.path} (from ${detected.source})`));
|
|
1477
|
-
} else {
|
|
1478
|
-
entryFile = path4.resolve(targetDir, entryFile);
|
|
1479
|
-
if (fs3.existsSync(entryFile) && fs3.statSync(entryFile).isDirectory()) {
|
|
1480
|
-
const dirFs = new NodeFileSystem2(entryFile);
|
|
1481
|
-
const detected = await detectEntryPoint2(dirFs);
|
|
1482
|
-
entryFile = path4.join(entryFile, detected.path);
|
|
1483
|
-
log(chalk4.gray(`Auto-detected entry point: ${detected.path} (from ${detected.source})`));
|
|
1484
|
-
}
|
|
1587
|
+
if (!entry) {
|
|
1588
|
+
log(chalk4.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
|
|
1485
1589
|
}
|
|
1486
1590
|
const resolveExternalTypes = !options.skipResolve;
|
|
1487
1591
|
const cliFilters = {
|
|
@@ -1526,23 +1630,34 @@ function registerGenerateCommand(program, dependencies = {}) {
|
|
|
1526
1630
|
if (!result) {
|
|
1527
1631
|
throw new Error("Failed to produce an OpenPkg spec.");
|
|
1528
1632
|
}
|
|
1529
|
-
const outputPath = path4.resolve(process.cwd(), options.output);
|
|
1530
1633
|
let normalized = normalize(result.spec);
|
|
1531
1634
|
if (options.docs === false) {
|
|
1532
1635
|
normalized = stripDocsFields(normalized);
|
|
1533
1636
|
}
|
|
1534
1637
|
const validation = validateSpec(normalized);
|
|
1535
1638
|
if (!validation.ok) {
|
|
1536
|
-
|
|
1639
|
+
error(chalk4.red("Spec failed schema validation"));
|
|
1537
1640
|
for (const err of validation.errors) {
|
|
1538
1641
|
error(chalk4.red(`schema: ${err.instancePath || "/"} ${err.message}`));
|
|
1539
1642
|
}
|
|
1540
1643
|
process.exit(1);
|
|
1541
1644
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1645
|
+
const format = options.format ?? "json";
|
|
1646
|
+
const outputPath = path4.resolve(process.cwd(), options.output);
|
|
1647
|
+
if (format === "markdown" || format === "html") {
|
|
1648
|
+
const stats = computeStats(normalized);
|
|
1649
|
+
const limit = parseInt(options.limit, 10) || 20;
|
|
1650
|
+
const reportOutput = format === "html" ? renderHtml(stats, { limit }) : renderMarkdown(stats, { limit });
|
|
1651
|
+
writeFileSync2(outputPath, reportOutput);
|
|
1652
|
+
log(chalk4.green(`✓ Generated ${format} report: ${options.output}`));
|
|
1653
|
+
log(chalk4.gray(` Coverage: ${stats.coverageScore}%`));
|
|
1654
|
+
log(chalk4.gray(` ${stats.totalExports} exports, ${stats.driftCount} drift issues`));
|
|
1655
|
+
} else {
|
|
1656
|
+
writeFileSync2(outputPath, JSON.stringify(normalized, null, 2));
|
|
1657
|
+
log(chalk4.green(`✓ Generated ${options.output}`));
|
|
1658
|
+
log(chalk4.gray(` ${getArrayLength(normalized.exports)} exports`));
|
|
1659
|
+
log(chalk4.gray(` ${getArrayLength(normalized.types)} types`));
|
|
1660
|
+
}
|
|
1546
1661
|
if (options.showDiagnostics && result.diagnostics.length > 0) {
|
|
1547
1662
|
log("");
|
|
1548
1663
|
log(chalk4.bold("Diagnostics"));
|
|
@@ -1688,517 +1803,32 @@ var buildTemplate = (format) => {
|
|
|
1688
1803
|
`);
|
|
1689
1804
|
};
|
|
1690
1805
|
|
|
1691
|
-
// src/commands/
|
|
1692
|
-
import * as
|
|
1693
|
-
import * as
|
|
1806
|
+
// src/commands/scan.ts
|
|
1807
|
+
import * as fs6 from "node:fs";
|
|
1808
|
+
import * as fsPromises from "node:fs/promises";
|
|
1809
|
+
import * as os from "node:os";
|
|
1810
|
+
import * as path7 from "node:path";
|
|
1694
1811
|
import {
|
|
1695
|
-
applyEdits as applyEdits2,
|
|
1696
|
-
createSourceFile as createSourceFile2,
|
|
1697
|
-
detectEntryPoint as detectEntryPoint3,
|
|
1698
|
-
detectMonorepo as detectMonorepo3,
|
|
1699
1812
|
DocCov as DocCov3,
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1813
|
+
buildCloneUrl,
|
|
1814
|
+
buildDisplayUrl,
|
|
1815
|
+
detectBuildInfo,
|
|
1816
|
+
detectEntryPoint,
|
|
1817
|
+
detectMonorepo,
|
|
1818
|
+
detectPackageManager,
|
|
1819
|
+
extractSpecSummary,
|
|
1820
|
+
findPackageByName,
|
|
1821
|
+
formatPackageList,
|
|
1822
|
+
getInstallCommand,
|
|
1705
1823
|
NodeFileSystem as NodeFileSystem3,
|
|
1706
|
-
|
|
1824
|
+
parseGitHubUrl
|
|
1707
1825
|
} from "@doccov/sdk";
|
|
1708
1826
|
import chalk6 from "chalk";
|
|
1709
|
-
var defaultDependencies5 = {
|
|
1710
|
-
createDocCov: (options) => new DocCov3(options),
|
|
1711
|
-
log: console.log,
|
|
1712
|
-
error: console.error
|
|
1713
|
-
};
|
|
1714
|
-
function getRawJSDoc(exp, targetDir) {
|
|
1715
|
-
if (!exp.source?.file)
|
|
1716
|
-
return;
|
|
1717
|
-
const filePath = path6.resolve(targetDir, exp.source.file);
|
|
1718
|
-
if (!fs5.existsSync(filePath))
|
|
1719
|
-
return;
|
|
1720
|
-
try {
|
|
1721
|
-
const sourceFile = createSourceFile2(filePath);
|
|
1722
|
-
const location = findJSDocLocation2(sourceFile, exp.name, exp.source.line);
|
|
1723
|
-
return location?.existingJSDoc;
|
|
1724
|
-
} catch {
|
|
1725
|
-
return;
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
function registerLintCommand(program, dependencies = {}) {
|
|
1729
|
-
const { createDocCov, log, error } = {
|
|
1730
|
-
...defaultDependencies5,
|
|
1731
|
-
...dependencies
|
|
1732
|
-
};
|
|
1733
|
-
program.command("lint [entry]").description("Lint documentation for style and quality issues").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--fix", "Auto-fix fixable issues").option("--write", "Alias for --fix").option("--rule <name>", "Run only a specific rule").option("--skip-resolve", "Skip external type resolution").action(async (entry, options) => {
|
|
1734
|
-
try {
|
|
1735
|
-
let targetDir = options.cwd;
|
|
1736
|
-
let entryFile = entry;
|
|
1737
|
-
const fileSystem = new NodeFileSystem3(options.cwd);
|
|
1738
|
-
if (options.package) {
|
|
1739
|
-
const mono = await detectMonorepo3(fileSystem);
|
|
1740
|
-
if (!mono.isMonorepo) {
|
|
1741
|
-
throw new Error("Not a monorepo. Remove --package flag.");
|
|
1742
|
-
}
|
|
1743
|
-
const pkg = findPackageByName3(mono.packages, options.package);
|
|
1744
|
-
if (!pkg) {
|
|
1745
|
-
const available = mono.packages.map((p) => p.name).join(", ");
|
|
1746
|
-
throw new Error(`Package "${options.package}" not found. Available: ${available}`);
|
|
1747
|
-
}
|
|
1748
|
-
targetDir = path6.join(options.cwd, pkg.path);
|
|
1749
|
-
log(chalk6.gray(`Found package at ${pkg.path}`));
|
|
1750
|
-
}
|
|
1751
|
-
if (!entryFile) {
|
|
1752
|
-
const targetFs = new NodeFileSystem3(targetDir);
|
|
1753
|
-
const detected = await detectEntryPoint3(targetFs);
|
|
1754
|
-
entryFile = path6.join(targetDir, detected.path);
|
|
1755
|
-
log(chalk6.gray(`Auto-detected entry point: ${detected.path}`));
|
|
1756
|
-
} else {
|
|
1757
|
-
entryFile = path6.resolve(targetDir, entryFile);
|
|
1758
|
-
if (fs5.existsSync(entryFile) && fs5.statSync(entryFile).isDirectory()) {
|
|
1759
|
-
targetDir = entryFile;
|
|
1760
|
-
const dirFs = new NodeFileSystem3(entryFile);
|
|
1761
|
-
const detected = await detectEntryPoint3(dirFs);
|
|
1762
|
-
entryFile = path6.join(entryFile, detected.path);
|
|
1763
|
-
log(chalk6.gray(`Auto-detected entry point: ${detected.path}`));
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
const resolveExternalTypes = !options.skipResolve;
|
|
1767
|
-
process.stdout.write(chalk6.cyan(`> Analyzing documentation...
|
|
1768
|
-
`));
|
|
1769
|
-
const doccov = createDocCov({ resolveExternalTypes });
|
|
1770
|
-
const specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
|
|
1771
|
-
if (!specResult) {
|
|
1772
|
-
throw new Error("Failed to analyze documentation.");
|
|
1773
|
-
}
|
|
1774
|
-
process.stdout.write(chalk6.cyan(`> Running lint rules...
|
|
1775
|
-
`));
|
|
1776
|
-
let config = getDefaultConfig();
|
|
1777
|
-
if (options.rule) {
|
|
1778
|
-
const rule = getRule(options.rule);
|
|
1779
|
-
if (!rule) {
|
|
1780
|
-
throw new Error(`Unknown rule: ${options.rule}`);
|
|
1781
|
-
}
|
|
1782
|
-
const rules = {};
|
|
1783
|
-
for (const key of Object.keys(config.rules)) {
|
|
1784
|
-
rules[key] = "off";
|
|
1785
|
-
}
|
|
1786
|
-
rules[options.rule] = rule.defaultSeverity === "off" ? "warn" : rule.defaultSeverity;
|
|
1787
|
-
config = { rules };
|
|
1788
|
-
}
|
|
1789
|
-
const exportsWithJSDoc = [];
|
|
1790
|
-
for (const exp of specResult.spec.exports ?? []) {
|
|
1791
|
-
const rawJSDoc = getRawJSDoc(exp, targetDir);
|
|
1792
|
-
exportsWithJSDoc.push({
|
|
1793
|
-
export: exp,
|
|
1794
|
-
rawJSDoc,
|
|
1795
|
-
filePath: exp.source?.file ? path6.resolve(targetDir, exp.source.file) : undefined
|
|
1796
|
-
});
|
|
1797
|
-
}
|
|
1798
|
-
const allViolations = [];
|
|
1799
|
-
for (const { export: exp, rawJSDoc, filePath } of exportsWithJSDoc) {
|
|
1800
|
-
const violations = lintExport2(exp, rawJSDoc, config);
|
|
1801
|
-
for (const violation of violations) {
|
|
1802
|
-
allViolations.push({ export: exp, violation, filePath, rawJSDoc });
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
const shouldFix = options.fix || options.write;
|
|
1806
|
-
const fixableViolations = allViolations.filter((v) => v.violation.fixable);
|
|
1807
|
-
if (shouldFix && fixableViolations.length > 0) {
|
|
1808
|
-
process.stdout.write(chalk6.cyan(`> Applying fixes...
|
|
1809
|
-
`));
|
|
1810
|
-
const edits = [];
|
|
1811
|
-
for (const { export: exp, rawJSDoc, filePath } of fixableViolations) {
|
|
1812
|
-
if (!filePath || !rawJSDoc)
|
|
1813
|
-
continue;
|
|
1814
|
-
if (filePath.endsWith(".d.ts"))
|
|
1815
|
-
continue;
|
|
1816
|
-
const sourceFile = createSourceFile2(filePath);
|
|
1817
|
-
const location = findJSDocLocation2(sourceFile, exp.name, exp.source?.line);
|
|
1818
|
-
if (!location)
|
|
1819
|
-
continue;
|
|
1820
|
-
const rule = getRule("consistent-param-style");
|
|
1821
|
-
if (!rule?.fix)
|
|
1822
|
-
continue;
|
|
1823
|
-
const patch = rule.fix(exp, rawJSDoc);
|
|
1824
|
-
if (!patch)
|
|
1825
|
-
continue;
|
|
1826
|
-
const newJSDoc = serializeJSDoc2(patch, location.indent);
|
|
1827
|
-
edits.push({
|
|
1828
|
-
filePath,
|
|
1829
|
-
symbolName: exp.name,
|
|
1830
|
-
startLine: location.startLine,
|
|
1831
|
-
endLine: location.endLine,
|
|
1832
|
-
hasExisting: location.hasExisting,
|
|
1833
|
-
existingJSDoc: location.existingJSDoc,
|
|
1834
|
-
newJSDoc,
|
|
1835
|
-
indent: location.indent
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
if (edits.length > 0) {
|
|
1839
|
-
const result = await applyEdits2(edits);
|
|
1840
|
-
if (result.errors.length > 0) {
|
|
1841
|
-
for (const err of result.errors) {
|
|
1842
|
-
error(chalk6.red(` ${err.file}: ${err.error}`));
|
|
1843
|
-
}
|
|
1844
|
-
} else {
|
|
1845
|
-
process.stdout.write(chalk6.green(`✓ Fixed ${result.editsApplied} issue(s) in ${result.filesModified} file(s)
|
|
1846
|
-
`));
|
|
1847
|
-
}
|
|
1848
|
-
const fixedExports = new Set(edits.map((e) => e.symbolName));
|
|
1849
|
-
const remaining = allViolations.filter((v) => !v.violation.fixable || !fixedExports.has(v.export.name));
|
|
1850
|
-
allViolations.length = 0;
|
|
1851
|
-
allViolations.push(...remaining);
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
if (allViolations.length === 0) {
|
|
1855
|
-
log(chalk6.green("✓ No lint issues found"));
|
|
1856
|
-
return;
|
|
1857
|
-
}
|
|
1858
|
-
const byFile = new Map;
|
|
1859
|
-
for (const v of allViolations) {
|
|
1860
|
-
const file = v.filePath ?? "unknown";
|
|
1861
|
-
const existing = byFile.get(file) ?? [];
|
|
1862
|
-
existing.push(v);
|
|
1863
|
-
byFile.set(file, existing);
|
|
1864
|
-
}
|
|
1865
|
-
log("");
|
|
1866
|
-
for (const [filePath, violations] of byFile) {
|
|
1867
|
-
const relativePath = path6.relative(targetDir, filePath);
|
|
1868
|
-
log(chalk6.underline(relativePath));
|
|
1869
|
-
for (const { export: exp, violation } of violations) {
|
|
1870
|
-
const line = exp.source?.line ?? 0;
|
|
1871
|
-
const severity = violation.severity === "error" ? chalk6.red("error") : chalk6.yellow("warning");
|
|
1872
|
-
const fixable = violation.fixable ? chalk6.gray(" (fixable)") : "";
|
|
1873
|
-
log(` ${line}:1 ${severity} ${violation.message} ${chalk6.gray(violation.rule)}${fixable}`);
|
|
1874
|
-
}
|
|
1875
|
-
log("");
|
|
1876
|
-
}
|
|
1877
|
-
const errorCount = allViolations.filter((v) => v.violation.severity === "error").length;
|
|
1878
|
-
const warnCount = allViolations.filter((v) => v.violation.severity === "warn").length;
|
|
1879
|
-
const fixableCount = allViolations.filter((v) => v.violation.fixable).length;
|
|
1880
|
-
const summary = [];
|
|
1881
|
-
if (errorCount > 0)
|
|
1882
|
-
summary.push(chalk6.red(`${errorCount} error(s)`));
|
|
1883
|
-
if (warnCount > 0)
|
|
1884
|
-
summary.push(chalk6.yellow(`${warnCount} warning(s)`));
|
|
1885
|
-
if (fixableCount > 0 && !shouldFix) {
|
|
1886
|
-
summary.push(chalk6.gray(`${fixableCount} fixable with --fix`));
|
|
1887
|
-
}
|
|
1888
|
-
log(summary.join(", "));
|
|
1889
|
-
if (errorCount > 0) {
|
|
1890
|
-
process.exit(1);
|
|
1891
|
-
}
|
|
1892
|
-
} catch (commandError) {
|
|
1893
|
-
error(chalk6.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
1894
|
-
process.exit(1);
|
|
1895
|
-
}
|
|
1896
|
-
});
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
// src/commands/report.ts
|
|
1900
|
-
import * as fs6 from "node:fs";
|
|
1901
|
-
import * as path7 from "node:path";
|
|
1902
|
-
import {
|
|
1903
|
-
DocCov as DocCov4,
|
|
1904
|
-
detectEntryPoint as detectEntryPoint4,
|
|
1905
|
-
detectMonorepo as detectMonorepo4,
|
|
1906
|
-
findPackageByName as findPackageByName4,
|
|
1907
|
-
NodeFileSystem as NodeFileSystem4
|
|
1908
|
-
} from "@doccov/sdk";
|
|
1909
|
-
import chalk7 from "chalk";
|
|
1910
|
-
|
|
1911
|
-
// src/reports/markdown.ts
|
|
1912
|
-
function bar(pct, width = 10) {
|
|
1913
|
-
const filled = Math.round(pct / 100 * width);
|
|
1914
|
-
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
1915
|
-
}
|
|
1916
|
-
function renderMarkdown(stats, options = {}) {
|
|
1917
|
-
const limit = options.limit ?? 20;
|
|
1918
|
-
const lines = [];
|
|
1919
|
-
lines.push(`# DocCov Report: ${stats.packageName}@${stats.version}`);
|
|
1920
|
-
lines.push("");
|
|
1921
|
-
lines.push(`**Coverage: ${stats.coverageScore}%** \`${bar(stats.coverageScore)}\``);
|
|
1922
|
-
lines.push("");
|
|
1923
|
-
lines.push("| Metric | Value |");
|
|
1924
|
-
lines.push("|--------|-------|");
|
|
1925
|
-
lines.push(`| Exports | ${stats.totalExports} |`);
|
|
1926
|
-
lines.push(`| Fully documented | ${stats.fullyDocumented} |`);
|
|
1927
|
-
lines.push(`| Partially documented | ${stats.partiallyDocumented} |`);
|
|
1928
|
-
lines.push(`| Undocumented | ${stats.undocumented} |`);
|
|
1929
|
-
lines.push(`| Drift issues | ${stats.driftCount} |`);
|
|
1930
|
-
lines.push("");
|
|
1931
|
-
lines.push("## Coverage by Signal");
|
|
1932
|
-
lines.push("");
|
|
1933
|
-
lines.push("| Signal | Coverage |");
|
|
1934
|
-
lines.push("|--------|----------|");
|
|
1935
|
-
for (const [sig, s] of Object.entries(stats.signalCoverage)) {
|
|
1936
|
-
lines.push(`| ${sig} | ${s.pct}% \`${bar(s.pct, 8)}\` |`);
|
|
1937
|
-
}
|
|
1938
|
-
if (stats.byKind.length > 0) {
|
|
1939
|
-
lines.push("");
|
|
1940
|
-
lines.push("## Coverage by Kind");
|
|
1941
|
-
lines.push("");
|
|
1942
|
-
lines.push("| Kind | Count | Avg Score |");
|
|
1943
|
-
lines.push("|------|-------|-----------|");
|
|
1944
|
-
for (const k of stats.byKind) {
|
|
1945
|
-
lines.push(`| ${k.kind} | ${k.count} | ${k.avgScore}% |`);
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
const lowExports = stats.exports.filter((e) => e.score < 100).slice(0, limit);
|
|
1949
|
-
if (lowExports.length > 0) {
|
|
1950
|
-
lines.push("");
|
|
1951
|
-
lines.push("## Lowest Coverage Exports");
|
|
1952
|
-
lines.push("");
|
|
1953
|
-
lines.push("| Export | Kind | Score | Missing |");
|
|
1954
|
-
lines.push("|--------|------|-------|---------|");
|
|
1955
|
-
for (const e of lowExports) {
|
|
1956
|
-
lines.push(`| \`${e.name}\` | ${e.kind} | ${e.score}% | ${e.missing.join(", ") || "-"} |`);
|
|
1957
|
-
}
|
|
1958
|
-
const totalLow = stats.exports.filter((e) => e.score < 100).length;
|
|
1959
|
-
if (totalLow > limit) {
|
|
1960
|
-
lines.push(`| ... | | | ${totalLow - limit} more |`);
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
if (stats.driftIssues.length > 0) {
|
|
1964
|
-
lines.push("");
|
|
1965
|
-
lines.push("## Drift Issues");
|
|
1966
|
-
lines.push("");
|
|
1967
|
-
lines.push("| Export | Type | Issue |");
|
|
1968
|
-
lines.push("|--------|------|-------|");
|
|
1969
|
-
for (const d of stats.driftIssues.slice(0, limit)) {
|
|
1970
|
-
const hint = d.suggestion ? ` → ${d.suggestion}` : "";
|
|
1971
|
-
lines.push(`| \`${d.exportName}\` | ${d.type} | ${d.issue}${hint} |`);
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
lines.push("");
|
|
1975
|
-
lines.push("---");
|
|
1976
|
-
lines.push("*Generated by [DocCov](https://doccov.com)*");
|
|
1977
|
-
return lines.join(`
|
|
1978
|
-
`);
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
// src/reports/html.ts
|
|
1982
|
-
function escapeHtml(s) {
|
|
1983
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1984
|
-
}
|
|
1985
|
-
function renderHtml(stats, options = {}) {
|
|
1986
|
-
const md = renderMarkdown(stats, options);
|
|
1987
|
-
return `<!DOCTYPE html>
|
|
1988
|
-
<html lang="en">
|
|
1989
|
-
<head>
|
|
1990
|
-
<meta charset="UTF-8">
|
|
1991
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1992
|
-
<title>DocCov Report: ${escapeHtml(stats.packageName)}</title>
|
|
1993
|
-
<style>
|
|
1994
|
-
:root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; }
|
|
1995
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
|
|
1996
|
-
h1, h2 { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
|
|
1997
|
-
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
1998
|
-
th, td { border: 1px solid var(--border); padding: 0.5rem 1rem; text-align: left; }
|
|
1999
|
-
th { background: #161b22; }
|
|
2000
|
-
code { background: #161b22; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
|
|
2001
|
-
a { color: var(--accent); }
|
|
2002
|
-
</style>
|
|
2003
|
-
</head>
|
|
2004
|
-
<body>
|
|
2005
|
-
<pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml(md)}</pre>
|
|
2006
|
-
</body>
|
|
2007
|
-
</html>`;
|
|
2008
|
-
}
|
|
2009
|
-
// src/reports/stats.ts
|
|
2010
|
-
function computeStats(spec) {
|
|
2011
|
-
const exports = spec.exports ?? [];
|
|
2012
|
-
const signals = {
|
|
2013
|
-
description: { covered: 0, total: 0 },
|
|
2014
|
-
params: { covered: 0, total: 0 },
|
|
2015
|
-
returns: { covered: 0, total: 0 },
|
|
2016
|
-
examples: { covered: 0, total: 0 }
|
|
2017
|
-
};
|
|
2018
|
-
const kindMap = new Map;
|
|
2019
|
-
const driftIssues = [];
|
|
2020
|
-
let fullyDocumented = 0;
|
|
2021
|
-
let partiallyDocumented = 0;
|
|
2022
|
-
let undocumented = 0;
|
|
2023
|
-
for (const exp of exports) {
|
|
2024
|
-
const score = exp.docs?.coverageScore ?? 0;
|
|
2025
|
-
const missing = exp.docs?.missing ?? [];
|
|
2026
|
-
for (const sig of ["description", "params", "returns", "examples"]) {
|
|
2027
|
-
signals[sig].total++;
|
|
2028
|
-
if (!missing.includes(sig))
|
|
2029
|
-
signals[sig].covered++;
|
|
2030
|
-
}
|
|
2031
|
-
const kindEntry = kindMap.get(exp.kind) ?? { count: 0, totalScore: 0 };
|
|
2032
|
-
kindEntry.count++;
|
|
2033
|
-
kindEntry.totalScore += score;
|
|
2034
|
-
kindMap.set(exp.kind, kindEntry);
|
|
2035
|
-
if (score === 100)
|
|
2036
|
-
fullyDocumented++;
|
|
2037
|
-
else if (score > 0)
|
|
2038
|
-
partiallyDocumented++;
|
|
2039
|
-
else
|
|
2040
|
-
undocumented++;
|
|
2041
|
-
for (const d of exp.docs?.drift ?? []) {
|
|
2042
|
-
driftIssues.push({
|
|
2043
|
-
exportName: exp.name,
|
|
2044
|
-
type: d.type,
|
|
2045
|
-
issue: d.issue,
|
|
2046
|
-
suggestion: d.suggestion
|
|
2047
|
-
});
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
const signalCoverage = Object.fromEntries(Object.entries(signals).map(([k, v]) => [
|
|
2051
|
-
k,
|
|
2052
|
-
{ ...v, pct: v.total ? Math.round(v.covered / v.total * 100) : 0 }
|
|
2053
|
-
]));
|
|
2054
|
-
const byKind = Array.from(kindMap.entries()).map(([kind, { count, totalScore }]) => ({
|
|
2055
|
-
kind,
|
|
2056
|
-
count,
|
|
2057
|
-
avgScore: Math.round(totalScore / count)
|
|
2058
|
-
})).sort((a, b) => b.count - a.count);
|
|
2059
|
-
const sortedExports = exports.map((e) => ({
|
|
2060
|
-
name: e.name,
|
|
2061
|
-
kind: e.kind,
|
|
2062
|
-
score: e.docs?.coverageScore ?? 0,
|
|
2063
|
-
missing: e.docs?.missing ?? []
|
|
2064
|
-
})).sort((a, b) => a.score - b.score);
|
|
2065
|
-
return {
|
|
2066
|
-
packageName: spec.meta.name ?? "unknown",
|
|
2067
|
-
version: spec.meta.version ?? "0.0.0",
|
|
2068
|
-
coverageScore: spec.docs?.coverageScore ?? 0,
|
|
2069
|
-
totalExports: exports.length,
|
|
2070
|
-
fullyDocumented,
|
|
2071
|
-
partiallyDocumented,
|
|
2072
|
-
undocumented,
|
|
2073
|
-
driftCount: driftIssues.length,
|
|
2074
|
-
signalCoverage,
|
|
2075
|
-
byKind,
|
|
2076
|
-
exports: sortedExports,
|
|
2077
|
-
driftIssues
|
|
2078
|
-
};
|
|
2079
|
-
}
|
|
2080
|
-
// src/commands/report.ts
|
|
2081
|
-
function registerReportCommand(program) {
|
|
2082
|
-
program.command("report [entry]").description("Generate a documentation coverage report").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--spec <file>", "Use existing openpkg.json instead of analyzing").option("--output <format>", "Output format: markdown, html, json", "markdown").option("--out <file>", "Write to file instead of stdout").option("--limit <n>", "Max exports to show in tables", "20").option("--skip-resolve", "Skip external type resolution from node_modules").action(async (entry, options) => {
|
|
2083
|
-
try {
|
|
2084
|
-
let spec;
|
|
2085
|
-
if (options.spec) {
|
|
2086
|
-
const specPath = path7.resolve(options.cwd, options.spec);
|
|
2087
|
-
spec = JSON.parse(fs6.readFileSync(specPath, "utf-8"));
|
|
2088
|
-
} else {
|
|
2089
|
-
let targetDir = options.cwd;
|
|
2090
|
-
let entryFile = entry;
|
|
2091
|
-
const fileSystem = new NodeFileSystem4(options.cwd);
|
|
2092
|
-
if (options.package) {
|
|
2093
|
-
const mono = await detectMonorepo4(fileSystem);
|
|
2094
|
-
if (!mono.isMonorepo) {
|
|
2095
|
-
throw new Error(`Not a monorepo. Remove --package flag for single-package repos.`);
|
|
2096
|
-
}
|
|
2097
|
-
const pkg = findPackageByName4(mono.packages, options.package);
|
|
2098
|
-
if (!pkg) {
|
|
2099
|
-
const available = mono.packages.map((p) => p.name).join(", ");
|
|
2100
|
-
throw new Error(`Package "${options.package}" not found. Available: ${available}`);
|
|
2101
|
-
}
|
|
2102
|
-
targetDir = path7.join(options.cwd, pkg.path);
|
|
2103
|
-
}
|
|
2104
|
-
if (!entryFile) {
|
|
2105
|
-
const targetFs = new NodeFileSystem4(targetDir);
|
|
2106
|
-
const detected = await detectEntryPoint4(targetFs);
|
|
2107
|
-
entryFile = path7.join(targetDir, detected.path);
|
|
2108
|
-
} else {
|
|
2109
|
-
entryFile = path7.resolve(targetDir, entryFile);
|
|
2110
|
-
}
|
|
2111
|
-
process.stdout.write(chalk7.cyan(`> Analyzing...
|
|
2112
|
-
`));
|
|
2113
|
-
try {
|
|
2114
|
-
const resolveExternalTypes = !options.skipResolve;
|
|
2115
|
-
const doccov = new DocCov4({ resolveExternalTypes });
|
|
2116
|
-
const result = await doccov.analyzeFileWithDiagnostics(entryFile);
|
|
2117
|
-
process.stdout.write(chalk7.green(`✓ Analysis complete
|
|
2118
|
-
`));
|
|
2119
|
-
spec = result.spec;
|
|
2120
|
-
} catch (analysisError) {
|
|
2121
|
-
process.stdout.write(chalk7.red(`✗ Analysis failed
|
|
2122
|
-
`));
|
|
2123
|
-
throw analysisError;
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
const stats = computeStats(spec);
|
|
2127
|
-
const format = options.output;
|
|
2128
|
-
const limit = parseInt(options.limit, 10) || 20;
|
|
2129
|
-
let output;
|
|
2130
|
-
if (format === "json") {
|
|
2131
|
-
output = JSON.stringify(stats, null, 2);
|
|
2132
|
-
} else if (format === "html") {
|
|
2133
|
-
output = renderHtml(stats, { limit });
|
|
2134
|
-
} else {
|
|
2135
|
-
output = renderMarkdown(stats, { limit });
|
|
2136
|
-
}
|
|
2137
|
-
if (options.out) {
|
|
2138
|
-
const outPath = path7.resolve(options.cwd, options.out);
|
|
2139
|
-
fs6.writeFileSync(outPath, output);
|
|
2140
|
-
console.log(chalk7.green(`Report written to ${outPath}`));
|
|
2141
|
-
} else {
|
|
2142
|
-
console.log(output);
|
|
2143
|
-
}
|
|
2144
|
-
} catch (err) {
|
|
2145
|
-
console.error(chalk7.red("Error:"), err instanceof Error ? err.message : err);
|
|
2146
|
-
process.exitCode = 1;
|
|
2147
|
-
}
|
|
2148
|
-
});
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
// src/commands/scan.ts
|
|
2152
|
-
import * as fs8 from "node:fs";
|
|
2153
|
-
import * as os from "node:os";
|
|
2154
|
-
import * as path9 from "node:path";
|
|
2155
|
-
import {
|
|
2156
|
-
DocCov as DocCov5,
|
|
2157
|
-
detectBuildInfo,
|
|
2158
|
-
detectEntryPoint as detectEntryPoint5,
|
|
2159
|
-
detectMonorepo as detectMonorepo5,
|
|
2160
|
-
detectPackageManager,
|
|
2161
|
-
findPackageByName as findPackageByName5,
|
|
2162
|
-
formatPackageList,
|
|
2163
|
-
getInstallCommand,
|
|
2164
|
-
NodeFileSystem as NodeFileSystem5
|
|
2165
|
-
} from "@doccov/sdk";
|
|
2166
|
-
import chalk8 from "chalk";
|
|
2167
1827
|
import { simpleGit } from "simple-git";
|
|
2168
1828
|
|
|
2169
|
-
// src/utils/github-url.ts
|
|
2170
|
-
function parseGitHubUrl(input, defaultRef = "main") {
|
|
2171
|
-
const trimmed = input.trim();
|
|
2172
|
-
if (!trimmed) {
|
|
2173
|
-
throw new Error("GitHub URL cannot be empty");
|
|
2174
|
-
}
|
|
2175
|
-
let normalized = trimmed.replace(/^https?:\/\//, "").replace(/^git@github\.com:/, "").replace(/\.git$/, "");
|
|
2176
|
-
normalized = normalized.replace(/^github\.com\//, "");
|
|
2177
|
-
const parts = normalized.split("/").filter(Boolean);
|
|
2178
|
-
if (parts.length < 2) {
|
|
2179
|
-
throw new Error(`Invalid GitHub URL format: "${input}". Expected owner/repo or https://github.com/owner/repo`);
|
|
2180
|
-
}
|
|
2181
|
-
const owner = parts[0];
|
|
2182
|
-
const repo = parts[1];
|
|
2183
|
-
let ref = defaultRef;
|
|
2184
|
-
if (parts.length >= 4 && (parts[2] === "tree" || parts[2] === "blob")) {
|
|
2185
|
-
ref = parts.slice(3).join("/");
|
|
2186
|
-
}
|
|
2187
|
-
if (!owner || !repo) {
|
|
2188
|
-
throw new Error(`Could not parse owner/repo from: "${input}"`);
|
|
2189
|
-
}
|
|
2190
|
-
return { owner, repo, ref };
|
|
2191
|
-
}
|
|
2192
|
-
function buildCloneUrl(parsed) {
|
|
2193
|
-
return `https://github.com/${parsed.owner}/${parsed.repo}.git`;
|
|
2194
|
-
}
|
|
2195
|
-
function buildDisplayUrl(parsed) {
|
|
2196
|
-
return `github.com/${parsed.owner}/${parsed.repo}`;
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
1829
|
// src/utils/llm-build-plan.ts
|
|
2200
|
-
import * as
|
|
2201
|
-
import * as
|
|
1830
|
+
import * as fs5 from "node:fs";
|
|
1831
|
+
import * as path6 from "node:path";
|
|
2202
1832
|
import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
|
|
2203
1833
|
import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
|
|
2204
1834
|
import { generateObject as generateObject3 } from "ai";
|
|
@@ -2234,10 +1864,10 @@ function getModel3() {
|
|
|
2234
1864
|
async function gatherContextFiles(repoDir) {
|
|
2235
1865
|
const sections = [];
|
|
2236
1866
|
for (const fileName of CONTEXT_FILES) {
|
|
2237
|
-
const filePath =
|
|
2238
|
-
if (
|
|
1867
|
+
const filePath = path6.join(repoDir, fileName);
|
|
1868
|
+
if (fs5.existsSync(filePath)) {
|
|
2239
1869
|
try {
|
|
2240
|
-
let content =
|
|
1870
|
+
let content = fs5.readFileSync(filePath, "utf-8");
|
|
2241
1871
|
if (content.length > MAX_FILE_CHARS) {
|
|
2242
1872
|
content = `${content.slice(0, MAX_FILE_CHARS)}
|
|
2243
1873
|
... (truncated)`;
|
|
@@ -2289,14 +1919,14 @@ async function generateBuildPlan(repoDir) {
|
|
|
2289
1919
|
}
|
|
2290
1920
|
|
|
2291
1921
|
// src/commands/scan.ts
|
|
2292
|
-
var
|
|
2293
|
-
createDocCov: (options) => new
|
|
1922
|
+
var defaultDependencies5 = {
|
|
1923
|
+
createDocCov: (options) => new DocCov3(options),
|
|
2294
1924
|
log: console.log,
|
|
2295
1925
|
error: console.error
|
|
2296
1926
|
};
|
|
2297
1927
|
function registerScanCommand(program, dependencies = {}) {
|
|
2298
1928
|
const { createDocCov, log, error } = {
|
|
2299
|
-
...
|
|
1929
|
+
...defaultDependencies5,
|
|
2300
1930
|
...dependencies
|
|
2301
1931
|
};
|
|
2302
1932
|
program.command("scan <url>").description("Analyze docs coverage for any public GitHub repository").option("--ref <branch>", "Branch or tag to analyze").option("--package <name>", "Target package in monorepo").option("--output <format>", "Output format: text or json", "text").option("--no-cleanup", "Keep cloned repo (for debugging)").option("--skip-install", "Skip dependency installation (faster, but may limit type resolution)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--save-spec <path>", "Save full OpenPkg spec to file").action(async (url, options) => {
|
|
@@ -2306,12 +1936,12 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2306
1936
|
const cloneUrl = buildCloneUrl(parsed);
|
|
2307
1937
|
const displayUrl = buildDisplayUrl(parsed);
|
|
2308
1938
|
log("");
|
|
2309
|
-
log(
|
|
2310
|
-
log(
|
|
1939
|
+
log(chalk6.bold(`Scanning ${displayUrl}`));
|
|
1940
|
+
log(chalk6.gray(`Branch/tag: ${parsed.ref}`));
|
|
2311
1941
|
log("");
|
|
2312
|
-
tempDir =
|
|
2313
|
-
|
|
2314
|
-
process.stdout.write(
|
|
1942
|
+
tempDir = path7.join(os.tmpdir(), `doccov-scan-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
1943
|
+
fs6.mkdirSync(tempDir, { recursive: true });
|
|
1944
|
+
process.stdout.write(chalk6.cyan(`> Cloning ${parsed.owner}/${parsed.repo}...
|
|
2315
1945
|
`));
|
|
2316
1946
|
try {
|
|
2317
1947
|
const git = simpleGit({
|
|
@@ -2333,10 +1963,10 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2333
1963
|
} finally {
|
|
2334
1964
|
process.env = originalEnv;
|
|
2335
1965
|
}
|
|
2336
|
-
process.stdout.write(
|
|
1966
|
+
process.stdout.write(chalk6.green(`✓ Cloned ${parsed.owner}/${parsed.repo}
|
|
2337
1967
|
`));
|
|
2338
1968
|
} catch (cloneError) {
|
|
2339
|
-
process.stdout.write(
|
|
1969
|
+
process.stdout.write(chalk6.red(`✗ Failed to clone repository
|
|
2340
1970
|
`));
|
|
2341
1971
|
const message = cloneError instanceof Error ? cloneError.message : String(cloneError);
|
|
2342
1972
|
if (message.includes("Authentication failed") || message.includes("could not read Username") || message.includes("terminal prompts disabled") || message.includes("Invalid username or password") || message.includes("Permission denied")) {
|
|
@@ -2352,11 +1982,11 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2352
1982
|
}
|
|
2353
1983
|
throw new Error(`Clone failed: ${message}`);
|
|
2354
1984
|
}
|
|
2355
|
-
const fileSystem = new
|
|
1985
|
+
const fileSystem = new NodeFileSystem3(tempDir);
|
|
2356
1986
|
if (options.skipInstall) {
|
|
2357
|
-
log(
|
|
1987
|
+
log(chalk6.gray("Skipping dependency installation (--skip-install)"));
|
|
2358
1988
|
} else {
|
|
2359
|
-
process.stdout.write(
|
|
1989
|
+
process.stdout.write(chalk6.cyan(`> Installing dependencies...
|
|
2360
1990
|
`));
|
|
2361
1991
|
const installErrors = [];
|
|
2362
1992
|
try {
|
|
@@ -2406,56 +2036,56 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2406
2036
|
}
|
|
2407
2037
|
}
|
|
2408
2038
|
if (installed) {
|
|
2409
|
-
process.stdout.write(
|
|
2039
|
+
process.stdout.write(chalk6.green(`✓ Dependencies installed
|
|
2410
2040
|
`));
|
|
2411
2041
|
} else {
|
|
2412
|
-
process.stdout.write(
|
|
2042
|
+
process.stdout.write(chalk6.yellow(`⚠ Could not install dependencies (analysis may be limited)
|
|
2413
2043
|
`));
|
|
2414
2044
|
for (const err of installErrors) {
|
|
2415
|
-
log(
|
|
2045
|
+
log(chalk6.gray(` ${err}`));
|
|
2416
2046
|
}
|
|
2417
2047
|
}
|
|
2418
2048
|
} catch (outerError) {
|
|
2419
2049
|
const msg = outerError instanceof Error ? outerError.message : String(outerError);
|
|
2420
|
-
process.stdout.write(
|
|
2050
|
+
process.stdout.write(chalk6.yellow(`⚠ Could not install dependencies: ${msg.slice(0, 100)}
|
|
2421
2051
|
`));
|
|
2422
2052
|
for (const err of installErrors) {
|
|
2423
|
-
log(
|
|
2053
|
+
log(chalk6.gray(` ${err}`));
|
|
2424
2054
|
}
|
|
2425
2055
|
}
|
|
2426
2056
|
}
|
|
2427
2057
|
let targetDir = tempDir;
|
|
2428
2058
|
let packageName;
|
|
2429
|
-
const mono = await
|
|
2059
|
+
const mono = await detectMonorepo(fileSystem);
|
|
2430
2060
|
if (mono.isMonorepo) {
|
|
2431
2061
|
if (!options.package) {
|
|
2432
2062
|
error("");
|
|
2433
|
-
error(
|
|
2063
|
+
error(chalk6.red(`Monorepo detected with ${mono.packages.length} packages. Specify target with --package:`));
|
|
2434
2064
|
error("");
|
|
2435
2065
|
error(formatPackageList(mono.packages));
|
|
2436
2066
|
error("");
|
|
2437
2067
|
throw new Error("Monorepo requires --package flag");
|
|
2438
2068
|
}
|
|
2439
|
-
const pkg =
|
|
2069
|
+
const pkg = findPackageByName(mono.packages, options.package);
|
|
2440
2070
|
if (!pkg) {
|
|
2441
2071
|
error("");
|
|
2442
|
-
error(
|
|
2072
|
+
error(chalk6.red(`Package "${options.package}" not found. Available packages:`));
|
|
2443
2073
|
error("");
|
|
2444
2074
|
error(formatPackageList(mono.packages));
|
|
2445
2075
|
error("");
|
|
2446
2076
|
throw new Error(`Package not found: ${options.package}`);
|
|
2447
2077
|
}
|
|
2448
|
-
targetDir =
|
|
2078
|
+
targetDir = path7.join(tempDir, pkg.path);
|
|
2449
2079
|
packageName = pkg.name;
|
|
2450
|
-
log(
|
|
2080
|
+
log(chalk6.gray(`Analyzing package: ${packageName}`));
|
|
2451
2081
|
}
|
|
2452
|
-
process.stdout.write(
|
|
2082
|
+
process.stdout.write(chalk6.cyan(`> Detecting entry point...
|
|
2453
2083
|
`));
|
|
2454
2084
|
let entryPath;
|
|
2455
|
-
const targetFs = mono.isMonorepo ? new
|
|
2085
|
+
const targetFs = mono.isMonorepo ? new NodeFileSystem3(targetDir) : fileSystem;
|
|
2456
2086
|
let buildFailed = false;
|
|
2457
2087
|
const runLlmFallback = async (reason) => {
|
|
2458
|
-
process.stdout.write(
|
|
2088
|
+
process.stdout.write(chalk6.cyan(`> ${reason}, trying LLM fallback...
|
|
2459
2089
|
`));
|
|
2460
2090
|
const plan = await generateBuildPlan(targetDir);
|
|
2461
2091
|
if (!plan) {
|
|
@@ -2464,117 +2094,100 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2464
2094
|
if (plan.buildCommands.length > 0) {
|
|
2465
2095
|
const { execSync } = await import("node:child_process");
|
|
2466
2096
|
for (const cmd of plan.buildCommands) {
|
|
2467
|
-
log(
|
|
2097
|
+
log(chalk6.gray(` Running: ${cmd}`));
|
|
2468
2098
|
try {
|
|
2469
2099
|
execSync(cmd, { cwd: targetDir, stdio: "pipe", timeout: 300000 });
|
|
2470
2100
|
} catch (buildError) {
|
|
2471
2101
|
buildFailed = true;
|
|
2472
2102
|
const msg = buildError instanceof Error ? buildError.message : String(buildError);
|
|
2473
2103
|
if (msg.includes("rustc") || msg.includes("cargo") || msg.includes("wasm-pack")) {
|
|
2474
|
-
log(
|
|
2104
|
+
log(chalk6.yellow(` ⚠ Build requires Rust toolchain (not available)`));
|
|
2475
2105
|
} else if (msg.includes("rimraf") || msg.includes("command not found")) {
|
|
2476
|
-
log(
|
|
2106
|
+
log(chalk6.yellow(` ⚠ Build failed: missing dependencies`));
|
|
2477
2107
|
} else {
|
|
2478
|
-
log(
|
|
2108
|
+
log(chalk6.yellow(` ⚠ Build failed: ${msg.slice(0, 80)}`));
|
|
2479
2109
|
}
|
|
2480
2110
|
}
|
|
2481
2111
|
}
|
|
2482
2112
|
}
|
|
2483
2113
|
if (plan.notes) {
|
|
2484
|
-
log(
|
|
2114
|
+
log(chalk6.gray(` Note: ${plan.notes}`));
|
|
2485
2115
|
}
|
|
2486
2116
|
return plan.entryPoint;
|
|
2487
2117
|
};
|
|
2488
2118
|
try {
|
|
2489
|
-
const entry = await
|
|
2119
|
+
const entry = await detectEntryPoint(targetFs);
|
|
2490
2120
|
const buildInfo = await detectBuildInfo(targetFs);
|
|
2491
2121
|
const needsBuildStep = entry.isDeclarationOnly && buildInfo.exoticIndicators.wasm;
|
|
2492
2122
|
if (needsBuildStep) {
|
|
2493
|
-
process.stdout.write(
|
|
2123
|
+
process.stdout.write(chalk6.cyan(`> Detected .d.ts entry with WASM indicators...
|
|
2494
2124
|
`));
|
|
2495
2125
|
const llmEntry = await runLlmFallback("WASM project detected");
|
|
2496
2126
|
if (llmEntry) {
|
|
2497
|
-
entryPath =
|
|
2127
|
+
entryPath = path7.join(targetDir, llmEntry);
|
|
2498
2128
|
if (buildFailed) {
|
|
2499
|
-
process.stdout.write(
|
|
2129
|
+
process.stdout.write(chalk6.green(`✓ Entry point: ${llmEntry} (using pre-committed declarations)
|
|
2500
2130
|
`));
|
|
2501
|
-
log(
|
|
2131
|
+
log(chalk6.gray(" Coverage may be limited - generated .d.ts files typically lack JSDoc"));
|
|
2502
2132
|
} else {
|
|
2503
|
-
process.stdout.write(
|
|
2133
|
+
process.stdout.write(chalk6.green(`✓ Entry point: ${llmEntry} (from LLM fallback - WASM project)
|
|
2504
2134
|
`));
|
|
2505
2135
|
}
|
|
2506
2136
|
} else {
|
|
2507
|
-
entryPath =
|
|
2508
|
-
process.stdout.write(
|
|
2137
|
+
entryPath = path7.join(targetDir, entry.path);
|
|
2138
|
+
process.stdout.write(chalk6.green(`✓ Entry point: ${entry.path} (from ${entry.source})
|
|
2509
2139
|
`));
|
|
2510
|
-
log(
|
|
2140
|
+
log(chalk6.yellow(" ⚠ WASM project detected but no API key - analysis may be limited"));
|
|
2511
2141
|
}
|
|
2512
2142
|
} else {
|
|
2513
|
-
entryPath =
|
|
2514
|
-
process.stdout.write(
|
|
2143
|
+
entryPath = path7.join(targetDir, entry.path);
|
|
2144
|
+
process.stdout.write(chalk6.green(`✓ Entry point: ${entry.path} (from ${entry.source})
|
|
2515
2145
|
`));
|
|
2516
2146
|
}
|
|
2517
2147
|
} catch (entryError) {
|
|
2518
2148
|
const llmEntry = await runLlmFallback("Heuristics failed");
|
|
2519
2149
|
if (llmEntry) {
|
|
2520
|
-
entryPath =
|
|
2521
|
-
process.stdout.write(
|
|
2150
|
+
entryPath = path7.join(targetDir, llmEntry);
|
|
2151
|
+
process.stdout.write(chalk6.green(`✓ Entry point: ${llmEntry} (from LLM fallback)
|
|
2522
2152
|
`));
|
|
2523
2153
|
} else {
|
|
2524
|
-
process.stdout.write(
|
|
2154
|
+
process.stdout.write(chalk6.red(`✗ Could not detect entry point (set OPENAI_API_KEY for smart fallback)
|
|
2525
2155
|
`));
|
|
2526
2156
|
throw entryError;
|
|
2527
2157
|
}
|
|
2528
2158
|
}
|
|
2529
|
-
process.stdout.write(
|
|
2159
|
+
process.stdout.write(chalk6.cyan(`> Analyzing documentation coverage...
|
|
2530
2160
|
`));
|
|
2531
2161
|
let result;
|
|
2532
2162
|
try {
|
|
2533
2163
|
const resolveExternalTypes = !options.skipResolve;
|
|
2534
2164
|
const doccov = createDocCov({ resolveExternalTypes });
|
|
2535
2165
|
result = await doccov.analyzeFileWithDiagnostics(entryPath);
|
|
2536
|
-
process.stdout.write(
|
|
2166
|
+
process.stdout.write(chalk6.green(`✓ Analysis complete
|
|
2537
2167
|
`));
|
|
2538
2168
|
} catch (analysisError) {
|
|
2539
|
-
process.stdout.write(
|
|
2169
|
+
process.stdout.write(chalk6.red(`✗ Analysis failed
|
|
2540
2170
|
`));
|
|
2541
2171
|
throw analysisError;
|
|
2542
2172
|
}
|
|
2543
2173
|
const spec = result.spec;
|
|
2544
|
-
const coverageScore = spec.docs?.coverageScore ?? 0;
|
|
2545
2174
|
if (options.saveSpec) {
|
|
2546
|
-
const specPath =
|
|
2547
|
-
|
|
2548
|
-
log(
|
|
2549
|
-
}
|
|
2550
|
-
const undocumented = [];
|
|
2551
|
-
const driftIssues = [];
|
|
2552
|
-
for (const exp of spec.exports ?? []) {
|
|
2553
|
-
const expDocs = exp.docs;
|
|
2554
|
-
if (!expDocs)
|
|
2555
|
-
continue;
|
|
2556
|
-
if ((expDocs.missing?.length ?? 0) > 0 || (expDocs.coverageScore ?? 0) < 100) {
|
|
2557
|
-
undocumented.push(exp.name);
|
|
2558
|
-
}
|
|
2559
|
-
for (const d of expDocs.drift ?? []) {
|
|
2560
|
-
driftIssues.push({
|
|
2561
|
-
export: exp.name,
|
|
2562
|
-
type: d.type,
|
|
2563
|
-
issue: d.issue
|
|
2564
|
-
});
|
|
2565
|
-
}
|
|
2175
|
+
const specPath = path7.resolve(process.cwd(), options.saveSpec);
|
|
2176
|
+
fs6.writeFileSync(specPath, JSON.stringify(spec, null, 2));
|
|
2177
|
+
log(chalk6.green(`✓ Saved spec to ${options.saveSpec}`));
|
|
2566
2178
|
}
|
|
2179
|
+
const summary = extractSpecSummary(spec);
|
|
2567
2180
|
const scanResult = {
|
|
2568
2181
|
owner: parsed.owner,
|
|
2569
2182
|
repo: parsed.repo,
|
|
2570
2183
|
ref: parsed.ref,
|
|
2571
2184
|
packageName,
|
|
2572
|
-
coverage:
|
|
2573
|
-
exportCount:
|
|
2574
|
-
typeCount:
|
|
2575
|
-
driftCount:
|
|
2576
|
-
undocumented,
|
|
2577
|
-
drift:
|
|
2185
|
+
coverage: summary.coverage,
|
|
2186
|
+
exportCount: summary.exportCount,
|
|
2187
|
+
typeCount: summary.typeCount,
|
|
2188
|
+
driftCount: summary.driftCount,
|
|
2189
|
+
undocumented: summary.undocumented,
|
|
2190
|
+
drift: summary.drift
|
|
2578
2191
|
};
|
|
2579
2192
|
if (options.output === "json") {
|
|
2580
2193
|
log(JSON.stringify(scanResult, null, 2));
|
|
@@ -2582,190 +2195,68 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2582
2195
|
printTextResult(scanResult, log);
|
|
2583
2196
|
}
|
|
2584
2197
|
} catch (commandError) {
|
|
2585
|
-
error(
|
|
2198
|
+
error(chalk6.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
2586
2199
|
process.exitCode = 1;
|
|
2587
2200
|
} finally {
|
|
2588
2201
|
if (tempDir && options.cleanup !== false) {
|
|
2589
|
-
|
|
2590
|
-
spawn("rm", ["-rf", tempDir], {
|
|
2591
|
-
detached: true,
|
|
2592
|
-
stdio: "ignore"
|
|
2593
|
-
}).unref();
|
|
2202
|
+
fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
2594
2203
|
} else if (tempDir) {
|
|
2595
|
-
log(
|
|
2204
|
+
log(chalk6.gray(`Repo preserved at: ${tempDir}`));
|
|
2596
2205
|
}
|
|
2597
2206
|
}
|
|
2598
2207
|
});
|
|
2599
2208
|
}
|
|
2600
2209
|
function printTextResult(result, log) {
|
|
2601
2210
|
log("");
|
|
2602
|
-
log(
|
|
2211
|
+
log(chalk6.bold("DocCov Scan Results"));
|
|
2603
2212
|
log("─".repeat(40));
|
|
2604
2213
|
const repoName = result.packageName ? `${result.owner}/${result.repo} (${result.packageName})` : `${result.owner}/${result.repo}`;
|
|
2605
|
-
log(`Repository: ${
|
|
2606
|
-
log(`Branch: ${
|
|
2214
|
+
log(`Repository: ${chalk6.cyan(repoName)}`);
|
|
2215
|
+
log(`Branch: ${chalk6.gray(result.ref)}`);
|
|
2607
2216
|
log("");
|
|
2608
|
-
const coverageColor = result.coverage >= 80 ?
|
|
2609
|
-
log(
|
|
2217
|
+
const coverageColor = result.coverage >= 80 ? chalk6.green : result.coverage >= 50 ? chalk6.yellow : chalk6.red;
|
|
2218
|
+
log(chalk6.bold("Coverage"));
|
|
2610
2219
|
log(` ${coverageColor(`${result.coverage}%`)}`);
|
|
2611
2220
|
log("");
|
|
2612
|
-
log(
|
|
2221
|
+
log(chalk6.bold("Stats"));
|
|
2613
2222
|
log(` ${result.exportCount} exports`);
|
|
2614
2223
|
log(` ${result.typeCount} types`);
|
|
2615
2224
|
log(` ${result.undocumented.length} undocumented`);
|
|
2616
2225
|
log(` ${result.driftCount} drift issues`);
|
|
2617
2226
|
if (result.undocumented.length > 0) {
|
|
2618
2227
|
log("");
|
|
2619
|
-
log(
|
|
2228
|
+
log(chalk6.bold("Undocumented Exports"));
|
|
2620
2229
|
for (const name of result.undocumented.slice(0, 10)) {
|
|
2621
|
-
log(
|
|
2230
|
+
log(chalk6.yellow(` ! ${name}`));
|
|
2622
2231
|
}
|
|
2623
2232
|
if (result.undocumented.length > 10) {
|
|
2624
|
-
log(
|
|
2233
|
+
log(chalk6.gray(` ... and ${result.undocumented.length - 10} more`));
|
|
2625
2234
|
}
|
|
2626
2235
|
}
|
|
2627
2236
|
if (result.drift.length > 0) {
|
|
2628
2237
|
log("");
|
|
2629
|
-
log(
|
|
2238
|
+
log(chalk6.bold("Drift Issues"));
|
|
2630
2239
|
for (const d of result.drift.slice(0, 5)) {
|
|
2631
|
-
log(
|
|
2240
|
+
log(chalk6.red(` • ${d.export}: ${d.issue}`));
|
|
2632
2241
|
}
|
|
2633
2242
|
if (result.drift.length > 5) {
|
|
2634
|
-
log(
|
|
2243
|
+
log(chalk6.gray(` ... and ${result.drift.length - 5} more`));
|
|
2635
2244
|
}
|
|
2636
2245
|
}
|
|
2637
2246
|
log("");
|
|
2638
2247
|
}
|
|
2639
2248
|
|
|
2640
|
-
// src/commands/typecheck.ts
|
|
2641
|
-
import * as fs9 from "node:fs";
|
|
2642
|
-
import * as path10 from "node:path";
|
|
2643
|
-
import {
|
|
2644
|
-
detectEntryPoint as detectEntryPoint6,
|
|
2645
|
-
detectMonorepo as detectMonorepo6,
|
|
2646
|
-
DocCov as DocCov6,
|
|
2647
|
-
findPackageByName as findPackageByName6,
|
|
2648
|
-
NodeFileSystem as NodeFileSystem6,
|
|
2649
|
-
typecheckExamples as typecheckExamples2
|
|
2650
|
-
} from "@doccov/sdk";
|
|
2651
|
-
import chalk9 from "chalk";
|
|
2652
|
-
var defaultDependencies7 = {
|
|
2653
|
-
createDocCov: (options) => new DocCov6(options),
|
|
2654
|
-
log: console.log,
|
|
2655
|
-
error: console.error
|
|
2656
|
-
};
|
|
2657
|
-
function registerTypecheckCommand(program, dependencies = {}) {
|
|
2658
|
-
const { createDocCov, log, error } = {
|
|
2659
|
-
...defaultDependencies7,
|
|
2660
|
-
...dependencies
|
|
2661
|
-
};
|
|
2662
|
-
program.command("typecheck [entry]").description("Type-check @example blocks without executing them").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--skip-resolve", "Skip external type resolution").action(async (entry, options) => {
|
|
2663
|
-
try {
|
|
2664
|
-
let targetDir = options.cwd;
|
|
2665
|
-
let entryFile = entry;
|
|
2666
|
-
const fileSystem = new NodeFileSystem6(options.cwd);
|
|
2667
|
-
if (options.package) {
|
|
2668
|
-
const mono = await detectMonorepo6(fileSystem);
|
|
2669
|
-
if (!mono.isMonorepo) {
|
|
2670
|
-
throw new Error("Not a monorepo. Remove --package flag.");
|
|
2671
|
-
}
|
|
2672
|
-
const pkg = findPackageByName6(mono.packages, options.package);
|
|
2673
|
-
if (!pkg) {
|
|
2674
|
-
const available = mono.packages.map((p) => p.name).join(", ");
|
|
2675
|
-
throw new Error(`Package "${options.package}" not found. Available: ${available}`);
|
|
2676
|
-
}
|
|
2677
|
-
targetDir = path10.join(options.cwd, pkg.path);
|
|
2678
|
-
log(chalk9.gray(`Found package at ${pkg.path}`));
|
|
2679
|
-
}
|
|
2680
|
-
if (!entryFile) {
|
|
2681
|
-
const targetFs = new NodeFileSystem6(targetDir);
|
|
2682
|
-
const detected = await detectEntryPoint6(targetFs);
|
|
2683
|
-
entryFile = path10.join(targetDir, detected.path);
|
|
2684
|
-
log(chalk9.gray(`Auto-detected entry point: ${detected.path}`));
|
|
2685
|
-
} else {
|
|
2686
|
-
entryFile = path10.resolve(targetDir, entryFile);
|
|
2687
|
-
if (fs9.existsSync(entryFile) && fs9.statSync(entryFile).isDirectory()) {
|
|
2688
|
-
targetDir = entryFile;
|
|
2689
|
-
const dirFs = new NodeFileSystem6(entryFile);
|
|
2690
|
-
const detected = await detectEntryPoint6(dirFs);
|
|
2691
|
-
entryFile = path10.join(entryFile, detected.path);
|
|
2692
|
-
log(chalk9.gray(`Auto-detected entry point: ${detected.path}`));
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
const resolveExternalTypes = !options.skipResolve;
|
|
2696
|
-
process.stdout.write(chalk9.cyan(`> Analyzing documentation...
|
|
2697
|
-
`));
|
|
2698
|
-
const doccov = createDocCov({ resolveExternalTypes });
|
|
2699
|
-
const specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
|
|
2700
|
-
if (!specResult) {
|
|
2701
|
-
throw new Error("Failed to analyze documentation.");
|
|
2702
|
-
}
|
|
2703
|
-
const allExamples = [];
|
|
2704
|
-
for (const exp of specResult.spec.exports ?? []) {
|
|
2705
|
-
if (exp.examples && exp.examples.length > 0) {
|
|
2706
|
-
allExamples.push({ exportName: exp.name, examples: exp.examples });
|
|
2707
|
-
}
|
|
2708
|
-
}
|
|
2709
|
-
if (allExamples.length === 0) {
|
|
2710
|
-
log(chalk9.gray("No @example blocks found"));
|
|
2711
|
-
return;
|
|
2712
|
-
}
|
|
2713
|
-
const totalExamples = allExamples.reduce((sum, e) => sum + e.examples.length, 0);
|
|
2714
|
-
process.stdout.write(chalk9.cyan(`> Type-checking ${totalExamples} example(s)...
|
|
2715
|
-
`));
|
|
2716
|
-
const allErrors = [];
|
|
2717
|
-
let passed = 0;
|
|
2718
|
-
let failed = 0;
|
|
2719
|
-
for (const { exportName, examples } of allExamples) {
|
|
2720
|
-
const result = typecheckExamples2(examples, targetDir);
|
|
2721
|
-
for (const err of result.errors) {
|
|
2722
|
-
allErrors.push({ exportName, error: err });
|
|
2723
|
-
}
|
|
2724
|
-
passed += result.passed;
|
|
2725
|
-
failed += result.failed;
|
|
2726
|
-
}
|
|
2727
|
-
if (allErrors.length === 0) {
|
|
2728
|
-
log(chalk9.green(`✓ All ${totalExamples} example(s) passed type checking`));
|
|
2729
|
-
return;
|
|
2730
|
-
}
|
|
2731
|
-
log("");
|
|
2732
|
-
const byExport = new Map;
|
|
2733
|
-
for (const { exportName, error: err } of allErrors) {
|
|
2734
|
-
const existing = byExport.get(exportName) ?? [];
|
|
2735
|
-
existing.push(err);
|
|
2736
|
-
byExport.set(exportName, existing);
|
|
2737
|
-
}
|
|
2738
|
-
for (const [exportName, errors] of byExport) {
|
|
2739
|
-
log(chalk9.red(`✗ ${exportName}`));
|
|
2740
|
-
for (const err of errors) {
|
|
2741
|
-
log(chalk9.gray(` @example block ${err.exampleIndex + 1}, line ${err.line}:`));
|
|
2742
|
-
log(chalk9.red(` ${err.message}`));
|
|
2743
|
-
}
|
|
2744
|
-
log("");
|
|
2745
|
-
}
|
|
2746
|
-
log(chalk9.red(`${failed} example(s) failed`) + chalk9.gray(`, ${passed} passed`));
|
|
2747
|
-
process.exit(1);
|
|
2748
|
-
} catch (commandError) {
|
|
2749
|
-
error(chalk9.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
2750
|
-
process.exit(1);
|
|
2751
|
-
}
|
|
2752
|
-
});
|
|
2753
|
-
}
|
|
2754
|
-
|
|
2755
2249
|
// src/cli.ts
|
|
2756
2250
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2757
|
-
var __dirname2 =
|
|
2758
|
-
var packageJson = JSON.parse(
|
|
2251
|
+
var __dirname2 = path8.dirname(__filename2);
|
|
2252
|
+
var packageJson = JSON.parse(readFileSync4(path8.join(__dirname2, "../package.json"), "utf-8"));
|
|
2759
2253
|
var program = new Command;
|
|
2760
2254
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
2761
2255
|
registerGenerateCommand(program);
|
|
2762
2256
|
registerCheckCommand(program);
|
|
2763
2257
|
registerDiffCommand(program);
|
|
2764
2258
|
registerInitCommand(program);
|
|
2765
|
-
registerLintCommand(program);
|
|
2766
|
-
registerReportCommand(program);
|
|
2767
2259
|
registerScanCommand(program);
|
|
2768
|
-
registerTypecheckCommand(program);
|
|
2769
2260
|
program.command("*", { hidden: true }).action(() => {
|
|
2770
2261
|
program.outputHelp();
|
|
2771
2262
|
});
|