@doccov/cli 0.15.1 → 0.17.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
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/config/doccov-config.ts
4
- import { access } from "node:fs/promises";
4
+ import { access, readFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
+ import { parse as parseYaml } from "yaml";
7
8
 
8
9
  // src/config/schema.ts
9
10
  import { z } from "zod";
@@ -95,7 +96,9 @@ var DOCCOV_CONFIG_FILENAMES = [
95
96
  "doccov.config.cts",
96
97
  "doccov.config.js",
97
98
  "doccov.config.mjs",
98
- "doccov.config.cjs"
99
+ "doccov.config.cjs",
100
+ "doccov.yml",
101
+ "doccov.yaml"
99
102
  ];
100
103
  var fileExists = async (filePath) => {
101
104
  try {
@@ -122,6 +125,11 @@ var findConfigFile = async (cwd) => {
122
125
  }
123
126
  };
124
127
  var importConfigModule = async (absolutePath) => {
128
+ const ext = path.extname(absolutePath);
129
+ if (ext === ".yml" || ext === ".yaml") {
130
+ const content = await readFile(absolutePath, "utf-8");
131
+ return parseYaml(content);
132
+ }
125
133
  const fileUrl = pathToFileURL(absolutePath);
126
134
  fileUrl.searchParams.set("t", Date.now().toString());
127
135
  const module = await import(fileUrl.href);
@@ -557,6 +565,205 @@ function renderHtml(stats, options = {}) {
557
565
  </body>
558
566
  </html>`;
559
567
  }
568
+ // src/reports/pr-comment.ts
569
+ function renderPRComment(data, opts = {}) {
570
+ const { diff, headSpec } = data;
571
+ const limit = opts.limit ?? 10;
572
+ const lines = [];
573
+ const hasIssues = diff.newUndocumented.length > 0 || diff.driftIntroduced > 0 || diff.breaking.length > 0 || opts.minCoverage !== undefined && diff.newCoverage < opts.minCoverage;
574
+ const statusIcon = hasIssues ? diff.coverageDelta < 0 ? "❌" : "⚠️" : "✅";
575
+ lines.push(`## ${statusIcon} DocCov — Documentation Coverage`);
576
+ lines.push("");
577
+ const targetStr = opts.minCoverage !== undefined ? ` (target: ${opts.minCoverage}%) ${diff.newCoverage >= opts.minCoverage ? "✅" : "❌"}` : "";
578
+ lines.push(`**Patch coverage:** ${diff.newCoverage}%${targetStr}`);
579
+ if (diff.newUndocumented.length > 0) {
580
+ lines.push(`**New undocumented exports:** ${diff.newUndocumented.length}`);
581
+ }
582
+ if (diff.driftIntroduced > 0) {
583
+ lines.push(`**Doc drift issues:** ${diff.driftIntroduced}`);
584
+ }
585
+ if (diff.newUndocumented.length > 0) {
586
+ lines.push("");
587
+ lines.push("### Undocumented exports in this PR");
588
+ lines.push("");
589
+ renderUndocumentedExports(lines, diff.newUndocumented, headSpec, opts, limit);
590
+ }
591
+ if (diff.driftIntroduced > 0 && headSpec) {
592
+ lines.push("");
593
+ lines.push("### Doc drift detected");
594
+ lines.push("");
595
+ renderDriftIssues(lines, diff.newUndocumented, headSpec, opts, limit);
596
+ }
597
+ const fixGuidance = renderFixGuidance(diff);
598
+ if (fixGuidance) {
599
+ lines.push("");
600
+ lines.push("### How to fix");
601
+ lines.push("");
602
+ lines.push(fixGuidance);
603
+ }
604
+ lines.push("");
605
+ lines.push("<details>");
606
+ lines.push("<summary>View full report</summary>");
607
+ lines.push("");
608
+ renderDetailsTable(lines, diff);
609
+ lines.push("");
610
+ lines.push("</details>");
611
+ return lines.join(`
612
+ `);
613
+ }
614
+ function renderUndocumentedExports(lines, undocumented, headSpec, opts, limit) {
615
+ if (!headSpec) {
616
+ for (const name of undocumented.slice(0, limit)) {
617
+ lines.push(`- \`${name}\``);
618
+ }
619
+ if (undocumented.length > limit) {
620
+ lines.push(`- _...and ${undocumented.length - limit} more_`);
621
+ }
622
+ return;
623
+ }
624
+ const byFile = new Map;
625
+ const undocSet = new Set(undocumented);
626
+ for (const exp of headSpec.exports) {
627
+ if (undocSet.has(exp.name)) {
628
+ const file = exp.source?.file ?? "unknown";
629
+ const list = byFile.get(file) ?? [];
630
+ list.push(exp);
631
+ byFile.set(file, list);
632
+ }
633
+ }
634
+ let count = 0;
635
+ for (const [file, exports] of byFile) {
636
+ if (count >= limit)
637
+ break;
638
+ const fileLink = buildFileLink(file, opts);
639
+ lines.push(`\uD83D\uDCC1 ${fileLink}`);
640
+ for (const exp of exports) {
641
+ if (count >= limit) {
642
+ lines.push(`- _...and more_`);
643
+ break;
644
+ }
645
+ const sig = formatExportSignature(exp);
646
+ lines.push(`- \`${sig}\``);
647
+ const missing = getMissingSignals(exp);
648
+ if (missing.length > 0) {
649
+ lines.push(` - Missing: ${missing.join(", ")}`);
650
+ }
651
+ count++;
652
+ }
653
+ lines.push("");
654
+ }
655
+ if (undocumented.length > count) {
656
+ lines.push(`_...and ${undocumented.length - count} more undocumented exports_`);
657
+ }
658
+ }
659
+ function renderDriftIssues(lines, _undocumented, headSpec, opts, limit) {
660
+ const driftIssues = [];
661
+ for (const exp of headSpec.exports) {
662
+ const drifts = exp.docs?.drift;
663
+ if (drifts) {
664
+ for (const d of drifts) {
665
+ driftIssues.push({
666
+ exportName: exp.name,
667
+ file: exp.source?.file,
668
+ drift: d
669
+ });
670
+ }
671
+ }
672
+ }
673
+ if (driftIssues.length === 0) {
674
+ lines.push("_No specific drift details available_");
675
+ return;
676
+ }
677
+ for (const issue of driftIssues.slice(0, limit)) {
678
+ const fileRef = issue.file ? `\`${issue.file}\`` : "unknown file";
679
+ const fileLink = issue.file ? buildFileLink(issue.file, opts) : fileRef;
680
+ lines.push(`⚠️ ${fileLink}: \`${issue.exportName}\``);
681
+ lines.push(`- ${issue.drift.issue}`);
682
+ if (issue.drift.suggestion) {
683
+ lines.push(`- Fix: ${issue.drift.suggestion}`);
684
+ }
685
+ lines.push("");
686
+ }
687
+ if (driftIssues.length > limit) {
688
+ lines.push(`_...and ${driftIssues.length - limit} more drift issues_`);
689
+ }
690
+ }
691
+ function buildFileLink(file, opts) {
692
+ if (opts.repoUrl && opts.sha) {
693
+ const url = `${opts.repoUrl}/blob/${opts.sha}/${file}`;
694
+ return `[\`${file}\`](${url})`;
695
+ }
696
+ return `\`${file}\``;
697
+ }
698
+ function formatExportSignature(exp) {
699
+ const prefix = `export ${exp.kind === "type" ? "type" : exp.kind === "interface" ? "interface" : exp.kind === "class" ? "class" : "function"}`;
700
+ if (exp.kind === "function" && exp.signatures?.[0]) {
701
+ const sig = exp.signatures[0];
702
+ const params = sig.parameters?.map((p) => `${p.name}${p.required === false ? "?" : ""}`).join(", ") ?? "";
703
+ const ret = sig.returns?.tsType ?? "void";
704
+ return `${prefix} ${exp.name}(${params}): ${ret}`;
705
+ }
706
+ if (exp.kind === "type" || exp.kind === "interface") {
707
+ return `${prefix} ${exp.name}`;
708
+ }
709
+ if (exp.kind === "class") {
710
+ return `${prefix} ${exp.name}`;
711
+ }
712
+ return `export ${exp.kind} ${exp.name}`;
713
+ }
714
+ function getMissingSignals(exp) {
715
+ const missing = [];
716
+ if (!exp.description) {
717
+ missing.push("description");
718
+ }
719
+ if (exp.kind === "function" && exp.signatures?.[0]) {
720
+ const sig = exp.signatures[0];
721
+ const undocParams = sig.parameters?.filter((p) => !p.description) ?? [];
722
+ if (undocParams.length > 0) {
723
+ missing.push(`\`@param ${undocParams.map((p) => p.name).join(", ")}\``);
724
+ }
725
+ if (!sig.returns?.description && sig.returns?.tsType !== "void") {
726
+ missing.push("`@returns`");
727
+ }
728
+ }
729
+ return missing;
730
+ }
731
+ function renderFixGuidance(diff) {
732
+ const sections = [];
733
+ if (diff.newUndocumented.length > 0) {
734
+ sections.push(`**For undocumented exports:**
735
+ ` + "Add JSDoc/TSDoc blocks with description, `@param`, and `@returns` tags.");
736
+ }
737
+ if (diff.driftIntroduced > 0) {
738
+ sections.push(`**For doc drift:**
739
+ ` + "Update the code examples in your markdown files to match current signatures.");
740
+ }
741
+ if (diff.breaking.length > 0) {
742
+ sections.push(`**For breaking changes:**
743
+ ` + "Consider adding a migration guide or updating changelog.");
744
+ }
745
+ if (sections.length === 0) {
746
+ return "";
747
+ }
748
+ sections.push(`
749
+ Push your changes — DocCov re-checks automatically.`);
750
+ return sections.join(`
751
+
752
+ `);
753
+ }
754
+ function renderDetailsTable(lines, diff) {
755
+ const delta = (n) => n > 0 ? `+${n}` : n === 0 ? "0" : String(n);
756
+ lines.push("| Metric | Before | After | Delta |");
757
+ lines.push("|--------|--------|-------|-------|");
758
+ lines.push(`| Coverage | ${diff.oldCoverage}% | ${diff.newCoverage}% | ${delta(diff.coverageDelta)}% |`);
759
+ lines.push(`| Breaking changes | - | ${diff.breaking.length} | - |`);
760
+ lines.push(`| New exports | - | ${diff.nonBreaking.length} | - |`);
761
+ lines.push(`| Undocumented | - | ${diff.newUndocumented.length} | - |`);
762
+ if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
763
+ const driftDelta = diff.driftIntroduced - diff.driftResolved;
764
+ lines.push(`| Drift | - | - | ${delta(driftDelta)} |`);
765
+ }
766
+ }
560
767
  // src/reports/stats.ts
561
768
  import { isFixableDrift } from "@doccov/sdk";
562
769
  import {
@@ -1308,7 +1515,7 @@ function registerDiffCommand(program, dependencies = {}) {
1308
1515
  ...defaultDependencies2,
1309
1516
  ...dependencies
1310
1517
  };
1311
- program.command("diff [base] [head]").description("Compare two OpenPkg specs and detect breaking changes").option("--base <file>", 'Base spec file (the "before" state)').option("--head <file>", 'Head spec file (the "after" state)').option("--format <format>", "Output format: text, json, markdown, html, github", "text").option("--stdout", "Output to stdout instead of writing to .doccov/").option("-o, --output <file>", "Custom output path").option("--cwd <dir>", "Working directory", process.cwd()).option("--limit <n>", "Max items to show in terminal/reports", "10").option("--min-coverage <n>", "Minimum coverage % for HEAD spec (0-100)").option("--max-drift <n>", "Maximum drift % for HEAD spec (0-100)").option("--strict <preset>", "Fail on conditions: ci, release, quality").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--no-cache", "Bypass cache and force regeneration").action(async (baseArg, headArg, options) => {
1518
+ program.command("diff [base] [head]").description("Compare two OpenPkg specs and detect breaking changes").option("--base <file>", 'Base spec file (the "before" state)').option("--head <file>", 'Head spec file (the "after" state)').option("--format <format>", "Output format: text, json, markdown, html, github, pr-comment", "text").option("--stdout", "Output to stdout instead of writing to .doccov/").option("-o, --output <file>", "Custom output path").option("--cwd <dir>", "Working directory", process.cwd()).option("--limit <n>", "Max items to show in terminal/reports", "10").option("--repo-url <url>", "GitHub repo URL for file links (pr-comment format)").option("--sha <sha>", "Commit SHA for file links (pr-comment format)").option("--min-coverage <n>", "Minimum coverage % for HEAD spec (0-100)").option("--max-drift <n>", "Maximum drift % for HEAD spec (0-100)").option("--strict <preset>", "Fail on conditions: ci, release, quality").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--no-cache", "Bypass cache and force regeneration").action(async (baseArg, headArg, options) => {
1312
1519
  try {
1313
1520
  const baseFile = options.base ?? baseArg;
1314
1521
  const headFile = options.head ?? headArg;
@@ -1418,6 +1625,16 @@ function registerDiffCommand(program, dependencies = {}) {
1418
1625
  case "github":
1419
1626
  printGitHubAnnotations(diff, log);
1420
1627
  break;
1628
+ case "pr-comment": {
1629
+ const content = renderPRComment({ diff, baseName, headName, headSpec }, {
1630
+ repoUrl: options.repoUrl,
1631
+ sha: options.sha,
1632
+ minCoverage,
1633
+ limit
1634
+ });
1635
+ log(content);
1636
+ break;
1637
+ }
1421
1638
  }
1422
1639
  const failures = validateDiff(diff, headSpec, {
1423
1640
  minCoverage,
@@ -1669,11 +1886,11 @@ function registerInitCommand(program, dependencies = {}) {
1669
1886
  ...defaultDependencies3,
1670
1887
  ...dependencies
1671
1888
  };
1672
- program.command("init").description("Create a DocCov configuration file").option("--cwd <dir>", "Working directory", process.cwd()).option("--format <format>", "Config format: auto, mjs, js, cjs", "auto").action((options) => {
1889
+ program.command("init").description("Create a DocCov configuration file").option("--cwd <dir>", "Working directory", process.cwd()).option("--format <format>", "Config format: auto, mjs, js, cjs, yaml", "auto").action((options) => {
1673
1890
  const cwd = path6.resolve(options.cwd);
1674
1891
  const formatOption = String(options.format ?? "auto").toLowerCase();
1675
1892
  if (!isValidFormat(formatOption)) {
1676
- error(chalk6.red(`Invalid format "${formatOption}". Use auto, mjs, js, or cjs.`));
1893
+ error(chalk6.red(`Invalid format "${formatOption}". Use auto, mjs, js, cjs, or yaml.`));
1677
1894
  process.exitCode = 1;
1678
1895
  return;
1679
1896
  }
@@ -1688,7 +1905,7 @@ function registerInitCommand(program, dependencies = {}) {
1688
1905
  if (targetFormat === "js" && packageType !== "module") {
1689
1906
  log(chalk6.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
1690
1907
  }
1691
- const fileName = `doccov.config.${targetFormat}`;
1908
+ const fileName = targetFormat === "yaml" ? "doccov.yml" : `doccov.config.${targetFormat}`;
1692
1909
  const outputPath = path6.join(cwd, fileName);
1693
1910
  if (fileExists2(outputPath)) {
1694
1911
  error(chalk6.red(`Cannot create ${fileName}; file already exists.`));
@@ -1701,7 +1918,7 @@ function registerInitCommand(program, dependencies = {}) {
1701
1918
  });
1702
1919
  }
1703
1920
  var isValidFormat = (value) => {
1704
- return value === "auto" || value === "mjs" || value === "js" || value === "cjs";
1921
+ return ["auto", "mjs", "js", "cjs", "yaml"].includes(value);
1705
1922
  };
1706
1923
  var findExistingConfig = (cwd, fileExists2) => {
1707
1924
  let current = path6.resolve(cwd);
@@ -1753,12 +1970,34 @@ var findNearestPackageJson = (cwd, fileExists2) => {
1753
1970
  return null;
1754
1971
  };
1755
1972
  var resolveFormat = (format, packageType) => {
1973
+ if (format === "yaml")
1974
+ return "yaml";
1756
1975
  if (format === "auto") {
1757
1976
  return packageType === "module" ? "js" : "mjs";
1758
1977
  }
1759
1978
  return format;
1760
1979
  };
1761
1980
  var buildTemplate = (format) => {
1981
+ if (format === "yaml") {
1982
+ return `# doccov.yml
1983
+ # include:
1984
+ # - "MyClass"
1985
+ # - "myFunction"
1986
+ # exclude:
1987
+ # - "internal*"
1988
+
1989
+ check:
1990
+ # minCoverage: 80
1991
+ # maxDrift: 20
1992
+ # examples: typecheck
1993
+
1994
+ quality:
1995
+ rules:
1996
+ # has-description: warn
1997
+ # has-params: off
1998
+ # has-returns: off
1999
+ `;
2000
+ }
1762
2001
  const configBody = `{
1763
2002
  // Filter which exports to analyze
1764
2003
  // include: ['MyClass', 'myFunction'],
@@ -1813,7 +2052,7 @@ import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, resolveTarget as
1813
2052
  import { normalize, validateSpec } from "@openpkg-ts/spec";
1814
2053
  import chalk8 from "chalk";
1815
2054
  // package.json
1816
- var version = "0.15.0";
2055
+ var version = "0.16.0";
1817
2056
 
1818
2057
  // src/utils/filter-options.ts
1819
2058
  import { mergeFilters, parseListFlag } from "@doccov/sdk";
@@ -38,7 +38,7 @@ declare const docCovConfigSchema: z.ZodObject<{
38
38
  }>;
39
39
  type DocCovConfigInput = z.infer<typeof docCovConfigSchema>;
40
40
  type NormalizedDocCovConfig = DocCovConfig;
41
- declare const DOCCOV_CONFIG_FILENAMES: readonly ["doccov.config.ts", "doccov.config.mts", "doccov.config.cts", "doccov.config.js", "doccov.config.mjs", "doccov.config.cjs"];
41
+ declare const DOCCOV_CONFIG_FILENAMES: readonly ["doccov.config.ts", "doccov.config.mts", "doccov.config.cts", "doccov.config.js", "doccov.config.mjs", "doccov.config.cjs", "doccov.yml", "doccov.yaml"];
42
42
  interface LoadedDocCovConfig extends NormalizedDocCovConfig {
43
43
  filePath: string;
44
44
  }
@@ -1,7 +1,8 @@
1
1
  // src/config/doccov-config.ts
2
- import { access } from "node:fs/promises";
2
+ import { access, readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
+ import { parse as parseYaml } from "yaml";
5
6
 
6
7
  // src/config/schema.ts
7
8
  import { z } from "zod";
@@ -93,7 +94,9 @@ var DOCCOV_CONFIG_FILENAMES = [
93
94
  "doccov.config.cts",
94
95
  "doccov.config.js",
95
96
  "doccov.config.mjs",
96
- "doccov.config.cjs"
97
+ "doccov.config.cjs",
98
+ "doccov.yml",
99
+ "doccov.yaml"
97
100
  ];
98
101
  var fileExists = async (filePath) => {
99
102
  try {
@@ -120,6 +123,11 @@ var findConfigFile = async (cwd) => {
120
123
  }
121
124
  };
122
125
  var importConfigModule = async (absolutePath) => {
126
+ const ext = path.extname(absolutePath);
127
+ if (ext === ".yml" || ext === ".yaml") {
128
+ const content = await readFile(absolutePath, "utf-8");
129
+ return parseYaml(content);
130
+ }
123
131
  const fileUrl = pathToFileURL(absolutePath);
124
132
  fileUrl.searchParams.set("t", Date.now().toString());
125
133
  const module = await import(fileUrl.href);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/cli",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "description": "DocCov CLI - Documentation coverage and drift detection for TypeScript",
5
5
  "keywords": [
6
6
  "typescript",
@@ -56,6 +56,7 @@
56
56
  "commander": "^14.0.0",
57
57
  "glob": "^11.0.0",
58
58
  "simple-git": "^3.27.0",
59
+ "yaml": "^2.8.2",
59
60
  "zod": "^3.25.0"
60
61
  },
61
62
  "devDependencies": {