@doccov/cli 0.5.7 → 0.5.8
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 +529 -110
- package/dist/config/index.d.ts +29 -1
- package/dist/config/index.js +29 -2
- package/package.json +2 -2
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
|
|
|
@@ -161,13 +188,16 @@ import {
|
|
|
161
188
|
findPackageByName,
|
|
162
189
|
findJSDocLocation,
|
|
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,56 @@ 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({ exportName: exp.name, examples: exp.examples });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (allExamplesForTypecheck.length > 0) {
|
|
408
|
+
process.stdout.write(chalk.cyan(`> Type-checking examples...
|
|
409
|
+
`));
|
|
410
|
+
for (const { exportName, examples } of allExamplesForTypecheck) {
|
|
411
|
+
const result = typecheckExamples(examples, targetDir);
|
|
412
|
+
for (const err of result.errors) {
|
|
413
|
+
typecheckErrors.push({ exportName, error: err });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (typecheckErrors.length === 0) {
|
|
417
|
+
process.stdout.write(chalk.green(`✓ All examples type-check
|
|
418
|
+
`));
|
|
419
|
+
} else {
|
|
420
|
+
process.stdout.write(chalk.red(`✗ ${typecheckErrors.length} type error(s)
|
|
421
|
+
`));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
347
425
|
const runtimeDrifts = [];
|
|
348
|
-
if (options.
|
|
426
|
+
if (options.exec) {
|
|
349
427
|
const allExamples = [];
|
|
350
428
|
for (const entry2 of spec.exports ?? []) {
|
|
351
429
|
if (entry2.examples && entry2.examples.length > 0) {
|
|
@@ -454,7 +532,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
454
532
|
const missingExamples = options.requireExamples ? failingExports.filter((item) => item.missing?.includes("examples")) : [];
|
|
455
533
|
let driftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
|
|
456
534
|
const fixedDriftKeys = new Set;
|
|
457
|
-
if (
|
|
535
|
+
if (shouldFix && driftExports.length > 0) {
|
|
458
536
|
const allDrifts = collectDriftsFromExports(spec.exports ?? []);
|
|
459
537
|
const filteredDrifts = filterDriftsByType(allDrifts, options.only);
|
|
460
538
|
if (filteredDrifts.length === 0 && options.only) {
|
|
@@ -567,7 +645,9 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
567
645
|
const coverageFailed = coverageScore < minCoverage;
|
|
568
646
|
const hasMissingExamples = missingExamples.length > 0;
|
|
569
647
|
const hasDrift = !options.ignoreDrift && driftExports.length > 0;
|
|
570
|
-
|
|
648
|
+
const hasLintErrors = lintViolations.filter((v) => v.violation.severity === "error").length > 0;
|
|
649
|
+
const hasTypecheckErrors = typecheckErrors.length > 0;
|
|
650
|
+
if (!coverageFailed && !hasMissingExamples && !hasDrift && !hasLintErrors && !hasTypecheckErrors) {
|
|
571
651
|
log(chalk.green(`✓ Docs coverage ${coverageScore}% (min ${minCoverage}%)`));
|
|
572
652
|
if (failingExports.length > 0) {
|
|
573
653
|
log(chalk.gray("Some exports have partial docs:"));
|
|
@@ -594,6 +674,20 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
594
674
|
if (hasMissingExamples) {
|
|
595
675
|
error(chalk.red(`${missingExamples.length} export(s) missing examples (required via --require-examples)`));
|
|
596
676
|
}
|
|
677
|
+
if (hasLintErrors) {
|
|
678
|
+
error("");
|
|
679
|
+
error(chalk.bold("Lint errors:"));
|
|
680
|
+
for (const { exportName, violation } of lintViolations.filter((v) => v.violation.severity === "error").slice(0, 10)) {
|
|
681
|
+
error(chalk.red(` • ${exportName}: ${violation.message}`));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (hasTypecheckErrors) {
|
|
685
|
+
error("");
|
|
686
|
+
error(chalk.bold("Type errors in examples:"));
|
|
687
|
+
for (const { exportName, error: err } of typecheckErrors.slice(0, 10)) {
|
|
688
|
+
error(chalk.red(` • ${exportName} @example ${err.exampleIndex + 1}, line ${err.line}: ${err.message}`));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
597
691
|
if (failingExports.length > 0 || driftExports.length > 0) {
|
|
598
692
|
error("");
|
|
599
693
|
error(chalk.bold("Missing documentation details:"));
|
|
@@ -1594,17 +1688,225 @@ var buildTemplate = (format) => {
|
|
|
1594
1688
|
`);
|
|
1595
1689
|
};
|
|
1596
1690
|
|
|
1597
|
-
// src/commands/
|
|
1691
|
+
// src/commands/lint.ts
|
|
1598
1692
|
import * as fs5 from "node:fs";
|
|
1599
1693
|
import * as path6 from "node:path";
|
|
1600
1694
|
import {
|
|
1601
|
-
|
|
1695
|
+
applyEdits as applyEdits2,
|
|
1696
|
+
createSourceFile as createSourceFile2,
|
|
1602
1697
|
detectEntryPoint as detectEntryPoint3,
|
|
1603
1698
|
detectMonorepo as detectMonorepo3,
|
|
1699
|
+
DocCov as DocCov3,
|
|
1700
|
+
findJSDocLocation as findJSDocLocation2,
|
|
1604
1701
|
findPackageByName as findPackageByName3,
|
|
1605
|
-
|
|
1702
|
+
getDefaultConfig,
|
|
1703
|
+
getRule,
|
|
1704
|
+
lintExport as lintExport2,
|
|
1705
|
+
NodeFileSystem as NodeFileSystem3,
|
|
1706
|
+
serializeJSDoc as serializeJSDoc2
|
|
1606
1707
|
} from "@doccov/sdk";
|
|
1607
1708
|
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";
|
|
1608
1910
|
|
|
1609
1911
|
// src/reports/markdown.ts
|
|
1610
1912
|
function bar(pct, width = 10) {
|
|
@@ -1781,42 +2083,42 @@ function registerReportCommand(program) {
|
|
|
1781
2083
|
try {
|
|
1782
2084
|
let spec;
|
|
1783
2085
|
if (options.spec) {
|
|
1784
|
-
const specPath =
|
|
1785
|
-
spec = JSON.parse(
|
|
2086
|
+
const specPath = path7.resolve(options.cwd, options.spec);
|
|
2087
|
+
spec = JSON.parse(fs6.readFileSync(specPath, "utf-8"));
|
|
1786
2088
|
} else {
|
|
1787
2089
|
let targetDir = options.cwd;
|
|
1788
2090
|
let entryFile = entry;
|
|
1789
|
-
const fileSystem = new
|
|
2091
|
+
const fileSystem = new NodeFileSystem4(options.cwd);
|
|
1790
2092
|
if (options.package) {
|
|
1791
|
-
const mono = await
|
|
2093
|
+
const mono = await detectMonorepo4(fileSystem);
|
|
1792
2094
|
if (!mono.isMonorepo) {
|
|
1793
2095
|
throw new Error(`Not a monorepo. Remove --package flag for single-package repos.`);
|
|
1794
2096
|
}
|
|
1795
|
-
const pkg =
|
|
2097
|
+
const pkg = findPackageByName4(mono.packages, options.package);
|
|
1796
2098
|
if (!pkg) {
|
|
1797
2099
|
const available = mono.packages.map((p) => p.name).join(", ");
|
|
1798
2100
|
throw new Error(`Package "${options.package}" not found. Available: ${available}`);
|
|
1799
2101
|
}
|
|
1800
|
-
targetDir =
|
|
2102
|
+
targetDir = path7.join(options.cwd, pkg.path);
|
|
1801
2103
|
}
|
|
1802
2104
|
if (!entryFile) {
|
|
1803
|
-
const targetFs = new
|
|
1804
|
-
const detected = await
|
|
1805
|
-
entryFile =
|
|
2105
|
+
const targetFs = new NodeFileSystem4(targetDir);
|
|
2106
|
+
const detected = await detectEntryPoint4(targetFs);
|
|
2107
|
+
entryFile = path7.join(targetDir, detected.path);
|
|
1806
2108
|
} else {
|
|
1807
|
-
entryFile =
|
|
2109
|
+
entryFile = path7.resolve(targetDir, entryFile);
|
|
1808
2110
|
}
|
|
1809
|
-
process.stdout.write(
|
|
2111
|
+
process.stdout.write(chalk7.cyan(`> Analyzing...
|
|
1810
2112
|
`));
|
|
1811
2113
|
try {
|
|
1812
2114
|
const resolveExternalTypes = !options.skipResolve;
|
|
1813
|
-
const doccov = new
|
|
2115
|
+
const doccov = new DocCov4({ resolveExternalTypes });
|
|
1814
2116
|
const result = await doccov.analyzeFileWithDiagnostics(entryFile);
|
|
1815
|
-
process.stdout.write(
|
|
2117
|
+
process.stdout.write(chalk7.green(`✓ Analysis complete
|
|
1816
2118
|
`));
|
|
1817
2119
|
spec = result.spec;
|
|
1818
2120
|
} catch (analysisError) {
|
|
1819
|
-
process.stdout.write(
|
|
2121
|
+
process.stdout.write(chalk7.red(`✗ Analysis failed
|
|
1820
2122
|
`));
|
|
1821
2123
|
throw analysisError;
|
|
1822
2124
|
}
|
|
@@ -1833,35 +2135,35 @@ function registerReportCommand(program) {
|
|
|
1833
2135
|
output = renderMarkdown(stats, { limit });
|
|
1834
2136
|
}
|
|
1835
2137
|
if (options.out) {
|
|
1836
|
-
const outPath =
|
|
1837
|
-
|
|
1838
|
-
console.log(
|
|
2138
|
+
const outPath = path7.resolve(options.cwd, options.out);
|
|
2139
|
+
fs6.writeFileSync(outPath, output);
|
|
2140
|
+
console.log(chalk7.green(`Report written to ${outPath}`));
|
|
1839
2141
|
} else {
|
|
1840
2142
|
console.log(output);
|
|
1841
2143
|
}
|
|
1842
2144
|
} catch (err) {
|
|
1843
|
-
console.error(
|
|
2145
|
+
console.error(chalk7.red("Error:"), err instanceof Error ? err.message : err);
|
|
1844
2146
|
process.exitCode = 1;
|
|
1845
2147
|
}
|
|
1846
2148
|
});
|
|
1847
2149
|
}
|
|
1848
2150
|
|
|
1849
2151
|
// src/commands/scan.ts
|
|
1850
|
-
import * as
|
|
2152
|
+
import * as fs8 from "node:fs";
|
|
1851
2153
|
import * as os from "node:os";
|
|
1852
|
-
import * as
|
|
2154
|
+
import * as path9 from "node:path";
|
|
1853
2155
|
import {
|
|
1854
|
-
DocCov as
|
|
2156
|
+
DocCov as DocCov5,
|
|
1855
2157
|
detectBuildInfo,
|
|
1856
|
-
detectEntryPoint as
|
|
1857
|
-
detectMonorepo as
|
|
2158
|
+
detectEntryPoint as detectEntryPoint5,
|
|
2159
|
+
detectMonorepo as detectMonorepo5,
|
|
1858
2160
|
detectPackageManager,
|
|
1859
|
-
findPackageByName as
|
|
2161
|
+
findPackageByName as findPackageByName5,
|
|
1860
2162
|
formatPackageList,
|
|
1861
2163
|
getInstallCommand,
|
|
1862
|
-
NodeFileSystem as
|
|
2164
|
+
NodeFileSystem as NodeFileSystem5
|
|
1863
2165
|
} from "@doccov/sdk";
|
|
1864
|
-
import
|
|
2166
|
+
import chalk8 from "chalk";
|
|
1865
2167
|
import { simpleGit } from "simple-git";
|
|
1866
2168
|
|
|
1867
2169
|
// src/utils/github-url.ts
|
|
@@ -1895,8 +2197,8 @@ function buildDisplayUrl(parsed) {
|
|
|
1895
2197
|
}
|
|
1896
2198
|
|
|
1897
2199
|
// src/utils/llm-build-plan.ts
|
|
1898
|
-
import * as
|
|
1899
|
-
import * as
|
|
2200
|
+
import * as fs7 from "node:fs";
|
|
2201
|
+
import * as path8 from "node:path";
|
|
1900
2202
|
import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
|
|
1901
2203
|
import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
|
|
1902
2204
|
import { generateObject as generateObject3 } from "ai";
|
|
@@ -1932,10 +2234,10 @@ function getModel3() {
|
|
|
1932
2234
|
async function gatherContextFiles(repoDir) {
|
|
1933
2235
|
const sections = [];
|
|
1934
2236
|
for (const fileName of CONTEXT_FILES) {
|
|
1935
|
-
const filePath =
|
|
1936
|
-
if (
|
|
2237
|
+
const filePath = path8.join(repoDir, fileName);
|
|
2238
|
+
if (fs7.existsSync(filePath)) {
|
|
1937
2239
|
try {
|
|
1938
|
-
let content =
|
|
2240
|
+
let content = fs7.readFileSync(filePath, "utf-8");
|
|
1939
2241
|
if (content.length > MAX_FILE_CHARS) {
|
|
1940
2242
|
content = `${content.slice(0, MAX_FILE_CHARS)}
|
|
1941
2243
|
... (truncated)`;
|
|
@@ -1987,14 +2289,14 @@ async function generateBuildPlan(repoDir) {
|
|
|
1987
2289
|
}
|
|
1988
2290
|
|
|
1989
2291
|
// src/commands/scan.ts
|
|
1990
|
-
var
|
|
1991
|
-
createDocCov: (options) => new
|
|
2292
|
+
var defaultDependencies6 = {
|
|
2293
|
+
createDocCov: (options) => new DocCov5(options),
|
|
1992
2294
|
log: console.log,
|
|
1993
2295
|
error: console.error
|
|
1994
2296
|
};
|
|
1995
2297
|
function registerScanCommand(program, dependencies = {}) {
|
|
1996
2298
|
const { createDocCov, log, error } = {
|
|
1997
|
-
...
|
|
2299
|
+
...defaultDependencies6,
|
|
1998
2300
|
...dependencies
|
|
1999
2301
|
};
|
|
2000
2302
|
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 +2306,12 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2004
2306
|
const cloneUrl = buildCloneUrl(parsed);
|
|
2005
2307
|
const displayUrl = buildDisplayUrl(parsed);
|
|
2006
2308
|
log("");
|
|
2007
|
-
log(
|
|
2008
|
-
log(
|
|
2309
|
+
log(chalk8.bold(`Scanning ${displayUrl}`));
|
|
2310
|
+
log(chalk8.gray(`Branch/tag: ${parsed.ref}`));
|
|
2009
2311
|
log("");
|
|
2010
|
-
tempDir =
|
|
2011
|
-
|
|
2012
|
-
process.stdout.write(
|
|
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}...
|
|
2013
2315
|
`));
|
|
2014
2316
|
try {
|
|
2015
2317
|
const git = simpleGit({
|
|
@@ -2031,10 +2333,10 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2031
2333
|
} finally {
|
|
2032
2334
|
process.env = originalEnv;
|
|
2033
2335
|
}
|
|
2034
|
-
process.stdout.write(
|
|
2336
|
+
process.stdout.write(chalk8.green(`✓ Cloned ${parsed.owner}/${parsed.repo}
|
|
2035
2337
|
`));
|
|
2036
2338
|
} catch (cloneError) {
|
|
2037
|
-
process.stdout.write(
|
|
2339
|
+
process.stdout.write(chalk8.red(`✗ Failed to clone repository
|
|
2038
2340
|
`));
|
|
2039
2341
|
const message = cloneError instanceof Error ? cloneError.message : String(cloneError);
|
|
2040
2342
|
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 +2352,11 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2050
2352
|
}
|
|
2051
2353
|
throw new Error(`Clone failed: ${message}`);
|
|
2052
2354
|
}
|
|
2053
|
-
const fileSystem = new
|
|
2355
|
+
const fileSystem = new NodeFileSystem5(tempDir);
|
|
2054
2356
|
if (options.skipInstall) {
|
|
2055
|
-
log(
|
|
2357
|
+
log(chalk8.gray("Skipping dependency installation (--skip-install)"));
|
|
2056
2358
|
} else {
|
|
2057
|
-
process.stdout.write(
|
|
2359
|
+
process.stdout.write(chalk8.cyan(`> Installing dependencies...
|
|
2058
2360
|
`));
|
|
2059
2361
|
const installErrors = [];
|
|
2060
2362
|
try {
|
|
@@ -2104,56 +2406,56 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2104
2406
|
}
|
|
2105
2407
|
}
|
|
2106
2408
|
if (installed) {
|
|
2107
|
-
process.stdout.write(
|
|
2409
|
+
process.stdout.write(chalk8.green(`✓ Dependencies installed
|
|
2108
2410
|
`));
|
|
2109
2411
|
} else {
|
|
2110
|
-
process.stdout.write(
|
|
2412
|
+
process.stdout.write(chalk8.yellow(`⚠ Could not install dependencies (analysis may be limited)
|
|
2111
2413
|
`));
|
|
2112
2414
|
for (const err of installErrors) {
|
|
2113
|
-
log(
|
|
2415
|
+
log(chalk8.gray(` ${err}`));
|
|
2114
2416
|
}
|
|
2115
2417
|
}
|
|
2116
2418
|
} catch (outerError) {
|
|
2117
2419
|
const msg = outerError instanceof Error ? outerError.message : String(outerError);
|
|
2118
|
-
process.stdout.write(
|
|
2420
|
+
process.stdout.write(chalk8.yellow(`⚠ Could not install dependencies: ${msg.slice(0, 100)}
|
|
2119
2421
|
`));
|
|
2120
2422
|
for (const err of installErrors) {
|
|
2121
|
-
log(
|
|
2423
|
+
log(chalk8.gray(` ${err}`));
|
|
2122
2424
|
}
|
|
2123
2425
|
}
|
|
2124
2426
|
}
|
|
2125
2427
|
let targetDir = tempDir;
|
|
2126
2428
|
let packageName;
|
|
2127
|
-
const mono = await
|
|
2429
|
+
const mono = await detectMonorepo5(fileSystem);
|
|
2128
2430
|
if (mono.isMonorepo) {
|
|
2129
2431
|
if (!options.package) {
|
|
2130
2432
|
error("");
|
|
2131
|
-
error(
|
|
2433
|
+
error(chalk8.red(`Monorepo detected with ${mono.packages.length} packages. Specify target with --package:`));
|
|
2132
2434
|
error("");
|
|
2133
2435
|
error(formatPackageList(mono.packages));
|
|
2134
2436
|
error("");
|
|
2135
2437
|
throw new Error("Monorepo requires --package flag");
|
|
2136
2438
|
}
|
|
2137
|
-
const pkg =
|
|
2439
|
+
const pkg = findPackageByName5(mono.packages, options.package);
|
|
2138
2440
|
if (!pkg) {
|
|
2139
2441
|
error("");
|
|
2140
|
-
error(
|
|
2442
|
+
error(chalk8.red(`Package "${options.package}" not found. Available packages:`));
|
|
2141
2443
|
error("");
|
|
2142
2444
|
error(formatPackageList(mono.packages));
|
|
2143
2445
|
error("");
|
|
2144
2446
|
throw new Error(`Package not found: ${options.package}`);
|
|
2145
2447
|
}
|
|
2146
|
-
targetDir =
|
|
2448
|
+
targetDir = path9.join(tempDir, pkg.path);
|
|
2147
2449
|
packageName = pkg.name;
|
|
2148
|
-
log(
|
|
2450
|
+
log(chalk8.gray(`Analyzing package: ${packageName}`));
|
|
2149
2451
|
}
|
|
2150
|
-
process.stdout.write(
|
|
2452
|
+
process.stdout.write(chalk8.cyan(`> Detecting entry point...
|
|
2151
2453
|
`));
|
|
2152
2454
|
let entryPath;
|
|
2153
|
-
const targetFs = mono.isMonorepo ? new
|
|
2455
|
+
const targetFs = mono.isMonorepo ? new NodeFileSystem5(targetDir) : fileSystem;
|
|
2154
2456
|
let buildFailed = false;
|
|
2155
2457
|
const runLlmFallback = async (reason) => {
|
|
2156
|
-
process.stdout.write(
|
|
2458
|
+
process.stdout.write(chalk8.cyan(`> ${reason}, trying LLM fallback...
|
|
2157
2459
|
`));
|
|
2158
2460
|
const plan = await generateBuildPlan(targetDir);
|
|
2159
2461
|
if (!plan) {
|
|
@@ -2162,88 +2464,88 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2162
2464
|
if (plan.buildCommands.length > 0) {
|
|
2163
2465
|
const { execSync } = await import("node:child_process");
|
|
2164
2466
|
for (const cmd of plan.buildCommands) {
|
|
2165
|
-
log(
|
|
2467
|
+
log(chalk8.gray(` Running: ${cmd}`));
|
|
2166
2468
|
try {
|
|
2167
2469
|
execSync(cmd, { cwd: targetDir, stdio: "pipe", timeout: 300000 });
|
|
2168
2470
|
} catch (buildError) {
|
|
2169
2471
|
buildFailed = true;
|
|
2170
2472
|
const msg = buildError instanceof Error ? buildError.message : String(buildError);
|
|
2171
2473
|
if (msg.includes("rustc") || msg.includes("cargo") || msg.includes("wasm-pack")) {
|
|
2172
|
-
log(
|
|
2474
|
+
log(chalk8.yellow(` ⚠ Build requires Rust toolchain (not available)`));
|
|
2173
2475
|
} else if (msg.includes("rimraf") || msg.includes("command not found")) {
|
|
2174
|
-
log(
|
|
2476
|
+
log(chalk8.yellow(` ⚠ Build failed: missing dependencies`));
|
|
2175
2477
|
} else {
|
|
2176
|
-
log(
|
|
2478
|
+
log(chalk8.yellow(` ⚠ Build failed: ${msg.slice(0, 80)}`));
|
|
2177
2479
|
}
|
|
2178
2480
|
}
|
|
2179
2481
|
}
|
|
2180
2482
|
}
|
|
2181
2483
|
if (plan.notes) {
|
|
2182
|
-
log(
|
|
2484
|
+
log(chalk8.gray(` Note: ${plan.notes}`));
|
|
2183
2485
|
}
|
|
2184
2486
|
return plan.entryPoint;
|
|
2185
2487
|
};
|
|
2186
2488
|
try {
|
|
2187
|
-
const entry = await
|
|
2489
|
+
const entry = await detectEntryPoint5(targetFs);
|
|
2188
2490
|
const buildInfo = await detectBuildInfo(targetFs);
|
|
2189
2491
|
const needsBuildStep = entry.isDeclarationOnly && buildInfo.exoticIndicators.wasm;
|
|
2190
2492
|
if (needsBuildStep) {
|
|
2191
|
-
process.stdout.write(
|
|
2493
|
+
process.stdout.write(chalk8.cyan(`> Detected .d.ts entry with WASM indicators...
|
|
2192
2494
|
`));
|
|
2193
2495
|
const llmEntry = await runLlmFallback("WASM project detected");
|
|
2194
2496
|
if (llmEntry) {
|
|
2195
|
-
entryPath =
|
|
2497
|
+
entryPath = path9.join(targetDir, llmEntry);
|
|
2196
2498
|
if (buildFailed) {
|
|
2197
|
-
process.stdout.write(
|
|
2499
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (using pre-committed declarations)
|
|
2198
2500
|
`));
|
|
2199
|
-
log(
|
|
2501
|
+
log(chalk8.gray(" Coverage may be limited - generated .d.ts files typically lack JSDoc"));
|
|
2200
2502
|
} else {
|
|
2201
|
-
process.stdout.write(
|
|
2503
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (from LLM fallback - WASM project)
|
|
2202
2504
|
`));
|
|
2203
2505
|
}
|
|
2204
2506
|
} else {
|
|
2205
|
-
entryPath =
|
|
2206
|
-
process.stdout.write(
|
|
2507
|
+
entryPath = path9.join(targetDir, entry.path);
|
|
2508
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${entry.path} (from ${entry.source})
|
|
2207
2509
|
`));
|
|
2208
|
-
log(
|
|
2510
|
+
log(chalk8.yellow(" ⚠ WASM project detected but no API key - analysis may be limited"));
|
|
2209
2511
|
}
|
|
2210
2512
|
} else {
|
|
2211
|
-
entryPath =
|
|
2212
|
-
process.stdout.write(
|
|
2513
|
+
entryPath = path9.join(targetDir, entry.path);
|
|
2514
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${entry.path} (from ${entry.source})
|
|
2213
2515
|
`));
|
|
2214
2516
|
}
|
|
2215
2517
|
} catch (entryError) {
|
|
2216
2518
|
const llmEntry = await runLlmFallback("Heuristics failed");
|
|
2217
2519
|
if (llmEntry) {
|
|
2218
|
-
entryPath =
|
|
2219
|
-
process.stdout.write(
|
|
2520
|
+
entryPath = path9.join(targetDir, llmEntry);
|
|
2521
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (from LLM fallback)
|
|
2220
2522
|
`));
|
|
2221
2523
|
} else {
|
|
2222
|
-
process.stdout.write(
|
|
2524
|
+
process.stdout.write(chalk8.red(`✗ Could not detect entry point (set OPENAI_API_KEY for smart fallback)
|
|
2223
2525
|
`));
|
|
2224
2526
|
throw entryError;
|
|
2225
2527
|
}
|
|
2226
2528
|
}
|
|
2227
|
-
process.stdout.write(
|
|
2529
|
+
process.stdout.write(chalk8.cyan(`> Analyzing documentation coverage...
|
|
2228
2530
|
`));
|
|
2229
2531
|
let result;
|
|
2230
2532
|
try {
|
|
2231
2533
|
const resolveExternalTypes = !options.skipResolve;
|
|
2232
2534
|
const doccov = createDocCov({ resolveExternalTypes });
|
|
2233
2535
|
result = await doccov.analyzeFileWithDiagnostics(entryPath);
|
|
2234
|
-
process.stdout.write(
|
|
2536
|
+
process.stdout.write(chalk8.green(`✓ Analysis complete
|
|
2235
2537
|
`));
|
|
2236
2538
|
} catch (analysisError) {
|
|
2237
|
-
process.stdout.write(
|
|
2539
|
+
process.stdout.write(chalk8.red(`✗ Analysis failed
|
|
2238
2540
|
`));
|
|
2239
2541
|
throw analysisError;
|
|
2240
2542
|
}
|
|
2241
2543
|
const spec = result.spec;
|
|
2242
2544
|
const coverageScore = spec.docs?.coverageScore ?? 0;
|
|
2243
2545
|
if (options.saveSpec) {
|
|
2244
|
-
const specPath =
|
|
2245
|
-
|
|
2246
|
-
log(
|
|
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}`));
|
|
2247
2549
|
}
|
|
2248
2550
|
const undocumented = [];
|
|
2249
2551
|
const driftIssues = [];
|
|
@@ -2280,7 +2582,7 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2280
2582
|
printTextResult(scanResult, log);
|
|
2281
2583
|
}
|
|
2282
2584
|
} catch (commandError) {
|
|
2283
|
-
error(
|
|
2585
|
+
error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
2284
2586
|
process.exitCode = 1;
|
|
2285
2587
|
} finally {
|
|
2286
2588
|
if (tempDir && options.cleanup !== false) {
|
|
@@ -2290,63 +2592,180 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2290
2592
|
stdio: "ignore"
|
|
2291
2593
|
}).unref();
|
|
2292
2594
|
} else if (tempDir) {
|
|
2293
|
-
log(
|
|
2595
|
+
log(chalk8.gray(`Repo preserved at: ${tempDir}`));
|
|
2294
2596
|
}
|
|
2295
2597
|
}
|
|
2296
2598
|
});
|
|
2297
2599
|
}
|
|
2298
2600
|
function printTextResult(result, log) {
|
|
2299
2601
|
log("");
|
|
2300
|
-
log(
|
|
2602
|
+
log(chalk8.bold("DocCov Scan Results"));
|
|
2301
2603
|
log("─".repeat(40));
|
|
2302
2604
|
const repoName = result.packageName ? `${result.owner}/${result.repo} (${result.packageName})` : `${result.owner}/${result.repo}`;
|
|
2303
|
-
log(`Repository: ${
|
|
2304
|
-
log(`Branch: ${
|
|
2605
|
+
log(`Repository: ${chalk8.cyan(repoName)}`);
|
|
2606
|
+
log(`Branch: ${chalk8.gray(result.ref)}`);
|
|
2305
2607
|
log("");
|
|
2306
|
-
const coverageColor = result.coverage >= 80 ?
|
|
2307
|
-
log(
|
|
2608
|
+
const coverageColor = result.coverage >= 80 ? chalk8.green : result.coverage >= 50 ? chalk8.yellow : chalk8.red;
|
|
2609
|
+
log(chalk8.bold("Coverage"));
|
|
2308
2610
|
log(` ${coverageColor(`${result.coverage}%`)}`);
|
|
2309
2611
|
log("");
|
|
2310
|
-
log(
|
|
2612
|
+
log(chalk8.bold("Stats"));
|
|
2311
2613
|
log(` ${result.exportCount} exports`);
|
|
2312
2614
|
log(` ${result.typeCount} types`);
|
|
2313
2615
|
log(` ${result.undocumented.length} undocumented`);
|
|
2314
2616
|
log(` ${result.driftCount} drift issues`);
|
|
2315
2617
|
if (result.undocumented.length > 0) {
|
|
2316
2618
|
log("");
|
|
2317
|
-
log(
|
|
2619
|
+
log(chalk8.bold("Undocumented Exports"));
|
|
2318
2620
|
for (const name of result.undocumented.slice(0, 10)) {
|
|
2319
|
-
log(
|
|
2621
|
+
log(chalk8.yellow(` ! ${name}`));
|
|
2320
2622
|
}
|
|
2321
2623
|
if (result.undocumented.length > 10) {
|
|
2322
|
-
log(
|
|
2624
|
+
log(chalk8.gray(` ... and ${result.undocumented.length - 10} more`));
|
|
2323
2625
|
}
|
|
2324
2626
|
}
|
|
2325
2627
|
if (result.drift.length > 0) {
|
|
2326
2628
|
log("");
|
|
2327
|
-
log(
|
|
2629
|
+
log(chalk8.bold("Drift Issues"));
|
|
2328
2630
|
for (const d of result.drift.slice(0, 5)) {
|
|
2329
|
-
log(
|
|
2631
|
+
log(chalk8.red(` • ${d.export}: ${d.issue}`));
|
|
2330
2632
|
}
|
|
2331
2633
|
if (result.drift.length > 5) {
|
|
2332
|
-
log(
|
|
2634
|
+
log(chalk8.gray(` ... and ${result.drift.length - 5} more`));
|
|
2333
2635
|
}
|
|
2334
2636
|
}
|
|
2335
2637
|
log("");
|
|
2336
2638
|
}
|
|
2337
2639
|
|
|
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
|
+
|
|
2338
2755
|
// src/cli.ts
|
|
2339
2756
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2340
|
-
var __dirname2 =
|
|
2341
|
-
var packageJson = JSON.parse(readFileSync5(
|
|
2757
|
+
var __dirname2 = path11.dirname(__filename2);
|
|
2758
|
+
var packageJson = JSON.parse(readFileSync5(path11.join(__dirname2, "../package.json"), "utf-8"));
|
|
2342
2759
|
var program = new Command;
|
|
2343
2760
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
2344
2761
|
registerGenerateCommand(program);
|
|
2345
2762
|
registerCheckCommand(program);
|
|
2346
2763
|
registerDiffCommand(program);
|
|
2347
2764
|
registerInitCommand(program);
|
|
2765
|
+
registerLintCommand(program);
|
|
2348
2766
|
registerReportCommand(program);
|
|
2349
2767
|
registerScanCommand(program);
|
|
2768
|
+
registerTypecheckCommand(program);
|
|
2350
2769
|
program.command("*", { hidden: true }).action(() => {
|
|
2351
2770
|
program.outputHelp();
|
|
2352
2771
|
});
|
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.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "DocCov CLI - Documentation coverage and drift detection for TypeScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -49,7 +49,7 @@
|
|
|
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.5.
|
|
52
|
+
"@doccov/sdk": "^0.5.8",
|
|
53
53
|
"@openpkg-ts/spec": "^0.4.0",
|
|
54
54
|
"ai": "^4.0.0",
|
|
55
55
|
"chalk": "^5.4.1",
|