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