@doccov/cli 0.5.7 → 0.6.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 +535 -117
- package/dist/config/index.d.ts +29 -1
- package/dist/config/index.js +29 -2
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -33,11 +33,22 @@ var docsConfigSchema = z.object({
|
|
|
33
33
|
include: stringList.optional(),
|
|
34
34
|
exclude: stringList.optional()
|
|
35
35
|
});
|
|
36
|
+
var lintSeveritySchema = z.enum(["error", "warn", "off"]);
|
|
37
|
+
var checkConfigSchema = z.object({
|
|
38
|
+
lint: z.boolean().optional(),
|
|
39
|
+
typecheck: z.boolean().optional(),
|
|
40
|
+
exec: z.boolean().optional()
|
|
41
|
+
});
|
|
42
|
+
var lintConfigSchema = z.object({
|
|
43
|
+
rules: z.record(lintSeveritySchema).optional()
|
|
44
|
+
});
|
|
36
45
|
var docCovConfigSchema = z.object({
|
|
37
46
|
include: stringList.optional(),
|
|
38
47
|
exclude: stringList.optional(),
|
|
39
48
|
plugins: z.array(z.unknown()).optional(),
|
|
40
|
-
docs: docsConfigSchema.optional()
|
|
49
|
+
docs: docsConfigSchema.optional(),
|
|
50
|
+
check: checkConfigSchema.optional(),
|
|
51
|
+
lint: lintConfigSchema.optional()
|
|
41
52
|
});
|
|
42
53
|
var normalizeList = (value) => {
|
|
43
54
|
if (!value) {
|
|
@@ -61,11 +72,27 @@ var normalizeConfig = (input) => {
|
|
|
61
72
|
};
|
|
62
73
|
}
|
|
63
74
|
}
|
|
75
|
+
let check;
|
|
76
|
+
if (input.check) {
|
|
77
|
+
check = {
|
|
78
|
+
lint: input.check.lint,
|
|
79
|
+
typecheck: input.check.typecheck,
|
|
80
|
+
exec: input.check.exec
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
let lint;
|
|
84
|
+
if (input.lint) {
|
|
85
|
+
lint = {
|
|
86
|
+
rules: input.lint.rules
|
|
87
|
+
};
|
|
88
|
+
}
|
|
64
89
|
return {
|
|
65
90
|
include,
|
|
66
91
|
exclude,
|
|
67
92
|
plugins: input.plugins,
|
|
68
|
-
docs
|
|
93
|
+
docs,
|
|
94
|
+
check,
|
|
95
|
+
lint
|
|
69
96
|
};
|
|
70
97
|
};
|
|
71
98
|
|
|
@@ -142,7 +169,7 @@ ${formatIssues(issues)}`);
|
|
|
142
169
|
var defineConfig = (config) => config;
|
|
143
170
|
// src/cli.ts
|
|
144
171
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
145
|
-
import * as
|
|
172
|
+
import * as path11 from "node:path";
|
|
146
173
|
import { fileURLToPath } from "node:url";
|
|
147
174
|
import { Command } from "commander";
|
|
148
175
|
|
|
@@ -158,16 +185,19 @@ import {
|
|
|
158
185
|
detectExampleAssertionFailures,
|
|
159
186
|
detectExampleRuntimeErrors,
|
|
160
187
|
detectMonorepo,
|
|
161
|
-
findPackageByName,
|
|
162
188
|
findJSDocLocation,
|
|
189
|
+
findPackageByName,
|
|
163
190
|
generateFixesForExport,
|
|
191
|
+
getDefaultConfig as getLintDefaultConfig,
|
|
164
192
|
hasNonAssertionComments,
|
|
193
|
+
lintExport,
|
|
165
194
|
mergeFixes,
|
|
166
195
|
NodeFileSystem,
|
|
167
196
|
parseAssertions,
|
|
168
197
|
parseJSDocToPatch,
|
|
169
198
|
runExamplesWithPackage,
|
|
170
|
-
serializeJSDoc
|
|
199
|
+
serializeJSDoc,
|
|
200
|
+
typecheckExamples
|
|
171
201
|
} from "@doccov/sdk";
|
|
172
202
|
import chalk from "chalk";
|
|
173
203
|
|
|
@@ -274,7 +304,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
274
304
|
...defaultDependencies,
|
|
275
305
|
...dependencies
|
|
276
306
|
};
|
|
277
|
-
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("--
|
|
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) => {
|
|
278
308
|
try {
|
|
279
309
|
let targetDir = options.cwd;
|
|
280
310
|
let entryFile = entry;
|
|
@@ -344,8 +374,59 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
344
374
|
}
|
|
345
375
|
log("");
|
|
346
376
|
}
|
|
377
|
+
const shouldFix = options.fix || options.write;
|
|
378
|
+
const lintViolations = [];
|
|
379
|
+
if (options.lint !== false) {
|
|
380
|
+
process.stdout.write(chalk.cyan(`> Running lint checks...
|
|
381
|
+
`));
|
|
382
|
+
const lintConfig = getLintDefaultConfig();
|
|
383
|
+
for (const exp of spec.exports ?? []) {
|
|
384
|
+
const violations = lintExport(exp, undefined, lintConfig);
|
|
385
|
+
for (const violation of violations) {
|
|
386
|
+
lintViolations.push({ exportName: exp.name, violation });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (lintViolations.length === 0) {
|
|
390
|
+
process.stdout.write(chalk.green(`✓ No lint issues
|
|
391
|
+
`));
|
|
392
|
+
} else {
|
|
393
|
+
const errors = lintViolations.filter((v) => v.violation.severity === "error").length;
|
|
394
|
+
const warns = lintViolations.filter((v) => v.violation.severity === "warn").length;
|
|
395
|
+
process.stdout.write(chalk.yellow(`⚠ ${lintViolations.length} lint issue(s) (${errors} error, ${warns} warn)
|
|
396
|
+
`));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const typecheckErrors = [];
|
|
400
|
+
if (options.typecheck !== false) {
|
|
401
|
+
const allExamplesForTypecheck = [];
|
|
402
|
+
for (const exp of spec.exports ?? []) {
|
|
403
|
+
if (exp.examples && exp.examples.length > 0) {
|
|
404
|
+
allExamplesForTypecheck.push({
|
|
405
|
+
exportName: exp.name,
|
|
406
|
+
examples: exp.examples
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (allExamplesForTypecheck.length > 0) {
|
|
411
|
+
process.stdout.write(chalk.cyan(`> Type-checking examples...
|
|
412
|
+
`));
|
|
413
|
+
for (const { exportName, examples } of allExamplesForTypecheck) {
|
|
414
|
+
const result = typecheckExamples(examples, targetDir);
|
|
415
|
+
for (const err of result.errors) {
|
|
416
|
+
typecheckErrors.push({ exportName, error: err });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (typecheckErrors.length === 0) {
|
|
420
|
+
process.stdout.write(chalk.green(`✓ All examples type-check
|
|
421
|
+
`));
|
|
422
|
+
} else {
|
|
423
|
+
process.stdout.write(chalk.red(`✗ ${typecheckErrors.length} type error(s)
|
|
424
|
+
`));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
347
428
|
const runtimeDrifts = [];
|
|
348
|
-
if (options.
|
|
429
|
+
if (options.exec) {
|
|
349
430
|
const allExamples = [];
|
|
350
431
|
for (const entry2 of spec.exports ?? []) {
|
|
351
432
|
if (entry2.examples && entry2.examples.length > 0) {
|
|
@@ -454,7 +535,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
454
535
|
const missingExamples = options.requireExamples ? failingExports.filter((item) => item.missing?.includes("examples")) : [];
|
|
455
536
|
let driftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
|
|
456
537
|
const fixedDriftKeys = new Set;
|
|
457
|
-
if (
|
|
538
|
+
if (shouldFix && driftExports.length > 0) {
|
|
458
539
|
const allDrifts = collectDriftsFromExports(spec.exports ?? []);
|
|
459
540
|
const filteredDrifts = filterDriftsByType(allDrifts, options.only);
|
|
460
541
|
if (filteredDrifts.length === 0 && options.only) {
|
|
@@ -567,7 +648,9 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
567
648
|
const coverageFailed = coverageScore < minCoverage;
|
|
568
649
|
const hasMissingExamples = missingExamples.length > 0;
|
|
569
650
|
const hasDrift = !options.ignoreDrift && driftExports.length > 0;
|
|
570
|
-
|
|
651
|
+
const hasLintErrors = lintViolations.filter((v) => v.violation.severity === "error").length > 0;
|
|
652
|
+
const hasTypecheckErrors = typecheckErrors.length > 0;
|
|
653
|
+
if (!coverageFailed && !hasMissingExamples && !hasDrift && !hasLintErrors && !hasTypecheckErrors) {
|
|
571
654
|
log(chalk.green(`✓ Docs coverage ${coverageScore}% (min ${minCoverage}%)`));
|
|
572
655
|
if (failingExports.length > 0) {
|
|
573
656
|
log(chalk.gray("Some exports have partial docs:"));
|
|
@@ -594,6 +677,20 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
594
677
|
if (hasMissingExamples) {
|
|
595
678
|
error(chalk.red(`${missingExamples.length} export(s) missing examples (required via --require-examples)`));
|
|
596
679
|
}
|
|
680
|
+
if (hasLintErrors) {
|
|
681
|
+
error("");
|
|
682
|
+
error(chalk.bold("Lint errors:"));
|
|
683
|
+
for (const { exportName, violation } of lintViolations.filter((v) => v.violation.severity === "error").slice(0, 10)) {
|
|
684
|
+
error(chalk.red(` • ${exportName}: ${violation.message}`));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (hasTypecheckErrors) {
|
|
688
|
+
error("");
|
|
689
|
+
error(chalk.bold("Type errors in examples:"));
|
|
690
|
+
for (const { exportName, error: err } of typecheckErrors.slice(0, 10)) {
|
|
691
|
+
error(chalk.red(` • ${exportName} @example ${err.exampleIndex + 1}, line ${err.line}: ${err.message}`));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
597
694
|
if (failingExports.length > 0 || driftExports.length > 0) {
|
|
598
695
|
error("");
|
|
599
696
|
error(chalk.bold("Missing documentation details:"));
|
|
@@ -795,7 +892,6 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
795
892
|
case "report":
|
|
796
893
|
log(generateHTMLReport(diff));
|
|
797
894
|
break;
|
|
798
|
-
case "text":
|
|
799
895
|
default:
|
|
800
896
|
printTextDiff(diff, log, error);
|
|
801
897
|
if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
|
|
@@ -933,7 +1029,7 @@ function printAPIChanges(diff, log) {
|
|
|
933
1029
|
}
|
|
934
1030
|
const added = changes.filter((c) => c.changeType === "added");
|
|
935
1031
|
if (added.length > 0) {
|
|
936
|
-
const addedNames = added.map((a) => a.memberName
|
|
1032
|
+
const addedNames = added.map((a) => `${a.memberName}()`).join(", ");
|
|
937
1033
|
log(chalk2.green(` + ${addedNames}`));
|
|
938
1034
|
}
|
|
939
1035
|
}
|
|
@@ -1594,17 +1690,225 @@ var buildTemplate = (format) => {
|
|
|
1594
1690
|
`);
|
|
1595
1691
|
};
|
|
1596
1692
|
|
|
1597
|
-
// src/commands/
|
|
1693
|
+
// src/commands/lint.ts
|
|
1598
1694
|
import * as fs5 from "node:fs";
|
|
1599
1695
|
import * as path6 from "node:path";
|
|
1600
1696
|
import {
|
|
1697
|
+
applyEdits as applyEdits2,
|
|
1698
|
+
createSourceFile as createSourceFile2,
|
|
1601
1699
|
DocCov as DocCov3,
|
|
1602
1700
|
detectEntryPoint as detectEntryPoint3,
|
|
1603
1701
|
detectMonorepo as detectMonorepo3,
|
|
1702
|
+
findJSDocLocation as findJSDocLocation2,
|
|
1604
1703
|
findPackageByName as findPackageByName3,
|
|
1605
|
-
|
|
1704
|
+
getDefaultConfig,
|
|
1705
|
+
getRule,
|
|
1706
|
+
lintExport as lintExport2,
|
|
1707
|
+
NodeFileSystem as NodeFileSystem3,
|
|
1708
|
+
serializeJSDoc as serializeJSDoc2
|
|
1606
1709
|
} from "@doccov/sdk";
|
|
1607
1710
|
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";
|
|
1608
1912
|
|
|
1609
1913
|
// src/reports/markdown.ts
|
|
1610
1914
|
function bar(pct, width = 10) {
|
|
@@ -1781,42 +2085,42 @@ function registerReportCommand(program) {
|
|
|
1781
2085
|
try {
|
|
1782
2086
|
let spec;
|
|
1783
2087
|
if (options.spec) {
|
|
1784
|
-
const specPath =
|
|
1785
|
-
spec = JSON.parse(
|
|
2088
|
+
const specPath = path7.resolve(options.cwd, options.spec);
|
|
2089
|
+
spec = JSON.parse(fs6.readFileSync(specPath, "utf-8"));
|
|
1786
2090
|
} else {
|
|
1787
2091
|
let targetDir = options.cwd;
|
|
1788
2092
|
let entryFile = entry;
|
|
1789
|
-
const fileSystem = new
|
|
2093
|
+
const fileSystem = new NodeFileSystem4(options.cwd);
|
|
1790
2094
|
if (options.package) {
|
|
1791
|
-
const mono = await
|
|
2095
|
+
const mono = await detectMonorepo4(fileSystem);
|
|
1792
2096
|
if (!mono.isMonorepo) {
|
|
1793
2097
|
throw new Error(`Not a monorepo. Remove --package flag for single-package repos.`);
|
|
1794
2098
|
}
|
|
1795
|
-
const pkg =
|
|
2099
|
+
const pkg = findPackageByName4(mono.packages, options.package);
|
|
1796
2100
|
if (!pkg) {
|
|
1797
2101
|
const available = mono.packages.map((p) => p.name).join(", ");
|
|
1798
2102
|
throw new Error(`Package "${options.package}" not found. Available: ${available}`);
|
|
1799
2103
|
}
|
|
1800
|
-
targetDir =
|
|
2104
|
+
targetDir = path7.join(options.cwd, pkg.path);
|
|
1801
2105
|
}
|
|
1802
2106
|
if (!entryFile) {
|
|
1803
|
-
const targetFs = new
|
|
1804
|
-
const detected = await
|
|
1805
|
-
entryFile =
|
|
2107
|
+
const targetFs = new NodeFileSystem4(targetDir);
|
|
2108
|
+
const detected = await detectEntryPoint4(targetFs);
|
|
2109
|
+
entryFile = path7.join(targetDir, detected.path);
|
|
1806
2110
|
} else {
|
|
1807
|
-
entryFile =
|
|
2111
|
+
entryFile = path7.resolve(targetDir, entryFile);
|
|
1808
2112
|
}
|
|
1809
|
-
process.stdout.write(
|
|
2113
|
+
process.stdout.write(chalk7.cyan(`> Analyzing...
|
|
1810
2114
|
`));
|
|
1811
2115
|
try {
|
|
1812
2116
|
const resolveExternalTypes = !options.skipResolve;
|
|
1813
|
-
const doccov = new
|
|
2117
|
+
const doccov = new DocCov4({ resolveExternalTypes });
|
|
1814
2118
|
const result = await doccov.analyzeFileWithDiagnostics(entryFile);
|
|
1815
|
-
process.stdout.write(
|
|
2119
|
+
process.stdout.write(chalk7.green(`✓ Analysis complete
|
|
1816
2120
|
`));
|
|
1817
2121
|
spec = result.spec;
|
|
1818
2122
|
} catch (analysisError) {
|
|
1819
|
-
process.stdout.write(
|
|
2123
|
+
process.stdout.write(chalk7.red(`✗ Analysis failed
|
|
1820
2124
|
`));
|
|
1821
2125
|
throw analysisError;
|
|
1822
2126
|
}
|
|
@@ -1833,35 +2137,36 @@ function registerReportCommand(program) {
|
|
|
1833
2137
|
output = renderMarkdown(stats, { limit });
|
|
1834
2138
|
}
|
|
1835
2139
|
if (options.out) {
|
|
1836
|
-
const outPath =
|
|
1837
|
-
|
|
1838
|
-
console.log(
|
|
2140
|
+
const outPath = path7.resolve(options.cwd, options.out);
|
|
2141
|
+
fs6.writeFileSync(outPath, output);
|
|
2142
|
+
console.log(chalk7.green(`Report written to ${outPath}`));
|
|
1839
2143
|
} else {
|
|
1840
2144
|
console.log(output);
|
|
1841
2145
|
}
|
|
1842
2146
|
} catch (err) {
|
|
1843
|
-
console.error(
|
|
2147
|
+
console.error(chalk7.red("Error:"), err instanceof Error ? err.message : err);
|
|
1844
2148
|
process.exitCode = 1;
|
|
1845
2149
|
}
|
|
1846
2150
|
});
|
|
1847
2151
|
}
|
|
1848
2152
|
|
|
1849
2153
|
// src/commands/scan.ts
|
|
1850
|
-
import * as
|
|
2154
|
+
import * as fs8 from "node:fs";
|
|
2155
|
+
import * as fsPromises from "node:fs/promises";
|
|
1851
2156
|
import * as os from "node:os";
|
|
1852
|
-
import * as
|
|
2157
|
+
import * as path9 from "node:path";
|
|
1853
2158
|
import {
|
|
1854
|
-
DocCov as
|
|
2159
|
+
DocCov as DocCov5,
|
|
1855
2160
|
detectBuildInfo,
|
|
1856
|
-
detectEntryPoint as
|
|
1857
|
-
detectMonorepo as
|
|
2161
|
+
detectEntryPoint as detectEntryPoint5,
|
|
2162
|
+
detectMonorepo as detectMonorepo5,
|
|
1858
2163
|
detectPackageManager,
|
|
1859
|
-
findPackageByName as
|
|
2164
|
+
findPackageByName as findPackageByName5,
|
|
1860
2165
|
formatPackageList,
|
|
1861
2166
|
getInstallCommand,
|
|
1862
|
-
NodeFileSystem as
|
|
2167
|
+
NodeFileSystem as NodeFileSystem5
|
|
1863
2168
|
} from "@doccov/sdk";
|
|
1864
|
-
import
|
|
2169
|
+
import chalk8 from "chalk";
|
|
1865
2170
|
import { simpleGit } from "simple-git";
|
|
1866
2171
|
|
|
1867
2172
|
// src/utils/github-url.ts
|
|
@@ -1895,8 +2200,8 @@ function buildDisplayUrl(parsed) {
|
|
|
1895
2200
|
}
|
|
1896
2201
|
|
|
1897
2202
|
// src/utils/llm-build-plan.ts
|
|
1898
|
-
import * as
|
|
1899
|
-
import * as
|
|
2203
|
+
import * as fs7 from "node:fs";
|
|
2204
|
+
import * as path8 from "node:path";
|
|
1900
2205
|
import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
|
|
1901
2206
|
import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
|
|
1902
2207
|
import { generateObject as generateObject3 } from "ai";
|
|
@@ -1932,10 +2237,10 @@ function getModel3() {
|
|
|
1932
2237
|
async function gatherContextFiles(repoDir) {
|
|
1933
2238
|
const sections = [];
|
|
1934
2239
|
for (const fileName of CONTEXT_FILES) {
|
|
1935
|
-
const filePath =
|
|
1936
|
-
if (
|
|
2240
|
+
const filePath = path8.join(repoDir, fileName);
|
|
2241
|
+
if (fs7.existsSync(filePath)) {
|
|
1937
2242
|
try {
|
|
1938
|
-
let content =
|
|
2243
|
+
let content = fs7.readFileSync(filePath, "utf-8");
|
|
1939
2244
|
if (content.length > MAX_FILE_CHARS) {
|
|
1940
2245
|
content = `${content.slice(0, MAX_FILE_CHARS)}
|
|
1941
2246
|
... (truncated)`;
|
|
@@ -1987,14 +2292,14 @@ async function generateBuildPlan(repoDir) {
|
|
|
1987
2292
|
}
|
|
1988
2293
|
|
|
1989
2294
|
// src/commands/scan.ts
|
|
1990
|
-
var
|
|
1991
|
-
createDocCov: (options) => new
|
|
2295
|
+
var defaultDependencies6 = {
|
|
2296
|
+
createDocCov: (options) => new DocCov5(options),
|
|
1992
2297
|
log: console.log,
|
|
1993
2298
|
error: console.error
|
|
1994
2299
|
};
|
|
1995
2300
|
function registerScanCommand(program, dependencies = {}) {
|
|
1996
2301
|
const { createDocCov, log, error } = {
|
|
1997
|
-
...
|
|
2302
|
+
...defaultDependencies6,
|
|
1998
2303
|
...dependencies
|
|
1999
2304
|
};
|
|
2000
2305
|
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) => {
|
|
@@ -2004,12 +2309,12 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2004
2309
|
const cloneUrl = buildCloneUrl(parsed);
|
|
2005
2310
|
const displayUrl = buildDisplayUrl(parsed);
|
|
2006
2311
|
log("");
|
|
2007
|
-
log(
|
|
2008
|
-
log(
|
|
2312
|
+
log(chalk8.bold(`Scanning ${displayUrl}`));
|
|
2313
|
+
log(chalk8.gray(`Branch/tag: ${parsed.ref}`));
|
|
2009
2314
|
log("");
|
|
2010
|
-
tempDir =
|
|
2011
|
-
|
|
2012
|
-
process.stdout.write(
|
|
2315
|
+
tempDir = path9.join(os.tmpdir(), `doccov-scan-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
2316
|
+
fs8.mkdirSync(tempDir, { recursive: true });
|
|
2317
|
+
process.stdout.write(chalk8.cyan(`> Cloning ${parsed.owner}/${parsed.repo}...
|
|
2013
2318
|
`));
|
|
2014
2319
|
try {
|
|
2015
2320
|
const git = simpleGit({
|
|
@@ -2031,10 +2336,10 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2031
2336
|
} finally {
|
|
2032
2337
|
process.env = originalEnv;
|
|
2033
2338
|
}
|
|
2034
|
-
process.stdout.write(
|
|
2339
|
+
process.stdout.write(chalk8.green(`✓ Cloned ${parsed.owner}/${parsed.repo}
|
|
2035
2340
|
`));
|
|
2036
2341
|
} catch (cloneError) {
|
|
2037
|
-
process.stdout.write(
|
|
2342
|
+
process.stdout.write(chalk8.red(`✗ Failed to clone repository
|
|
2038
2343
|
`));
|
|
2039
2344
|
const message = cloneError instanceof Error ? cloneError.message : String(cloneError);
|
|
2040
2345
|
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")) {
|
|
@@ -2050,11 +2355,11 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2050
2355
|
}
|
|
2051
2356
|
throw new Error(`Clone failed: ${message}`);
|
|
2052
2357
|
}
|
|
2053
|
-
const fileSystem = new
|
|
2358
|
+
const fileSystem = new NodeFileSystem5(tempDir);
|
|
2054
2359
|
if (options.skipInstall) {
|
|
2055
|
-
log(
|
|
2360
|
+
log(chalk8.gray("Skipping dependency installation (--skip-install)"));
|
|
2056
2361
|
} else {
|
|
2057
|
-
process.stdout.write(
|
|
2362
|
+
process.stdout.write(chalk8.cyan(`> Installing dependencies...
|
|
2058
2363
|
`));
|
|
2059
2364
|
const installErrors = [];
|
|
2060
2365
|
try {
|
|
@@ -2104,56 +2409,56 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2104
2409
|
}
|
|
2105
2410
|
}
|
|
2106
2411
|
if (installed) {
|
|
2107
|
-
process.stdout.write(
|
|
2412
|
+
process.stdout.write(chalk8.green(`✓ Dependencies installed
|
|
2108
2413
|
`));
|
|
2109
2414
|
} else {
|
|
2110
|
-
process.stdout.write(
|
|
2415
|
+
process.stdout.write(chalk8.yellow(`⚠ Could not install dependencies (analysis may be limited)
|
|
2111
2416
|
`));
|
|
2112
2417
|
for (const err of installErrors) {
|
|
2113
|
-
log(
|
|
2418
|
+
log(chalk8.gray(` ${err}`));
|
|
2114
2419
|
}
|
|
2115
2420
|
}
|
|
2116
2421
|
} catch (outerError) {
|
|
2117
2422
|
const msg = outerError instanceof Error ? outerError.message : String(outerError);
|
|
2118
|
-
process.stdout.write(
|
|
2423
|
+
process.stdout.write(chalk8.yellow(`⚠ Could not install dependencies: ${msg.slice(0, 100)}
|
|
2119
2424
|
`));
|
|
2120
2425
|
for (const err of installErrors) {
|
|
2121
|
-
log(
|
|
2426
|
+
log(chalk8.gray(` ${err}`));
|
|
2122
2427
|
}
|
|
2123
2428
|
}
|
|
2124
2429
|
}
|
|
2125
2430
|
let targetDir = tempDir;
|
|
2126
2431
|
let packageName;
|
|
2127
|
-
const mono = await
|
|
2432
|
+
const mono = await detectMonorepo5(fileSystem);
|
|
2128
2433
|
if (mono.isMonorepo) {
|
|
2129
2434
|
if (!options.package) {
|
|
2130
2435
|
error("");
|
|
2131
|
-
error(
|
|
2436
|
+
error(chalk8.red(`Monorepo detected with ${mono.packages.length} packages. Specify target with --package:`));
|
|
2132
2437
|
error("");
|
|
2133
2438
|
error(formatPackageList(mono.packages));
|
|
2134
2439
|
error("");
|
|
2135
2440
|
throw new Error("Monorepo requires --package flag");
|
|
2136
2441
|
}
|
|
2137
|
-
const pkg =
|
|
2442
|
+
const pkg = findPackageByName5(mono.packages, options.package);
|
|
2138
2443
|
if (!pkg) {
|
|
2139
2444
|
error("");
|
|
2140
|
-
error(
|
|
2445
|
+
error(chalk8.red(`Package "${options.package}" not found. Available packages:`));
|
|
2141
2446
|
error("");
|
|
2142
2447
|
error(formatPackageList(mono.packages));
|
|
2143
2448
|
error("");
|
|
2144
2449
|
throw new Error(`Package not found: ${options.package}`);
|
|
2145
2450
|
}
|
|
2146
|
-
targetDir =
|
|
2451
|
+
targetDir = path9.join(tempDir, pkg.path);
|
|
2147
2452
|
packageName = pkg.name;
|
|
2148
|
-
log(
|
|
2453
|
+
log(chalk8.gray(`Analyzing package: ${packageName}`));
|
|
2149
2454
|
}
|
|
2150
|
-
process.stdout.write(
|
|
2455
|
+
process.stdout.write(chalk8.cyan(`> Detecting entry point...
|
|
2151
2456
|
`));
|
|
2152
2457
|
let entryPath;
|
|
2153
|
-
const targetFs = mono.isMonorepo ? new
|
|
2458
|
+
const targetFs = mono.isMonorepo ? new NodeFileSystem5(targetDir) : fileSystem;
|
|
2154
2459
|
let buildFailed = false;
|
|
2155
2460
|
const runLlmFallback = async (reason) => {
|
|
2156
|
-
process.stdout.write(
|
|
2461
|
+
process.stdout.write(chalk8.cyan(`> ${reason}, trying LLM fallback...
|
|
2157
2462
|
`));
|
|
2158
2463
|
const plan = await generateBuildPlan(targetDir);
|
|
2159
2464
|
if (!plan) {
|
|
@@ -2162,88 +2467,88 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2162
2467
|
if (plan.buildCommands.length > 0) {
|
|
2163
2468
|
const { execSync } = await import("node:child_process");
|
|
2164
2469
|
for (const cmd of plan.buildCommands) {
|
|
2165
|
-
log(
|
|
2470
|
+
log(chalk8.gray(` Running: ${cmd}`));
|
|
2166
2471
|
try {
|
|
2167
2472
|
execSync(cmd, { cwd: targetDir, stdio: "pipe", timeout: 300000 });
|
|
2168
2473
|
} catch (buildError) {
|
|
2169
2474
|
buildFailed = true;
|
|
2170
2475
|
const msg = buildError instanceof Error ? buildError.message : String(buildError);
|
|
2171
2476
|
if (msg.includes("rustc") || msg.includes("cargo") || msg.includes("wasm-pack")) {
|
|
2172
|
-
log(
|
|
2477
|
+
log(chalk8.yellow(` ⚠ Build requires Rust toolchain (not available)`));
|
|
2173
2478
|
} else if (msg.includes("rimraf") || msg.includes("command not found")) {
|
|
2174
|
-
log(
|
|
2479
|
+
log(chalk8.yellow(` ⚠ Build failed: missing dependencies`));
|
|
2175
2480
|
} else {
|
|
2176
|
-
log(
|
|
2481
|
+
log(chalk8.yellow(` ⚠ Build failed: ${msg.slice(0, 80)}`));
|
|
2177
2482
|
}
|
|
2178
2483
|
}
|
|
2179
2484
|
}
|
|
2180
2485
|
}
|
|
2181
2486
|
if (plan.notes) {
|
|
2182
|
-
log(
|
|
2487
|
+
log(chalk8.gray(` Note: ${plan.notes}`));
|
|
2183
2488
|
}
|
|
2184
2489
|
return plan.entryPoint;
|
|
2185
2490
|
};
|
|
2186
2491
|
try {
|
|
2187
|
-
const entry = await
|
|
2492
|
+
const entry = await detectEntryPoint5(targetFs);
|
|
2188
2493
|
const buildInfo = await detectBuildInfo(targetFs);
|
|
2189
2494
|
const needsBuildStep = entry.isDeclarationOnly && buildInfo.exoticIndicators.wasm;
|
|
2190
2495
|
if (needsBuildStep) {
|
|
2191
|
-
process.stdout.write(
|
|
2496
|
+
process.stdout.write(chalk8.cyan(`> Detected .d.ts entry with WASM indicators...
|
|
2192
2497
|
`));
|
|
2193
2498
|
const llmEntry = await runLlmFallback("WASM project detected");
|
|
2194
2499
|
if (llmEntry) {
|
|
2195
|
-
entryPath =
|
|
2500
|
+
entryPath = path9.join(targetDir, llmEntry);
|
|
2196
2501
|
if (buildFailed) {
|
|
2197
|
-
process.stdout.write(
|
|
2502
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (using pre-committed declarations)
|
|
2198
2503
|
`));
|
|
2199
|
-
log(
|
|
2504
|
+
log(chalk8.gray(" Coverage may be limited - generated .d.ts files typically lack JSDoc"));
|
|
2200
2505
|
} else {
|
|
2201
|
-
process.stdout.write(
|
|
2506
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (from LLM fallback - WASM project)
|
|
2202
2507
|
`));
|
|
2203
2508
|
}
|
|
2204
2509
|
} else {
|
|
2205
|
-
entryPath =
|
|
2206
|
-
process.stdout.write(
|
|
2510
|
+
entryPath = path9.join(targetDir, entry.path);
|
|
2511
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${entry.path} (from ${entry.source})
|
|
2207
2512
|
`));
|
|
2208
|
-
log(
|
|
2513
|
+
log(chalk8.yellow(" ⚠ WASM project detected but no API key - analysis may be limited"));
|
|
2209
2514
|
}
|
|
2210
2515
|
} else {
|
|
2211
|
-
entryPath =
|
|
2212
|
-
process.stdout.write(
|
|
2516
|
+
entryPath = path9.join(targetDir, entry.path);
|
|
2517
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${entry.path} (from ${entry.source})
|
|
2213
2518
|
`));
|
|
2214
2519
|
}
|
|
2215
2520
|
} catch (entryError) {
|
|
2216
2521
|
const llmEntry = await runLlmFallback("Heuristics failed");
|
|
2217
2522
|
if (llmEntry) {
|
|
2218
|
-
entryPath =
|
|
2219
|
-
process.stdout.write(
|
|
2523
|
+
entryPath = path9.join(targetDir, llmEntry);
|
|
2524
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (from LLM fallback)
|
|
2220
2525
|
`));
|
|
2221
2526
|
} else {
|
|
2222
|
-
process.stdout.write(
|
|
2527
|
+
process.stdout.write(chalk8.red(`✗ Could not detect entry point (set OPENAI_API_KEY for smart fallback)
|
|
2223
2528
|
`));
|
|
2224
2529
|
throw entryError;
|
|
2225
2530
|
}
|
|
2226
2531
|
}
|
|
2227
|
-
process.stdout.write(
|
|
2532
|
+
process.stdout.write(chalk8.cyan(`> Analyzing documentation coverage...
|
|
2228
2533
|
`));
|
|
2229
2534
|
let result;
|
|
2230
2535
|
try {
|
|
2231
2536
|
const resolveExternalTypes = !options.skipResolve;
|
|
2232
2537
|
const doccov = createDocCov({ resolveExternalTypes });
|
|
2233
2538
|
result = await doccov.analyzeFileWithDiagnostics(entryPath);
|
|
2234
|
-
process.stdout.write(
|
|
2539
|
+
process.stdout.write(chalk8.green(`✓ Analysis complete
|
|
2235
2540
|
`));
|
|
2236
2541
|
} catch (analysisError) {
|
|
2237
|
-
process.stdout.write(
|
|
2542
|
+
process.stdout.write(chalk8.red(`✗ Analysis failed
|
|
2238
2543
|
`));
|
|
2239
2544
|
throw analysisError;
|
|
2240
2545
|
}
|
|
2241
2546
|
const spec = result.spec;
|
|
2242
2547
|
const coverageScore = spec.docs?.coverageScore ?? 0;
|
|
2243
2548
|
if (options.saveSpec) {
|
|
2244
|
-
const specPath =
|
|
2245
|
-
|
|
2246
|
-
log(
|
|
2549
|
+
const specPath = path9.resolve(process.cwd(), options.saveSpec);
|
|
2550
|
+
fs8.writeFileSync(specPath, JSON.stringify(spec, null, 2));
|
|
2551
|
+
log(chalk8.green(`✓ Saved spec to ${options.saveSpec}`));
|
|
2247
2552
|
}
|
|
2248
2553
|
const undocumented = [];
|
|
2249
2554
|
const driftIssues = [];
|
|
@@ -2280,73 +2585,186 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2280
2585
|
printTextResult(scanResult, log);
|
|
2281
2586
|
}
|
|
2282
2587
|
} catch (commandError) {
|
|
2283
|
-
error(
|
|
2588
|
+
error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
2284
2589
|
process.exitCode = 1;
|
|
2285
2590
|
} finally {
|
|
2286
2591
|
if (tempDir && options.cleanup !== false) {
|
|
2287
|
-
|
|
2288
|
-
spawn("rm", ["-rf", tempDir], {
|
|
2289
|
-
detached: true,
|
|
2290
|
-
stdio: "ignore"
|
|
2291
|
-
}).unref();
|
|
2592
|
+
fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
2292
2593
|
} else if (tempDir) {
|
|
2293
|
-
log(
|
|
2594
|
+
log(chalk8.gray(`Repo preserved at: ${tempDir}`));
|
|
2294
2595
|
}
|
|
2295
2596
|
}
|
|
2296
2597
|
});
|
|
2297
2598
|
}
|
|
2298
2599
|
function printTextResult(result, log) {
|
|
2299
2600
|
log("");
|
|
2300
|
-
log(
|
|
2601
|
+
log(chalk8.bold("DocCov Scan Results"));
|
|
2301
2602
|
log("─".repeat(40));
|
|
2302
2603
|
const repoName = result.packageName ? `${result.owner}/${result.repo} (${result.packageName})` : `${result.owner}/${result.repo}`;
|
|
2303
|
-
log(`Repository: ${
|
|
2304
|
-
log(`Branch: ${
|
|
2604
|
+
log(`Repository: ${chalk8.cyan(repoName)}`);
|
|
2605
|
+
log(`Branch: ${chalk8.gray(result.ref)}`);
|
|
2305
2606
|
log("");
|
|
2306
|
-
const coverageColor = result.coverage >= 80 ?
|
|
2307
|
-
log(
|
|
2607
|
+
const coverageColor = result.coverage >= 80 ? chalk8.green : result.coverage >= 50 ? chalk8.yellow : chalk8.red;
|
|
2608
|
+
log(chalk8.bold("Coverage"));
|
|
2308
2609
|
log(` ${coverageColor(`${result.coverage}%`)}`);
|
|
2309
2610
|
log("");
|
|
2310
|
-
log(
|
|
2611
|
+
log(chalk8.bold("Stats"));
|
|
2311
2612
|
log(` ${result.exportCount} exports`);
|
|
2312
2613
|
log(` ${result.typeCount} types`);
|
|
2313
2614
|
log(` ${result.undocumented.length} undocumented`);
|
|
2314
2615
|
log(` ${result.driftCount} drift issues`);
|
|
2315
2616
|
if (result.undocumented.length > 0) {
|
|
2316
2617
|
log("");
|
|
2317
|
-
log(
|
|
2618
|
+
log(chalk8.bold("Undocumented Exports"));
|
|
2318
2619
|
for (const name of result.undocumented.slice(0, 10)) {
|
|
2319
|
-
log(
|
|
2620
|
+
log(chalk8.yellow(` ! ${name}`));
|
|
2320
2621
|
}
|
|
2321
2622
|
if (result.undocumented.length > 10) {
|
|
2322
|
-
log(
|
|
2623
|
+
log(chalk8.gray(` ... and ${result.undocumented.length - 10} more`));
|
|
2323
2624
|
}
|
|
2324
2625
|
}
|
|
2325
2626
|
if (result.drift.length > 0) {
|
|
2326
2627
|
log("");
|
|
2327
|
-
log(
|
|
2628
|
+
log(chalk8.bold("Drift Issues"));
|
|
2328
2629
|
for (const d of result.drift.slice(0, 5)) {
|
|
2329
|
-
log(
|
|
2630
|
+
log(chalk8.red(` • ${d.export}: ${d.issue}`));
|
|
2330
2631
|
}
|
|
2331
2632
|
if (result.drift.length > 5) {
|
|
2332
|
-
log(
|
|
2633
|
+
log(chalk8.gray(` ... and ${result.drift.length - 5} more`));
|
|
2333
2634
|
}
|
|
2334
2635
|
}
|
|
2335
2636
|
log("");
|
|
2336
2637
|
}
|
|
2337
2638
|
|
|
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
|
+
|
|
2338
2754
|
// src/cli.ts
|
|
2339
2755
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2340
|
-
var __dirname2 =
|
|
2341
|
-
var packageJson = JSON.parse(readFileSync5(
|
|
2756
|
+
var __dirname2 = path11.dirname(__filename2);
|
|
2757
|
+
var packageJson = JSON.parse(readFileSync5(path11.join(__dirname2, "../package.json"), "utf-8"));
|
|
2342
2758
|
var program = new Command;
|
|
2343
2759
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
2344
2760
|
registerGenerateCommand(program);
|
|
2345
2761
|
registerCheckCommand(program);
|
|
2346
2762
|
registerDiffCommand(program);
|
|
2347
2763
|
registerInitCommand(program);
|
|
2764
|
+
registerLintCommand(program);
|
|
2348
2765
|
registerReportCommand(program);
|
|
2349
2766
|
registerScanCommand(program);
|
|
2767
|
+
registerTypecheckCommand(program);
|
|
2350
2768
|
program.command("*", { hidden: true }).action(() => {
|
|
2351
2769
|
program.outputHelp();
|
|
2352
2770
|
});
|
package/dist/config/index.d.ts
CHANGED
|
@@ -7,22 +7,50 @@ declare const docsConfigSchema: z.ZodObject<{
|
|
|
7
7
|
include: z.ZodOptional<typeof stringList>
|
|
8
8
|
exclude: z.ZodOptional<typeof stringList>
|
|
9
9
|
}>;
|
|
10
|
+
/** Lint severity levels */
|
|
11
|
+
declare const lintSeveritySchema: z.ZodEnum<["error", "warn", "off"]>;
|
|
12
|
+
/**
|
|
13
|
+
* Check command configuration schema
|
|
14
|
+
*/
|
|
15
|
+
declare const checkConfigSchema: z.ZodObject<{
|
|
16
|
+
lint: z.ZodOptional<z.ZodBoolean>
|
|
17
|
+
typecheck: z.ZodOptional<z.ZodBoolean>
|
|
18
|
+
exec: z.ZodOptional<z.ZodBoolean>
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Lint configuration schema
|
|
22
|
+
*/
|
|
23
|
+
declare const lintConfigSchema: z.ZodObject<{
|
|
24
|
+
rules: z.ZodOptional<z.ZodRecord<z.ZodString, typeof lintSeveritySchema>>
|
|
25
|
+
}>;
|
|
10
26
|
declare const docCovConfigSchema: z.ZodObject<{
|
|
11
27
|
include: z.ZodOptional<typeof stringList>
|
|
12
28
|
exclude: z.ZodOptional<typeof stringList>
|
|
13
29
|
plugins: z.ZodOptional<z.ZodArray<z.ZodUnknown>>
|
|
14
30
|
docs: z.ZodOptional<typeof docsConfigSchema>
|
|
31
|
+
check: z.ZodOptional<typeof checkConfigSchema>
|
|
32
|
+
lint: z.ZodOptional<typeof lintConfigSchema>
|
|
15
33
|
}>;
|
|
16
34
|
type DocCovConfigInput = z.infer<typeof docCovConfigSchema>;
|
|
17
35
|
interface DocsConfig {
|
|
18
36
|
include?: string[];
|
|
19
37
|
exclude?: string[];
|
|
20
38
|
}
|
|
39
|
+
interface CheckConfig {
|
|
40
|
+
lint?: boolean;
|
|
41
|
+
typecheck?: boolean;
|
|
42
|
+
exec?: boolean;
|
|
43
|
+
}
|
|
44
|
+
interface LintRulesConfig {
|
|
45
|
+
rules?: Record<string, "error" | "warn" | "off">;
|
|
46
|
+
}
|
|
21
47
|
interface NormalizedDocCovConfig {
|
|
22
48
|
include?: string[];
|
|
23
49
|
exclude?: string[];
|
|
24
50
|
plugins?: unknown[];
|
|
25
51
|
docs?: DocsConfig;
|
|
52
|
+
check?: CheckConfig;
|
|
53
|
+
lint?: LintRulesConfig;
|
|
26
54
|
}
|
|
27
55
|
declare const DOCCOV_CONFIG_FILENAMES: readonly ["doccov.config.ts", "doccov.config.mts", "doccov.config.cts", "doccov.config.js", "doccov.config.mjs", "doccov.config.cjs"];
|
|
28
56
|
interface LoadedDocCovConfig extends NormalizedDocCovConfig {
|
|
@@ -30,4 +58,4 @@ interface LoadedDocCovConfig extends NormalizedDocCovConfig {
|
|
|
30
58
|
}
|
|
31
59
|
declare const loadDocCovConfig: (cwd: string) => Promise<LoadedDocCovConfig | null>;
|
|
32
60
|
declare const defineConfig: (config: DocCovConfigInput) => DocCovConfigInput;
|
|
33
|
-
export { loadDocCovConfig, defineConfig, NormalizedDocCovConfig, LoadedDocCovConfig, DocsConfig, DocCovConfigInput, DOCCOV_CONFIG_FILENAMES };
|
|
61
|
+
export { loadDocCovConfig, defineConfig, NormalizedDocCovConfig, LoadedDocCovConfig, LintRulesConfig, DocsConfig, DocCovConfigInput, DOCCOV_CONFIG_FILENAMES, CheckConfig };
|
package/dist/config/index.js
CHANGED
|
@@ -32,11 +32,22 @@ var docsConfigSchema = z.object({
|
|
|
32
32
|
include: stringList.optional(),
|
|
33
33
|
exclude: stringList.optional()
|
|
34
34
|
});
|
|
35
|
+
var lintSeveritySchema = z.enum(["error", "warn", "off"]);
|
|
36
|
+
var checkConfigSchema = z.object({
|
|
37
|
+
lint: z.boolean().optional(),
|
|
38
|
+
typecheck: z.boolean().optional(),
|
|
39
|
+
exec: z.boolean().optional()
|
|
40
|
+
});
|
|
41
|
+
var lintConfigSchema = z.object({
|
|
42
|
+
rules: z.record(lintSeveritySchema).optional()
|
|
43
|
+
});
|
|
35
44
|
var docCovConfigSchema = z.object({
|
|
36
45
|
include: stringList.optional(),
|
|
37
46
|
exclude: stringList.optional(),
|
|
38
47
|
plugins: z.array(z.unknown()).optional(),
|
|
39
|
-
docs: docsConfigSchema.optional()
|
|
48
|
+
docs: docsConfigSchema.optional(),
|
|
49
|
+
check: checkConfigSchema.optional(),
|
|
50
|
+
lint: lintConfigSchema.optional()
|
|
40
51
|
});
|
|
41
52
|
var normalizeList = (value) => {
|
|
42
53
|
if (!value) {
|
|
@@ -60,11 +71,27 @@ var normalizeConfig = (input) => {
|
|
|
60
71
|
};
|
|
61
72
|
}
|
|
62
73
|
}
|
|
74
|
+
let check;
|
|
75
|
+
if (input.check) {
|
|
76
|
+
check = {
|
|
77
|
+
lint: input.check.lint,
|
|
78
|
+
typecheck: input.check.typecheck,
|
|
79
|
+
exec: input.check.exec
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
let lint;
|
|
83
|
+
if (input.lint) {
|
|
84
|
+
lint = {
|
|
85
|
+
rules: input.lint.rules
|
|
86
|
+
};
|
|
87
|
+
}
|
|
63
88
|
return {
|
|
64
89
|
include,
|
|
65
90
|
exclude,
|
|
66
91
|
plugins: input.plugins,
|
|
67
|
-
docs
|
|
92
|
+
docs,
|
|
93
|
+
check,
|
|
94
|
+
lint
|
|
68
95
|
};
|
|
69
96
|
};
|
|
70
97
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doccov/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "DocCov CLI - Documentation coverage and drift detection for TypeScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -49,14 +49,14 @@
|
|
|
49
49
|
"@ai-sdk/anthropic": "^1.0.0",
|
|
50
50
|
"@ai-sdk/openai": "^1.0.0",
|
|
51
51
|
"@inquirer/prompts": "^7.8.0",
|
|
52
|
-
"@doccov/sdk": "^0.
|
|
53
|
-
"@openpkg-ts/spec": "^0.4.
|
|
52
|
+
"@doccov/sdk": "^0.6.0",
|
|
53
|
+
"@openpkg-ts/spec": "^0.4.1",
|
|
54
54
|
"ai": "^4.0.0",
|
|
55
55
|
"chalk": "^5.4.1",
|
|
56
56
|
"commander": "^14.0.0",
|
|
57
57
|
"glob": "^11.0.0",
|
|
58
58
|
"simple-git": "^3.27.0",
|
|
59
|
-
"zod": "^3.
|
|
59
|
+
"zod": "^3.25.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/bun": "latest",
|