@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 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 readFileSync5 } from "node:fs";
172
- import * as path11 from "node:path";
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("--only <types>", "Only fix specific drift types (comma-separated)").option("--dry-run", "Preview fixes without writing (requires --fix)").action(async (entry, options) => {
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
- if (options.package) {
313
- const mono = await detectMonorepo(fileSystem);
314
- if (!mono.isMonorepo) {
315
- throw new Error(`Not a monorepo. Remove --package flag for single-package repos.`);
316
- }
317
- const pkg = findPackageByName(mono.packages, options.package);
318
- if (!pkg) {
319
- const available = mono.packages.map((p) => p.name).join(", ");
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 (!entryFile) {
326
- const targetFs = new NodeFileSystem(targetDir);
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({ exportName: exp.name, examples: exp.examples });
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
- const filteredDrifts = filterDriftsByType(allDrifts, options.only);
538
- if (filteredDrifts.length === 0 && options.only) {
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(filteredDrifts.filter((d) => fixable.includes(d.drift)));
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 + "()").join(", ");
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
- detectEntryPoint as detectEntryPoint2,
1365
- detectMonorepo as detectMonorepo2,
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
- const configInclude = config?.include;
1387
- const configExclude = config?.exclude;
1388
- const cliInclude = cliOptions.include;
1389
- const cliExclude = cliOptions.exclude;
1390
- let include = configInclude;
1391
- let exclude = configExclude;
1392
- let source = include || exclude ? "config" : undefined;
1393
- if (configInclude) {
1394
- messages.push(formatList("include filters from config", configInclude));
1395
- }
1396
- if (configExclude) {
1397
- messages.push(formatList("exclude filters from config", configExclude));
1398
- }
1399
- if (cliInclude) {
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
- if (options.package) {
1460
- const mono = await detectMonorepo2(fileSystem);
1461
- if (!mono.isMonorepo) {
1462
- throw new Error(`Not a monorepo. Remove --package flag for single-package repos.`);
1463
- }
1464
- const pkg = findPackageByName2(mono.packages, options.package);
1465
- if (!pkg) {
1466
- const available = mono.packages.map((p) => p.name).join(", ");
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 (!entryFile) {
1473
- const targetFs = new NodeFileSystem2(targetDir);
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
- spinnerInstance.fail("Spec failed schema validation");
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
- writeFileSync2(outputPath, JSON.stringify(normalized, null, 2));
1543
- log(chalk4.green(`✓ Generated ${options.output}`));
1544
- log(chalk4.gray(` ${getArrayLength(normalized.exports)} exports`));
1545
- log(chalk4.gray(` ${getArrayLength(normalized.types)} types`));
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/lint.ts
1692
- import * as fs5 from "node:fs";
1693
- import * as path6 from "node:path";
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
- findJSDocLocation as findJSDocLocation2,
1701
- findPackageByName as findPackageByName3,
1702
- getDefaultConfig,
1703
- getRule,
1704
- lintExport as lintExport2,
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
- serializeJSDoc as serializeJSDoc2
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 fs7 from "node:fs";
2201
- import * as path8 from "node:path";
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 = path8.join(repoDir, fileName);
2238
- if (fs7.existsSync(filePath)) {
1867
+ const filePath = path6.join(repoDir, fileName);
1868
+ if (fs5.existsSync(filePath)) {
2239
1869
  try {
2240
- let content = fs7.readFileSync(filePath, "utf-8");
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 defaultDependencies6 = {
2293
- createDocCov: (options) => new DocCov5(options),
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
- ...defaultDependencies6,
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(chalk8.bold(`Scanning ${displayUrl}`));
2310
- log(chalk8.gray(`Branch/tag: ${parsed.ref}`));
1939
+ log(chalk6.bold(`Scanning ${displayUrl}`));
1940
+ log(chalk6.gray(`Branch/tag: ${parsed.ref}`));
2311
1941
  log("");
2312
- tempDir = path9.join(os.tmpdir(), `doccov-scan-${Date.now()}-${Math.random().toString(36).slice(2)}`);
2313
- fs8.mkdirSync(tempDir, { recursive: true });
2314
- process.stdout.write(chalk8.cyan(`> Cloning ${parsed.owner}/${parsed.repo}...
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(chalk8.green(`✓ Cloned ${parsed.owner}/${parsed.repo}
1966
+ process.stdout.write(chalk6.green(`✓ Cloned ${parsed.owner}/${parsed.repo}
2337
1967
  `));
2338
1968
  } catch (cloneError) {
2339
- process.stdout.write(chalk8.red(`✗ Failed to clone repository
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 NodeFileSystem5(tempDir);
1985
+ const fileSystem = new NodeFileSystem3(tempDir);
2356
1986
  if (options.skipInstall) {
2357
- log(chalk8.gray("Skipping dependency installation (--skip-install)"));
1987
+ log(chalk6.gray("Skipping dependency installation (--skip-install)"));
2358
1988
  } else {
2359
- process.stdout.write(chalk8.cyan(`> Installing dependencies...
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(chalk8.green(`✓ Dependencies installed
2039
+ process.stdout.write(chalk6.green(`✓ Dependencies installed
2410
2040
  `));
2411
2041
  } else {
2412
- process.stdout.write(chalk8.yellow(`⚠ Could not install dependencies (analysis may be limited)
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(chalk8.gray(` ${err}`));
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(chalk8.yellow(`⚠ Could not install dependencies: ${msg.slice(0, 100)}
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(chalk8.gray(` ${err}`));
2053
+ log(chalk6.gray(` ${err}`));
2424
2054
  }
2425
2055
  }
2426
2056
  }
2427
2057
  let targetDir = tempDir;
2428
2058
  let packageName;
2429
- const mono = await detectMonorepo5(fileSystem);
2059
+ const mono = await detectMonorepo(fileSystem);
2430
2060
  if (mono.isMonorepo) {
2431
2061
  if (!options.package) {
2432
2062
  error("");
2433
- error(chalk8.red(`Monorepo detected with ${mono.packages.length} packages. Specify target with --package:`));
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 = findPackageByName5(mono.packages, options.package);
2069
+ const pkg = findPackageByName(mono.packages, options.package);
2440
2070
  if (!pkg) {
2441
2071
  error("");
2442
- error(chalk8.red(`Package "${options.package}" not found. Available packages:`));
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 = path9.join(tempDir, pkg.path);
2078
+ targetDir = path7.join(tempDir, pkg.path);
2449
2079
  packageName = pkg.name;
2450
- log(chalk8.gray(`Analyzing package: ${packageName}`));
2080
+ log(chalk6.gray(`Analyzing package: ${packageName}`));
2451
2081
  }
2452
- process.stdout.write(chalk8.cyan(`> Detecting entry point...
2082
+ process.stdout.write(chalk6.cyan(`> Detecting entry point...
2453
2083
  `));
2454
2084
  let entryPath;
2455
- const targetFs = mono.isMonorepo ? new NodeFileSystem5(targetDir) : fileSystem;
2085
+ const targetFs = mono.isMonorepo ? new NodeFileSystem3(targetDir) : fileSystem;
2456
2086
  let buildFailed = false;
2457
2087
  const runLlmFallback = async (reason) => {
2458
- process.stdout.write(chalk8.cyan(`> ${reason}, trying LLM fallback...
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(chalk8.gray(` Running: ${cmd}`));
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(chalk8.yellow(` ⚠ Build requires Rust toolchain (not available)`));
2104
+ log(chalk6.yellow(` ⚠ Build requires Rust toolchain (not available)`));
2475
2105
  } else if (msg.includes("rimraf") || msg.includes("command not found")) {
2476
- log(chalk8.yellow(` ⚠ Build failed: missing dependencies`));
2106
+ log(chalk6.yellow(` ⚠ Build failed: missing dependencies`));
2477
2107
  } else {
2478
- log(chalk8.yellow(` ⚠ Build failed: ${msg.slice(0, 80)}`));
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(chalk8.gray(` Note: ${plan.notes}`));
2114
+ log(chalk6.gray(` Note: ${plan.notes}`));
2485
2115
  }
2486
2116
  return plan.entryPoint;
2487
2117
  };
2488
2118
  try {
2489
- const entry = await detectEntryPoint5(targetFs);
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(chalk8.cyan(`> Detected .d.ts entry with WASM indicators...
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 = path9.join(targetDir, llmEntry);
2127
+ entryPath = path7.join(targetDir, llmEntry);
2498
2128
  if (buildFailed) {
2499
- process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (using pre-committed declarations)
2129
+ process.stdout.write(chalk6.green(`✓ Entry point: ${llmEntry} (using pre-committed declarations)
2500
2130
  `));
2501
- log(chalk8.gray(" Coverage may be limited - generated .d.ts files typically lack JSDoc"));
2131
+ log(chalk6.gray(" Coverage may be limited - generated .d.ts files typically lack JSDoc"));
2502
2132
  } else {
2503
- process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (from LLM fallback - WASM project)
2133
+ process.stdout.write(chalk6.green(`✓ Entry point: ${llmEntry} (from LLM fallback - WASM project)
2504
2134
  `));
2505
2135
  }
2506
2136
  } else {
2507
- entryPath = path9.join(targetDir, entry.path);
2508
- process.stdout.write(chalk8.green(`✓ Entry point: ${entry.path} (from ${entry.source})
2137
+ entryPath = path7.join(targetDir, entry.path);
2138
+ process.stdout.write(chalk6.green(`✓ Entry point: ${entry.path} (from ${entry.source})
2509
2139
  `));
2510
- log(chalk8.yellow(" ⚠ WASM project detected but no API key - analysis may be limited"));
2140
+ log(chalk6.yellow(" ⚠ WASM project detected but no API key - analysis may be limited"));
2511
2141
  }
2512
2142
  } else {
2513
- entryPath = path9.join(targetDir, entry.path);
2514
- process.stdout.write(chalk8.green(`✓ Entry point: ${entry.path} (from ${entry.source})
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 = path9.join(targetDir, llmEntry);
2521
- process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (from LLM fallback)
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(chalk8.red(`✗ Could not detect entry point (set OPENAI_API_KEY for smart fallback)
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(chalk8.cyan(`> Analyzing documentation coverage...
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(chalk8.green(`✓ Analysis complete
2166
+ process.stdout.write(chalk6.green(`✓ Analysis complete
2537
2167
  `));
2538
2168
  } catch (analysisError) {
2539
- process.stdout.write(chalk8.red(`✗ Analysis failed
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 = path9.resolve(process.cwd(), options.saveSpec);
2547
- fs8.writeFileSync(specPath, JSON.stringify(spec, null, 2));
2548
- log(chalk8.green(`✓ Saved spec to ${options.saveSpec}`));
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: coverageScore,
2573
- exportCount: spec.exports?.length ?? 0,
2574
- typeCount: spec.types?.length ?? 0,
2575
- driftCount: driftIssues.length,
2576
- undocumented,
2577
- drift: driftIssues
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(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
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
- const { spawn } = await import("node:child_process");
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(chalk8.gray(`Repo preserved at: ${tempDir}`));
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(chalk8.bold("DocCov Scan Results"));
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: ${chalk8.cyan(repoName)}`);
2606
- log(`Branch: ${chalk8.gray(result.ref)}`);
2214
+ log(`Repository: ${chalk6.cyan(repoName)}`);
2215
+ log(`Branch: ${chalk6.gray(result.ref)}`);
2607
2216
  log("");
2608
- const coverageColor = result.coverage >= 80 ? chalk8.green : result.coverage >= 50 ? chalk8.yellow : chalk8.red;
2609
- log(chalk8.bold("Coverage"));
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(chalk8.bold("Stats"));
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(chalk8.bold("Undocumented Exports"));
2228
+ log(chalk6.bold("Undocumented Exports"));
2620
2229
  for (const name of result.undocumented.slice(0, 10)) {
2621
- log(chalk8.yellow(` ! ${name}`));
2230
+ log(chalk6.yellow(` ! ${name}`));
2622
2231
  }
2623
2232
  if (result.undocumented.length > 10) {
2624
- log(chalk8.gray(` ... and ${result.undocumented.length - 10} more`));
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(chalk8.bold("Drift Issues"));
2238
+ log(chalk6.bold("Drift Issues"));
2630
2239
  for (const d of result.drift.slice(0, 5)) {
2631
- log(chalk8.red(` • ${d.export}: ${d.issue}`));
2240
+ log(chalk6.red(` • ${d.export}: ${d.issue}`));
2632
2241
  }
2633
2242
  if (result.drift.length > 5) {
2634
- log(chalk8.gray(` ... and ${result.drift.length - 5} more`));
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 = path11.dirname(__filename2);
2758
- var packageJson = JSON.parse(readFileSync5(path11.join(__dirname2, "../package.json"), "utf-8"));
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
  });