@doccov/cli 0.5.6 → 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 +946 -239
- package/dist/config/index.d.ts +29 -1
- package/dist/config/index.js +29 -2
- package/package.json +3 -3
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:"));
|
|
@@ -730,12 +824,37 @@ var defaultDependencies2 = {
|
|
|
730
824
|
log: console.log,
|
|
731
825
|
error: console.error
|
|
732
826
|
};
|
|
827
|
+
var VALID_STRICT_OPTIONS = [
|
|
828
|
+
"regression",
|
|
829
|
+
"drift",
|
|
830
|
+
"docs-impact",
|
|
831
|
+
"breaking",
|
|
832
|
+
"undocumented",
|
|
833
|
+
"all"
|
|
834
|
+
];
|
|
835
|
+
function parseStrictOptions(value) {
|
|
836
|
+
if (!value)
|
|
837
|
+
return new Set;
|
|
838
|
+
const options = value.split(",").map((s) => s.trim().toLowerCase());
|
|
839
|
+
const result = new Set;
|
|
840
|
+
for (const opt of options) {
|
|
841
|
+
if (opt === "all") {
|
|
842
|
+
for (const o of VALID_STRICT_OPTIONS) {
|
|
843
|
+
if (o !== "all")
|
|
844
|
+
result.add(o);
|
|
845
|
+
}
|
|
846
|
+
} else if (VALID_STRICT_OPTIONS.includes(opt)) {
|
|
847
|
+
result.add(opt);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return result;
|
|
851
|
+
}
|
|
733
852
|
function registerDiffCommand(program, dependencies = {}) {
|
|
734
853
|
const { readFileSync: readFileSync2, log, error } = {
|
|
735
854
|
...defaultDependencies2,
|
|
736
855
|
...dependencies
|
|
737
856
|
};
|
|
738
|
-
program.command("diff <base> <head>").description("Compare two OpenPkg specs and report coverage delta").option("--
|
|
857
|
+
program.command("diff <base> <head>").description("Compare two OpenPkg specs and report coverage delta").option("--format <format>", "Output format: text, json, github, report", "text").option("--strict <options>", "Fail on conditions (comma-separated): regression, drift, docs-impact, breaking, undocumented, all").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--output <format>", "DEPRECATED: Use --format instead").option("--fail-on-regression", "DEPRECATED: Use --strict regression").option("--fail-on-drift", "DEPRECATED: Use --strict drift").option("--fail-on-docs-impact", "DEPRECATED: Use --strict docs-impact").action(async (base, head, options) => {
|
|
739
858
|
try {
|
|
740
859
|
const baseSpec = loadSpec(base, readFileSync2);
|
|
741
860
|
const headSpec = loadSpec(head, readFileSync2);
|
|
@@ -752,52 +871,81 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
752
871
|
markdownFiles = await loadMarkdownFiles(docsPatterns);
|
|
753
872
|
}
|
|
754
873
|
const diff = diffSpecWithDocs(baseSpec, headSpec, { markdownFiles });
|
|
755
|
-
const format = options.output ?? "text";
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
874
|
+
const format = options.format ?? options.output ?? "text";
|
|
875
|
+
const strictOptions = parseStrictOptions(options.strict);
|
|
876
|
+
if (options.failOnRegression)
|
|
877
|
+
strictOptions.add("regression");
|
|
878
|
+
if (options.failOnDrift)
|
|
879
|
+
strictOptions.add("drift");
|
|
880
|
+
if (options.failOnDocsImpact)
|
|
881
|
+
strictOptions.add("docs-impact");
|
|
882
|
+
switch (format) {
|
|
883
|
+
case "json":
|
|
884
|
+
log(JSON.stringify(diff, null, 2));
|
|
885
|
+
break;
|
|
886
|
+
case "github":
|
|
887
|
+
printGitHubAnnotations(diff, log);
|
|
888
|
+
break;
|
|
889
|
+
case "report":
|
|
890
|
+
log(generateHTMLReport(diff));
|
|
891
|
+
break;
|
|
892
|
+
case "text":
|
|
893
|
+
default:
|
|
894
|
+
printTextDiff(diff, log, error);
|
|
895
|
+
if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
|
|
896
|
+
if (!isAIDocsAnalysisAvailable()) {
|
|
897
|
+
log(chalk2.yellow(`
|
|
763
898
|
⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
|
|
764
|
-
|
|
765
|
-
|
|
899
|
+
} else {
|
|
900
|
+
log(chalk2.gray(`
|
|
766
901
|
Generating AI summary...`));
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
902
|
+
const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
|
|
903
|
+
file: f.file,
|
|
904
|
+
exportName: r.exportName,
|
|
905
|
+
changeType: r.changeType,
|
|
906
|
+
context: r.context
|
|
907
|
+
})));
|
|
908
|
+
const summary = await generateImpactSummary(impacts);
|
|
909
|
+
if (summary) {
|
|
910
|
+
log("");
|
|
911
|
+
log(chalk2.bold("AI Summary"));
|
|
912
|
+
log(chalk2.cyan(` ${summary}`));
|
|
913
|
+
}
|
|
778
914
|
}
|
|
779
915
|
}
|
|
780
|
-
|
|
916
|
+
break;
|
|
781
917
|
}
|
|
782
|
-
if (
|
|
918
|
+
if (strictOptions.has("regression") && diff.coverageDelta < 0) {
|
|
783
919
|
error(chalk2.red(`
|
|
784
920
|
Coverage regressed by ${Math.abs(diff.coverageDelta)}%`));
|
|
785
921
|
process.exitCode = 1;
|
|
786
922
|
return;
|
|
787
923
|
}
|
|
788
|
-
if (
|
|
924
|
+
if (strictOptions.has("drift") && diff.driftIntroduced > 0) {
|
|
789
925
|
error(chalk2.red(`
|
|
790
926
|
${diff.driftIntroduced} new drift issue(s) introduced`));
|
|
791
927
|
process.exitCode = 1;
|
|
792
928
|
return;
|
|
793
929
|
}
|
|
794
|
-
if (
|
|
930
|
+
if (strictOptions.has("docs-impact") && hasDocsImpact(diff)) {
|
|
795
931
|
const summary = getDocsImpactSummary(diff);
|
|
796
932
|
error(chalk2.red(`
|
|
797
933
|
${summary.totalIssues} docs issue(s) require attention`));
|
|
798
934
|
process.exitCode = 1;
|
|
799
935
|
return;
|
|
800
936
|
}
|
|
937
|
+
if (strictOptions.has("breaking") && diff.breaking.length > 0) {
|
|
938
|
+
error(chalk2.red(`
|
|
939
|
+
${diff.breaking.length} breaking change(s) detected`));
|
|
940
|
+
process.exitCode = 1;
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (strictOptions.has("undocumented") && diff.newUndocumented.length > 0) {
|
|
944
|
+
error(chalk2.red(`
|
|
945
|
+
${diff.newUndocumented.length} new undocumented export(s)`));
|
|
946
|
+
process.exitCode = 1;
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
801
949
|
} catch (commandError) {
|
|
802
950
|
error(chalk2.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
803
951
|
process.exitCode = 1;
|
|
@@ -836,142 +984,376 @@ function printTextDiff(diff, log, _error) {
|
|
|
836
984
|
log("");
|
|
837
985
|
log(chalk2.bold("DocCov Diff Report"));
|
|
838
986
|
log("─".repeat(40));
|
|
987
|
+
printCoverage(diff, log);
|
|
988
|
+
printAPIChanges(diff, log);
|
|
989
|
+
if (diff.docsImpact) {
|
|
990
|
+
printDocsRequiringUpdates(diff, log);
|
|
991
|
+
}
|
|
992
|
+
log("");
|
|
993
|
+
}
|
|
994
|
+
function printCoverage(diff, log) {
|
|
839
995
|
const coverageColor = diff.coverageDelta > 0 ? chalk2.green : diff.coverageDelta < 0 ? chalk2.red : chalk2.gray;
|
|
840
996
|
const coverageSymbol = diff.coverageDelta > 0 ? "↑" : diff.coverageDelta < 0 ? "↓" : "→";
|
|
841
997
|
const deltaStr = diff.coverageDelta > 0 ? `+${diff.coverageDelta}` : String(diff.coverageDelta);
|
|
842
998
|
log("");
|
|
843
999
|
log(chalk2.bold("Coverage"));
|
|
844
1000
|
log(` ${diff.oldCoverage}% ${coverageSymbol} ${diff.newCoverage}% ${coverageColor(`(${deltaStr}%)`)}`);
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1001
|
+
}
|
|
1002
|
+
function printAPIChanges(diff, log) {
|
|
1003
|
+
const hasChanges = diff.breaking.length > 0 || diff.nonBreaking.length > 0 || diff.memberChanges && diff.memberChanges.length > 0;
|
|
1004
|
+
if (!hasChanges)
|
|
1005
|
+
return;
|
|
1006
|
+
log("");
|
|
1007
|
+
log(chalk2.bold("API Changes"));
|
|
1008
|
+
const membersByClass = groupMemberChangesByClass(diff.memberChanges ?? []);
|
|
1009
|
+
const classesWithMembers = new Set(membersByClass.keys());
|
|
1010
|
+
for (const [className, changes] of membersByClass) {
|
|
1011
|
+
const categorized = diff.categorizedBreaking?.find((c) => c.id === className);
|
|
1012
|
+
const isHighSeverity = categorized?.severity === "high";
|
|
1013
|
+
const label = isHighSeverity ? chalk2.red(" [BREAKING]") : chalk2.yellow(" [CHANGED]");
|
|
1014
|
+
log(chalk2.cyan(` ${className}`) + label);
|
|
1015
|
+
const removed = changes.filter((c) => c.changeType === "removed");
|
|
1016
|
+
for (const mc of removed) {
|
|
1017
|
+
const suggestion = mc.suggestion ? chalk2.gray(` → ${mc.suggestion}`) : "";
|
|
1018
|
+
log(chalk2.red(` ✖ ${mc.memberName}()`) + suggestion);
|
|
856
1019
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
log(chalk2.gray(` ... and ${diff.nonBreaking.length - 5} more`));
|
|
1020
|
+
const changed = changes.filter((c) => c.changeType === "signature-changed");
|
|
1021
|
+
for (const mc of changed) {
|
|
1022
|
+
log(chalk2.yellow(` ~ ${mc.memberName}() signature changed`));
|
|
1023
|
+
if (mc.oldSignature && mc.newSignature && mc.oldSignature !== mc.newSignature) {
|
|
1024
|
+
log(chalk2.gray(` was: ${mc.oldSignature}`));
|
|
1025
|
+
log(chalk2.gray(` now: ${mc.newSignature}`));
|
|
864
1026
|
}
|
|
865
1027
|
}
|
|
1028
|
+
const added = changes.filter((c) => c.changeType === "added");
|
|
1029
|
+
if (added.length > 0) {
|
|
1030
|
+
const addedNames = added.map((a) => a.memberName + "()").join(", ");
|
|
1031
|
+
log(chalk2.green(` + ${addedNames}`));
|
|
1032
|
+
}
|
|
866
1033
|
}
|
|
867
|
-
|
|
1034
|
+
const nonClassBreaking = (diff.categorizedBreaking ?? []).filter((c) => !classesWithMembers.has(c.id));
|
|
1035
|
+
const typeChanges = nonClassBreaking.filter((c) => c.kind === "interface" || c.kind === "type" || c.kind === "enum");
|
|
1036
|
+
const functionChanges = nonClassBreaking.filter((c) => c.kind === "function");
|
|
1037
|
+
const otherChanges = nonClassBreaking.filter((c) => !["interface", "type", "enum", "function"].includes(c.kind));
|
|
1038
|
+
if (functionChanges.length > 0) {
|
|
868
1039
|
log("");
|
|
869
|
-
log(chalk2.
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
list.push(mc);
|
|
874
|
-
byClass.set(mc.className, list);
|
|
1040
|
+
log(chalk2.red(` Function Changes (${functionChanges.length}):`));
|
|
1041
|
+
for (const fc of functionChanges.slice(0, 3)) {
|
|
1042
|
+
const reason = fc.reason === "removed" ? "removed" : "signature changed";
|
|
1043
|
+
log(chalk2.red(` ✖ ${fc.name} (${reason})`));
|
|
875
1044
|
}
|
|
876
|
-
|
|
877
|
-
log(chalk2.
|
|
878
|
-
const removed = changes.filter((c) => c.changeType === "removed");
|
|
879
|
-
const added = changes.filter((c) => c.changeType === "added");
|
|
880
|
-
const changed = changes.filter((c) => c.changeType === "signature-changed");
|
|
881
|
-
for (const mc of removed.slice(0, 3)) {
|
|
882
|
-
const suggestion = mc.suggestion ? ` (${mc.suggestion})` : "";
|
|
883
|
-
log(chalk2.red(` - ${mc.memberName}()${suggestion}`));
|
|
884
|
-
}
|
|
885
|
-
for (const mc of added.slice(0, 3)) {
|
|
886
|
-
log(chalk2.green(` + ${mc.memberName}()`));
|
|
887
|
-
}
|
|
888
|
-
for (const mc of changed.slice(0, 3)) {
|
|
889
|
-
log(chalk2.yellow(` ~ ${mc.memberName}() signature changed`));
|
|
890
|
-
}
|
|
891
|
-
const total = removed.length + added.length + changed.length;
|
|
892
|
-
const shown = Math.min(removed.length, 3) + Math.min(added.length, 3) + Math.min(changed.length, 3);
|
|
893
|
-
if (total > shown) {
|
|
894
|
-
log(chalk2.gray(` ... and ${total - shown} more member change(s)`));
|
|
895
|
-
}
|
|
1045
|
+
if (functionChanges.length > 3) {
|
|
1046
|
+
log(chalk2.gray(` ... and ${functionChanges.length - 3} more`));
|
|
896
1047
|
}
|
|
897
1048
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
log(chalk2.yellow(` ! ${id}`));
|
|
904
|
-
}
|
|
905
|
-
if (diff.newUndocumented.length > 5) {
|
|
906
|
-
log(chalk2.gray(` ... and ${diff.newUndocumented.length - 5} more`));
|
|
907
|
-
}
|
|
1049
|
+
if (typeChanges.length > 0) {
|
|
1050
|
+
log("");
|
|
1051
|
+
log(chalk2.yellow(` Type/Interface Changes (${typeChanges.length}):`));
|
|
1052
|
+
const typeNames = typeChanges.slice(0, 5).map((t) => t.name);
|
|
1053
|
+
log(chalk2.yellow(` ~ ${typeNames.join(", ")}${typeChanges.length > 5 ? ", ..." : ""}`));
|
|
908
1054
|
}
|
|
909
|
-
if (
|
|
910
|
-
log(
|
|
1055
|
+
if (otherChanges.length > 0) {
|
|
1056
|
+
log("");
|
|
1057
|
+
log(chalk2.gray(` Other Changes (${otherChanges.length}):`));
|
|
1058
|
+
const otherNames = otherChanges.slice(0, 3).map((o) => o.name);
|
|
1059
|
+
log(chalk2.gray(` ${otherNames.join(", ")}${otherChanges.length > 3 ? ", ..." : ""}`));
|
|
911
1060
|
}
|
|
912
|
-
if (diff.
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
1061
|
+
if (diff.nonBreaking.length > 0) {
|
|
1062
|
+
const undocCount = diff.newUndocumented.length;
|
|
1063
|
+
const undocSuffix = undocCount > 0 ? chalk2.yellow(` (${undocCount} undocumented)`) : "";
|
|
1064
|
+
log("");
|
|
1065
|
+
log(chalk2.green(` New Exports (${diff.nonBreaking.length})`) + undocSuffix);
|
|
1066
|
+
const exportNames = diff.nonBreaking.slice(0, 3);
|
|
1067
|
+
log(chalk2.green(` + ${exportNames.join(", ")}${diff.nonBreaking.length > 3 ? ", ..." : ""}`));
|
|
917
1068
|
}
|
|
918
1069
|
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
919
1070
|
log("");
|
|
920
|
-
|
|
1071
|
+
const parts = [];
|
|
921
1072
|
if (diff.driftIntroduced > 0) {
|
|
922
|
-
|
|
1073
|
+
parts.push(chalk2.red(`+${diff.driftIntroduced} drift`));
|
|
923
1074
|
}
|
|
924
1075
|
if (diff.driftResolved > 0) {
|
|
925
|
-
|
|
1076
|
+
parts.push(chalk2.green(`-${diff.driftResolved} resolved`));
|
|
926
1077
|
}
|
|
1078
|
+
log(` Drift: ${parts.join(", ")}`);
|
|
927
1079
|
}
|
|
928
|
-
|
|
1080
|
+
}
|
|
1081
|
+
function printDocsRequiringUpdates(diff, log) {
|
|
1082
|
+
if (!diff.docsImpact)
|
|
1083
|
+
return;
|
|
1084
|
+
const { impactedFiles, missingDocs, stats } = diff.docsImpact;
|
|
1085
|
+
log("");
|
|
1086
|
+
log(chalk2.bold("Docs Requiring Updates"));
|
|
1087
|
+
log(chalk2.gray(` Scanned ${stats.filesScanned} files, ${stats.codeBlocksFound} code blocks`));
|
|
1088
|
+
if (impactedFiles.length === 0 && missingDocs.length === 0) {
|
|
1089
|
+
log(chalk2.green(" ✓ No updates needed"));
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
const sortedFiles = [...impactedFiles].sort((a, b) => b.references.length - a.references.length);
|
|
1093
|
+
const actionableFiles = [];
|
|
1094
|
+
const instantiationOnlyFiles = [];
|
|
1095
|
+
for (const file of sortedFiles) {
|
|
1096
|
+
const hasActionableRefs = file.references.some((r) => r.memberName && !r.isInstantiation || !r.memberName && !r.isInstantiation);
|
|
1097
|
+
if (hasActionableRefs) {
|
|
1098
|
+
actionableFiles.push(file);
|
|
1099
|
+
} else {
|
|
1100
|
+
instantiationOnlyFiles.push(file);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
for (const file of actionableFiles.slice(0, 6)) {
|
|
1104
|
+
const filename = path3.basename(file.file);
|
|
1105
|
+
const issueCount = file.references.length;
|
|
929
1106
|
log("");
|
|
930
|
-
log(chalk2.
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1107
|
+
log(chalk2.yellow(` ${filename}`) + chalk2.gray(` (${issueCount} issue${issueCount > 1 ? "s" : ""})`));
|
|
1108
|
+
const actionableRefs = file.references.filter((r) => !r.isInstantiation);
|
|
1109
|
+
for (const ref of actionableRefs.slice(0, 4)) {
|
|
1110
|
+
if (ref.memberName) {
|
|
1111
|
+
const action = ref.changeType === "method-removed" ? "→" : "~";
|
|
1112
|
+
const hint = ref.replacementSuggestion ?? (ref.changeType === "method-changed" ? "signature changed" : "removed");
|
|
1113
|
+
log(chalk2.gray(` L${ref.line}: ${ref.memberName}() ${action} ${hint}`));
|
|
1114
|
+
} else {
|
|
1115
|
+
const action = ref.changeType === "removed" ? "→" : "~";
|
|
1116
|
+
const hint = ref.changeType === "removed" ? "removed" : ref.changeType === "signature-changed" ? "signature changed" : "changed";
|
|
1117
|
+
log(chalk2.gray(` L${ref.line}: ${ref.exportName} ${action} ${hint}`));
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (actionableRefs.length > 4) {
|
|
1121
|
+
log(chalk2.gray(` ... and ${actionableRefs.length - 4} more`));
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (actionableFiles.length > 6) {
|
|
1125
|
+
log(chalk2.gray(` ... and ${actionableFiles.length - 6} more files with method changes`));
|
|
1126
|
+
}
|
|
1127
|
+
if (instantiationOnlyFiles.length > 0) {
|
|
1128
|
+
log("");
|
|
1129
|
+
const fileNames = instantiationOnlyFiles.slice(0, 4).map((f) => path3.basename(f.file));
|
|
1130
|
+
const suffix = instantiationOnlyFiles.length > 4 ? ", ..." : "";
|
|
1131
|
+
log(chalk2.gray(` ${instantiationOnlyFiles.length} file(s) with class instantiation to review:`));
|
|
1132
|
+
log(chalk2.gray(` ${fileNames.join(", ")}${suffix}`));
|
|
1133
|
+
}
|
|
1134
|
+
const { allUndocumented } = diff.docsImpact;
|
|
1135
|
+
if (missingDocs.length > 0) {
|
|
1136
|
+
log("");
|
|
1137
|
+
log(chalk2.yellow(` New exports missing docs (${missingDocs.length}):`));
|
|
1138
|
+
const names = missingDocs.slice(0, 4);
|
|
1139
|
+
log(chalk2.gray(` ${names.join(", ")}${missingDocs.length > 4 ? ", ..." : ""}`));
|
|
1140
|
+
}
|
|
1141
|
+
if (allUndocumented && allUndocumented.length > 0) {
|
|
1142
|
+
const existingUndocumented = allUndocumented.filter((name) => !missingDocs.includes(name));
|
|
1143
|
+
log("");
|
|
1144
|
+
log(chalk2.gray(` Total undocumented exports: ${allUndocumented.length}/${stats.totalExports} (${Math.round((1 - allUndocumented.length / stats.totalExports) * 100)}% documented)`));
|
|
1145
|
+
if (existingUndocumented.length > 0 && existingUndocumented.length <= 10) {
|
|
1146
|
+
log(chalk2.gray(` ${existingUndocumented.slice(0, 6).join(", ")}${existingUndocumented.length > 6 ? ", ..." : ""}`));
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function groupMemberChangesByClass(memberChanges) {
|
|
1151
|
+
const byClass = new Map;
|
|
1152
|
+
for (const mc of memberChanges) {
|
|
1153
|
+
const list = byClass.get(mc.className) ?? [];
|
|
1154
|
+
list.push(mc);
|
|
1155
|
+
byClass.set(mc.className, list);
|
|
1156
|
+
}
|
|
1157
|
+
return byClass;
|
|
1158
|
+
}
|
|
1159
|
+
function printGitHubAnnotations(diff, log) {
|
|
1160
|
+
if (diff.coverageDelta !== 0) {
|
|
1161
|
+
const level = diff.coverageDelta < 0 ? "warning" : "notice";
|
|
1162
|
+
const sign = diff.coverageDelta > 0 ? "+" : "";
|
|
1163
|
+
log(`::${level} title=Coverage Change::Coverage ${diff.oldCoverage}% → ${diff.newCoverage}% (${sign}${diff.coverageDelta}%)`);
|
|
1164
|
+
}
|
|
1165
|
+
for (const breaking of diff.categorizedBreaking ?? []) {
|
|
1166
|
+
const level = breaking.severity === "high" ? "error" : "warning";
|
|
1167
|
+
log(`::${level} title=Breaking Change::${breaking.name} - ${breaking.reason}`);
|
|
1168
|
+
}
|
|
1169
|
+
for (const mc of diff.memberChanges ?? []) {
|
|
1170
|
+
if (mc.changeType === "removed") {
|
|
1171
|
+
const suggestion = mc.suggestion ? ` ${mc.suggestion}` : "";
|
|
1172
|
+
log(`::warning title=Method Removed::${mc.className}.${mc.memberName}() removed.${suggestion}`);
|
|
1173
|
+
} else if (mc.changeType === "signature-changed") {
|
|
1174
|
+
log(`::warning title=Signature Changed::${mc.className}.${mc.memberName}() signature changed`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (diff.docsImpact) {
|
|
1178
|
+
for (const file of diff.docsImpact.impactedFiles) {
|
|
1179
|
+
for (const ref of file.references) {
|
|
1180
|
+
const level = ref.changeType === "removed" || ref.changeType === "method-removed" ? "error" : "warning";
|
|
1181
|
+
const name = ref.memberName ? `${ref.memberName}()` : ref.exportName;
|
|
1182
|
+
const changeDesc = ref.changeType === "removed" || ref.changeType === "method-removed" ? "removed" : ref.changeType === "signature-changed" || ref.changeType === "method-changed" ? "signature changed" : "changed";
|
|
1183
|
+
const suggestion = ref.replacementSuggestion ? ` → ${ref.replacementSuggestion}` : "";
|
|
1184
|
+
log(`::${level} file=${file.file},line=${ref.line},title=API Change::${name} ${changeDesc}${suggestion}`);
|
|
955
1185
|
}
|
|
956
|
-
|
|
957
|
-
|
|
1186
|
+
}
|
|
1187
|
+
for (const name of diff.docsImpact.missingDocs) {
|
|
1188
|
+
log(`::notice title=Missing Documentation::New export ${name} needs documentation`);
|
|
1189
|
+
}
|
|
1190
|
+
const { stats, allUndocumented } = diff.docsImpact;
|
|
1191
|
+
if (allUndocumented && allUndocumented.length > 0) {
|
|
1192
|
+
const docPercent = Math.round((1 - allUndocumented.length / stats.totalExports) * 100);
|
|
1193
|
+
log(`::notice title=Documentation Coverage::${stats.documentedExports}/${stats.totalExports} exports documented (${docPercent}%)`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (!diff.docsImpact && diff.newUndocumented.length > 0) {
|
|
1197
|
+
for (const name of diff.newUndocumented) {
|
|
1198
|
+
log(`::notice title=Missing Documentation::New export ${name} needs documentation`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (diff.driftIntroduced > 0) {
|
|
1202
|
+
log(`::warning title=Drift Detected::${diff.driftIntroduced} new drift issue(s) introduced`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function generateHTMLReport(diff) {
|
|
1206
|
+
const coverageClass = diff.coverageDelta > 0 ? "positive" : diff.coverageDelta < 0 ? "negative" : "neutral";
|
|
1207
|
+
const coverageSign = diff.coverageDelta > 0 ? "+" : "";
|
|
1208
|
+
let html = `<!DOCTYPE html>
|
|
1209
|
+
<html lang="en">
|
|
1210
|
+
<head>
|
|
1211
|
+
<meta charset="UTF-8">
|
|
1212
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1213
|
+
<title>DocCov Diff Report</title>
|
|
1214
|
+
<style>
|
|
1215
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1216
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; padding: 2rem; line-height: 1.5; }
|
|
1217
|
+
.container { max-width: 900px; margin: 0 auto; }
|
|
1218
|
+
h1 { font-size: 1.5rem; margin-bottom: 1.5rem; color: #f0f6fc; }
|
|
1219
|
+
h2 { font-size: 1.1rem; margin: 1.5rem 0 0.75rem; color: #f0f6fc; border-bottom: 1px solid #30363d; padding-bottom: 0.5rem; }
|
|
1220
|
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin-bottom: 1rem; }
|
|
1221
|
+
.metric { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; }
|
|
1222
|
+
.metric-label { color: #8b949e; }
|
|
1223
|
+
.metric-value { font-weight: 600; }
|
|
1224
|
+
.positive { color: #3fb950; }
|
|
1225
|
+
.negative { color: #f85149; }
|
|
1226
|
+
.neutral { color: #8b949e; }
|
|
1227
|
+
.warning { color: #d29922; }
|
|
1228
|
+
.badge { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 12px; font-size: 0.75rem; font-weight: 500; }
|
|
1229
|
+
.badge-breaking { background: #f8514933; color: #f85149; }
|
|
1230
|
+
.badge-changed { background: #d2992233; color: #d29922; }
|
|
1231
|
+
.badge-added { background: #3fb95033; color: #3fb950; }
|
|
1232
|
+
.file-item { padding: 0.5rem; margin: 0.25rem 0; background: #0d1117; border-radius: 4px; }
|
|
1233
|
+
.file-name { font-family: monospace; font-size: 0.9rem; }
|
|
1234
|
+
.ref-list { margin-top: 0.5rem; padding-left: 1rem; font-size: 0.85rem; color: #8b949e; }
|
|
1235
|
+
.ref-item { margin: 0.25rem 0; }
|
|
1236
|
+
ul { list-style: none; }
|
|
1237
|
+
li { padding: 0.25rem 0; }
|
|
1238
|
+
code { font-family: monospace; background: #0d1117; padding: 0.125rem 0.375rem; border-radius: 3px; font-size: 0.9rem; }
|
|
1239
|
+
</style>
|
|
1240
|
+
</head>
|
|
1241
|
+
<body>
|
|
1242
|
+
<div class="container">
|
|
1243
|
+
<h1>\uD83D\uDCCA DocCov Diff Report</h1>
|
|
1244
|
+
|
|
1245
|
+
<div class="card">
|
|
1246
|
+
<div class="metric">
|
|
1247
|
+
<span class="metric-label">Coverage</span>
|
|
1248
|
+
<span class="metric-value ${coverageClass}">${diff.oldCoverage}% → ${diff.newCoverage}% (${coverageSign}${diff.coverageDelta}%)</span>
|
|
1249
|
+
</div>
|
|
1250
|
+
<div class="metric">
|
|
1251
|
+
<span class="metric-label">Breaking Changes</span>
|
|
1252
|
+
<span class="metric-value ${diff.breaking.length > 0 ? "negative" : "neutral"}">${diff.breaking.length}</span>
|
|
1253
|
+
</div>
|
|
1254
|
+
<div class="metric">
|
|
1255
|
+
<span class="metric-label">New Exports</span>
|
|
1256
|
+
<span class="metric-value positive">${diff.nonBreaking.length}</span>
|
|
1257
|
+
</div>
|
|
1258
|
+
<div class="metric">
|
|
1259
|
+
<span class="metric-label">Undocumented</span>
|
|
1260
|
+
<span class="metric-value ${diff.newUndocumented.length > 0 ? "warning" : "neutral"}">${diff.newUndocumented.length}</span>
|
|
1261
|
+
</div>
|
|
1262
|
+
</div>`;
|
|
1263
|
+
if (diff.breaking.length > 0) {
|
|
1264
|
+
html += `
|
|
1265
|
+
<h2>Breaking Changes</h2>
|
|
1266
|
+
<div class="card">
|
|
1267
|
+
<ul>`;
|
|
1268
|
+
for (const item of diff.categorizedBreaking ?? []) {
|
|
1269
|
+
const badgeClass = item.severity === "high" ? "badge-breaking" : "badge-changed";
|
|
1270
|
+
html += `
|
|
1271
|
+
<li><code>${item.name}</code> <span class="badge ${badgeClass}">${item.reason}</span></li>`;
|
|
1272
|
+
}
|
|
1273
|
+
html += `
|
|
1274
|
+
</ul>
|
|
1275
|
+
</div>`;
|
|
1276
|
+
}
|
|
1277
|
+
if (diff.memberChanges && diff.memberChanges.length > 0) {
|
|
1278
|
+
html += `
|
|
1279
|
+
<h2>Member Changes</h2>
|
|
1280
|
+
<div class="card">
|
|
1281
|
+
<ul>`;
|
|
1282
|
+
for (const mc of diff.memberChanges) {
|
|
1283
|
+
const badgeClass = mc.changeType === "removed" ? "badge-breaking" : mc.changeType === "added" ? "badge-added" : "badge-changed";
|
|
1284
|
+
const suggestion = mc.suggestion ? ` → ${mc.suggestion}` : "";
|
|
1285
|
+
html += `
|
|
1286
|
+
<li><code>${mc.className}.${mc.memberName}()</code> <span class="badge ${badgeClass}">${mc.changeType}</span>${suggestion}</li>`;
|
|
1287
|
+
}
|
|
1288
|
+
html += `
|
|
1289
|
+
</ul>
|
|
1290
|
+
</div>`;
|
|
1291
|
+
}
|
|
1292
|
+
if (diff.docsImpact && diff.docsImpact.impactedFiles.length > 0) {
|
|
1293
|
+
html += `
|
|
1294
|
+
<h2>Documentation Impact</h2>
|
|
1295
|
+
<div class="card">`;
|
|
1296
|
+
for (const file of diff.docsImpact.impactedFiles.slice(0, 10)) {
|
|
1297
|
+
const filename = path3.basename(file.file);
|
|
1298
|
+
html += `
|
|
1299
|
+
<div class="file-item">
|
|
1300
|
+
<div class="file-name">\uD83D\uDCC4 ${filename} <span class="neutral">(${file.references.length} issue${file.references.length > 1 ? "s" : ""})</span></div>
|
|
1301
|
+
<div class="ref-list">`;
|
|
1302
|
+
for (const ref of file.references.slice(0, 5)) {
|
|
1303
|
+
const name = ref.memberName ? `${ref.memberName}()` : ref.exportName;
|
|
1304
|
+
const change = ref.changeType === "removed" || ref.changeType === "method-removed" ? "removed" : "signature changed";
|
|
1305
|
+
html += `
|
|
1306
|
+
<div class="ref-item">Line ${ref.line}: <code>${name}</code> ${change}</div>`;
|
|
1307
|
+
}
|
|
1308
|
+
if (file.references.length > 5) {
|
|
1309
|
+
html += `
|
|
1310
|
+
<div class="ref-item neutral">... and ${file.references.length - 5} more</div>`;
|
|
958
1311
|
}
|
|
1312
|
+
html += `
|
|
1313
|
+
</div>
|
|
1314
|
+
</div>`;
|
|
959
1315
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1316
|
+
html += `
|
|
1317
|
+
</div>`;
|
|
1318
|
+
}
|
|
1319
|
+
const hasNewUndocumented = diff.newUndocumented.length > 0;
|
|
1320
|
+
const hasAllUndocumented = diff.docsImpact?.allUndocumented && diff.docsImpact.allUndocumented.length > 0;
|
|
1321
|
+
if (hasNewUndocumented || hasAllUndocumented) {
|
|
1322
|
+
html += `
|
|
1323
|
+
<h2>Missing Documentation</h2>
|
|
1324
|
+
<div class="card">`;
|
|
1325
|
+
if (hasNewUndocumented) {
|
|
1326
|
+
html += `
|
|
1327
|
+
<p class="warning">New exports missing docs (${diff.newUndocumented.length}):</p>
|
|
1328
|
+
<ul>`;
|
|
1329
|
+
for (const name of diff.newUndocumented.slice(0, 10)) {
|
|
1330
|
+
html += `
|
|
1331
|
+
<li><code>${name}</code></li>`;
|
|
965
1332
|
}
|
|
966
|
-
if (
|
|
967
|
-
|
|
1333
|
+
if (diff.newUndocumented.length > 10) {
|
|
1334
|
+
html += `
|
|
1335
|
+
<li class="neutral">... and ${diff.newUndocumented.length - 10} more</li>`;
|
|
968
1336
|
}
|
|
1337
|
+
html += `
|
|
1338
|
+
</ul>`;
|
|
969
1339
|
}
|
|
970
|
-
if (
|
|
971
|
-
|
|
1340
|
+
if (diff.docsImpact?.stats) {
|
|
1341
|
+
const { stats, allUndocumented } = diff.docsImpact;
|
|
1342
|
+
const docPercent = Math.round((1 - (allUndocumented?.length ?? 0) / stats.totalExports) * 100);
|
|
1343
|
+
html += `
|
|
1344
|
+
<div class="metric" style="margin-top: 1rem; border-top: 1px solid #30363d; padding-top: 1rem;">
|
|
1345
|
+
<span class="metric-label">Total Documentation Coverage</span>
|
|
1346
|
+
<span class="metric-value ${docPercent >= 80 ? "positive" : docPercent >= 50 ? "warning" : "negative"}">${stats.documentedExports}/${stats.totalExports} (${docPercent}%)</span>
|
|
1347
|
+
</div>`;
|
|
972
1348
|
}
|
|
1349
|
+
html += `
|
|
1350
|
+
</div>`;
|
|
973
1351
|
}
|
|
974
|
-
|
|
1352
|
+
html += `
|
|
1353
|
+
</div>
|
|
1354
|
+
</body>
|
|
1355
|
+
</html>`;
|
|
1356
|
+
return html;
|
|
975
1357
|
}
|
|
976
1358
|
|
|
977
1359
|
// src/commands/generate.ts
|
|
@@ -1306,17 +1688,225 @@ var buildTemplate = (format) => {
|
|
|
1306
1688
|
`);
|
|
1307
1689
|
};
|
|
1308
1690
|
|
|
1309
|
-
// src/commands/
|
|
1691
|
+
// src/commands/lint.ts
|
|
1310
1692
|
import * as fs5 from "node:fs";
|
|
1311
1693
|
import * as path6 from "node:path";
|
|
1312
1694
|
import {
|
|
1313
|
-
|
|
1695
|
+
applyEdits as applyEdits2,
|
|
1696
|
+
createSourceFile as createSourceFile2,
|
|
1314
1697
|
detectEntryPoint as detectEntryPoint3,
|
|
1315
1698
|
detectMonorepo as detectMonorepo3,
|
|
1699
|
+
DocCov as DocCov3,
|
|
1700
|
+
findJSDocLocation as findJSDocLocation2,
|
|
1316
1701
|
findPackageByName as findPackageByName3,
|
|
1317
|
-
|
|
1702
|
+
getDefaultConfig,
|
|
1703
|
+
getRule,
|
|
1704
|
+
lintExport as lintExport2,
|
|
1705
|
+
NodeFileSystem as NodeFileSystem3,
|
|
1706
|
+
serializeJSDoc as serializeJSDoc2
|
|
1318
1707
|
} from "@doccov/sdk";
|
|
1319
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";
|
|
1320
1910
|
|
|
1321
1911
|
// src/reports/markdown.ts
|
|
1322
1912
|
function bar(pct, width = 10) {
|
|
@@ -1493,42 +2083,42 @@ function registerReportCommand(program) {
|
|
|
1493
2083
|
try {
|
|
1494
2084
|
let spec;
|
|
1495
2085
|
if (options.spec) {
|
|
1496
|
-
const specPath =
|
|
1497
|
-
spec = JSON.parse(
|
|
2086
|
+
const specPath = path7.resolve(options.cwd, options.spec);
|
|
2087
|
+
spec = JSON.parse(fs6.readFileSync(specPath, "utf-8"));
|
|
1498
2088
|
} else {
|
|
1499
2089
|
let targetDir = options.cwd;
|
|
1500
2090
|
let entryFile = entry;
|
|
1501
|
-
const fileSystem = new
|
|
2091
|
+
const fileSystem = new NodeFileSystem4(options.cwd);
|
|
1502
2092
|
if (options.package) {
|
|
1503
|
-
const mono = await
|
|
2093
|
+
const mono = await detectMonorepo4(fileSystem);
|
|
1504
2094
|
if (!mono.isMonorepo) {
|
|
1505
2095
|
throw new Error(`Not a monorepo. Remove --package flag for single-package repos.`);
|
|
1506
2096
|
}
|
|
1507
|
-
const pkg =
|
|
2097
|
+
const pkg = findPackageByName4(mono.packages, options.package);
|
|
1508
2098
|
if (!pkg) {
|
|
1509
2099
|
const available = mono.packages.map((p) => p.name).join(", ");
|
|
1510
2100
|
throw new Error(`Package "${options.package}" not found. Available: ${available}`);
|
|
1511
2101
|
}
|
|
1512
|
-
targetDir =
|
|
2102
|
+
targetDir = path7.join(options.cwd, pkg.path);
|
|
1513
2103
|
}
|
|
1514
2104
|
if (!entryFile) {
|
|
1515
|
-
const targetFs = new
|
|
1516
|
-
const detected = await
|
|
1517
|
-
entryFile =
|
|
2105
|
+
const targetFs = new NodeFileSystem4(targetDir);
|
|
2106
|
+
const detected = await detectEntryPoint4(targetFs);
|
|
2107
|
+
entryFile = path7.join(targetDir, detected.path);
|
|
1518
2108
|
} else {
|
|
1519
|
-
entryFile =
|
|
2109
|
+
entryFile = path7.resolve(targetDir, entryFile);
|
|
1520
2110
|
}
|
|
1521
|
-
process.stdout.write(
|
|
2111
|
+
process.stdout.write(chalk7.cyan(`> Analyzing...
|
|
1522
2112
|
`));
|
|
1523
2113
|
try {
|
|
1524
2114
|
const resolveExternalTypes = !options.skipResolve;
|
|
1525
|
-
const doccov = new
|
|
2115
|
+
const doccov = new DocCov4({ resolveExternalTypes });
|
|
1526
2116
|
const result = await doccov.analyzeFileWithDiagnostics(entryFile);
|
|
1527
|
-
process.stdout.write(
|
|
2117
|
+
process.stdout.write(chalk7.green(`✓ Analysis complete
|
|
1528
2118
|
`));
|
|
1529
2119
|
spec = result.spec;
|
|
1530
2120
|
} catch (analysisError) {
|
|
1531
|
-
process.stdout.write(
|
|
2121
|
+
process.stdout.write(chalk7.red(`✗ Analysis failed
|
|
1532
2122
|
`));
|
|
1533
2123
|
throw analysisError;
|
|
1534
2124
|
}
|
|
@@ -1545,35 +2135,35 @@ function registerReportCommand(program) {
|
|
|
1545
2135
|
output = renderMarkdown(stats, { limit });
|
|
1546
2136
|
}
|
|
1547
2137
|
if (options.out) {
|
|
1548
|
-
const outPath =
|
|
1549
|
-
|
|
1550
|
-
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}`));
|
|
1551
2141
|
} else {
|
|
1552
2142
|
console.log(output);
|
|
1553
2143
|
}
|
|
1554
2144
|
} catch (err) {
|
|
1555
|
-
console.error(
|
|
2145
|
+
console.error(chalk7.red("Error:"), err instanceof Error ? err.message : err);
|
|
1556
2146
|
process.exitCode = 1;
|
|
1557
2147
|
}
|
|
1558
2148
|
});
|
|
1559
2149
|
}
|
|
1560
2150
|
|
|
1561
2151
|
// src/commands/scan.ts
|
|
1562
|
-
import * as
|
|
2152
|
+
import * as fs8 from "node:fs";
|
|
1563
2153
|
import * as os from "node:os";
|
|
1564
|
-
import * as
|
|
2154
|
+
import * as path9 from "node:path";
|
|
1565
2155
|
import {
|
|
1566
|
-
DocCov as
|
|
2156
|
+
DocCov as DocCov5,
|
|
1567
2157
|
detectBuildInfo,
|
|
1568
|
-
detectEntryPoint as
|
|
1569
|
-
detectMonorepo as
|
|
2158
|
+
detectEntryPoint as detectEntryPoint5,
|
|
2159
|
+
detectMonorepo as detectMonorepo5,
|
|
1570
2160
|
detectPackageManager,
|
|
1571
|
-
findPackageByName as
|
|
2161
|
+
findPackageByName as findPackageByName5,
|
|
1572
2162
|
formatPackageList,
|
|
1573
2163
|
getInstallCommand,
|
|
1574
|
-
NodeFileSystem as
|
|
2164
|
+
NodeFileSystem as NodeFileSystem5
|
|
1575
2165
|
} from "@doccov/sdk";
|
|
1576
|
-
import
|
|
2166
|
+
import chalk8 from "chalk";
|
|
1577
2167
|
import { simpleGit } from "simple-git";
|
|
1578
2168
|
|
|
1579
2169
|
// src/utils/github-url.ts
|
|
@@ -1607,8 +2197,8 @@ function buildDisplayUrl(parsed) {
|
|
|
1607
2197
|
}
|
|
1608
2198
|
|
|
1609
2199
|
// src/utils/llm-build-plan.ts
|
|
1610
|
-
import * as
|
|
1611
|
-
import * as
|
|
2200
|
+
import * as fs7 from "node:fs";
|
|
2201
|
+
import * as path8 from "node:path";
|
|
1612
2202
|
import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
|
|
1613
2203
|
import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
|
|
1614
2204
|
import { generateObject as generateObject3 } from "ai";
|
|
@@ -1644,10 +2234,10 @@ function getModel3() {
|
|
|
1644
2234
|
async function gatherContextFiles(repoDir) {
|
|
1645
2235
|
const sections = [];
|
|
1646
2236
|
for (const fileName of CONTEXT_FILES) {
|
|
1647
|
-
const filePath =
|
|
1648
|
-
if (
|
|
2237
|
+
const filePath = path8.join(repoDir, fileName);
|
|
2238
|
+
if (fs7.existsSync(filePath)) {
|
|
1649
2239
|
try {
|
|
1650
|
-
let content =
|
|
2240
|
+
let content = fs7.readFileSync(filePath, "utf-8");
|
|
1651
2241
|
if (content.length > MAX_FILE_CHARS) {
|
|
1652
2242
|
content = `${content.slice(0, MAX_FILE_CHARS)}
|
|
1653
2243
|
... (truncated)`;
|
|
@@ -1699,14 +2289,14 @@ async function generateBuildPlan(repoDir) {
|
|
|
1699
2289
|
}
|
|
1700
2290
|
|
|
1701
2291
|
// src/commands/scan.ts
|
|
1702
|
-
var
|
|
1703
|
-
createDocCov: (options) => new
|
|
2292
|
+
var defaultDependencies6 = {
|
|
2293
|
+
createDocCov: (options) => new DocCov5(options),
|
|
1704
2294
|
log: console.log,
|
|
1705
2295
|
error: console.error
|
|
1706
2296
|
};
|
|
1707
2297
|
function registerScanCommand(program, dependencies = {}) {
|
|
1708
2298
|
const { createDocCov, log, error } = {
|
|
1709
|
-
...
|
|
2299
|
+
...defaultDependencies6,
|
|
1710
2300
|
...dependencies
|
|
1711
2301
|
};
|
|
1712
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) => {
|
|
@@ -1716,12 +2306,12 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
1716
2306
|
const cloneUrl = buildCloneUrl(parsed);
|
|
1717
2307
|
const displayUrl = buildDisplayUrl(parsed);
|
|
1718
2308
|
log("");
|
|
1719
|
-
log(
|
|
1720
|
-
log(
|
|
2309
|
+
log(chalk8.bold(`Scanning ${displayUrl}`));
|
|
2310
|
+
log(chalk8.gray(`Branch/tag: ${parsed.ref}`));
|
|
1721
2311
|
log("");
|
|
1722
|
-
tempDir =
|
|
1723
|
-
|
|
1724
|
-
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}...
|
|
1725
2315
|
`));
|
|
1726
2316
|
try {
|
|
1727
2317
|
const git = simpleGit({
|
|
@@ -1743,10 +2333,10 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
1743
2333
|
} finally {
|
|
1744
2334
|
process.env = originalEnv;
|
|
1745
2335
|
}
|
|
1746
|
-
process.stdout.write(
|
|
2336
|
+
process.stdout.write(chalk8.green(`✓ Cloned ${parsed.owner}/${parsed.repo}
|
|
1747
2337
|
`));
|
|
1748
2338
|
} catch (cloneError) {
|
|
1749
|
-
process.stdout.write(
|
|
2339
|
+
process.stdout.write(chalk8.red(`✗ Failed to clone repository
|
|
1750
2340
|
`));
|
|
1751
2341
|
const message = cloneError instanceof Error ? cloneError.message : String(cloneError);
|
|
1752
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")) {
|
|
@@ -1762,11 +2352,11 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
1762
2352
|
}
|
|
1763
2353
|
throw new Error(`Clone failed: ${message}`);
|
|
1764
2354
|
}
|
|
1765
|
-
const fileSystem = new
|
|
2355
|
+
const fileSystem = new NodeFileSystem5(tempDir);
|
|
1766
2356
|
if (options.skipInstall) {
|
|
1767
|
-
log(
|
|
2357
|
+
log(chalk8.gray("Skipping dependency installation (--skip-install)"));
|
|
1768
2358
|
} else {
|
|
1769
|
-
process.stdout.write(
|
|
2359
|
+
process.stdout.write(chalk8.cyan(`> Installing dependencies...
|
|
1770
2360
|
`));
|
|
1771
2361
|
const installErrors = [];
|
|
1772
2362
|
try {
|
|
@@ -1816,56 +2406,56 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
1816
2406
|
}
|
|
1817
2407
|
}
|
|
1818
2408
|
if (installed) {
|
|
1819
|
-
process.stdout.write(
|
|
2409
|
+
process.stdout.write(chalk8.green(`✓ Dependencies installed
|
|
1820
2410
|
`));
|
|
1821
2411
|
} else {
|
|
1822
|
-
process.stdout.write(
|
|
2412
|
+
process.stdout.write(chalk8.yellow(`⚠ Could not install dependencies (analysis may be limited)
|
|
1823
2413
|
`));
|
|
1824
2414
|
for (const err of installErrors) {
|
|
1825
|
-
log(
|
|
2415
|
+
log(chalk8.gray(` ${err}`));
|
|
1826
2416
|
}
|
|
1827
2417
|
}
|
|
1828
2418
|
} catch (outerError) {
|
|
1829
2419
|
const msg = outerError instanceof Error ? outerError.message : String(outerError);
|
|
1830
|
-
process.stdout.write(
|
|
2420
|
+
process.stdout.write(chalk8.yellow(`⚠ Could not install dependencies: ${msg.slice(0, 100)}
|
|
1831
2421
|
`));
|
|
1832
2422
|
for (const err of installErrors) {
|
|
1833
|
-
log(
|
|
2423
|
+
log(chalk8.gray(` ${err}`));
|
|
1834
2424
|
}
|
|
1835
2425
|
}
|
|
1836
2426
|
}
|
|
1837
2427
|
let targetDir = tempDir;
|
|
1838
2428
|
let packageName;
|
|
1839
|
-
const mono = await
|
|
2429
|
+
const mono = await detectMonorepo5(fileSystem);
|
|
1840
2430
|
if (mono.isMonorepo) {
|
|
1841
2431
|
if (!options.package) {
|
|
1842
2432
|
error("");
|
|
1843
|
-
error(
|
|
2433
|
+
error(chalk8.red(`Monorepo detected with ${mono.packages.length} packages. Specify target with --package:`));
|
|
1844
2434
|
error("");
|
|
1845
2435
|
error(formatPackageList(mono.packages));
|
|
1846
2436
|
error("");
|
|
1847
2437
|
throw new Error("Monorepo requires --package flag");
|
|
1848
2438
|
}
|
|
1849
|
-
const pkg =
|
|
2439
|
+
const pkg = findPackageByName5(mono.packages, options.package);
|
|
1850
2440
|
if (!pkg) {
|
|
1851
2441
|
error("");
|
|
1852
|
-
error(
|
|
2442
|
+
error(chalk8.red(`Package "${options.package}" not found. Available packages:`));
|
|
1853
2443
|
error("");
|
|
1854
2444
|
error(formatPackageList(mono.packages));
|
|
1855
2445
|
error("");
|
|
1856
2446
|
throw new Error(`Package not found: ${options.package}`);
|
|
1857
2447
|
}
|
|
1858
|
-
targetDir =
|
|
2448
|
+
targetDir = path9.join(tempDir, pkg.path);
|
|
1859
2449
|
packageName = pkg.name;
|
|
1860
|
-
log(
|
|
2450
|
+
log(chalk8.gray(`Analyzing package: ${packageName}`));
|
|
1861
2451
|
}
|
|
1862
|
-
process.stdout.write(
|
|
2452
|
+
process.stdout.write(chalk8.cyan(`> Detecting entry point...
|
|
1863
2453
|
`));
|
|
1864
2454
|
let entryPath;
|
|
1865
|
-
const targetFs = mono.isMonorepo ? new
|
|
2455
|
+
const targetFs = mono.isMonorepo ? new NodeFileSystem5(targetDir) : fileSystem;
|
|
1866
2456
|
let buildFailed = false;
|
|
1867
2457
|
const runLlmFallback = async (reason) => {
|
|
1868
|
-
process.stdout.write(
|
|
2458
|
+
process.stdout.write(chalk8.cyan(`> ${reason}, trying LLM fallback...
|
|
1869
2459
|
`));
|
|
1870
2460
|
const plan = await generateBuildPlan(targetDir);
|
|
1871
2461
|
if (!plan) {
|
|
@@ -1874,88 +2464,88 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
1874
2464
|
if (plan.buildCommands.length > 0) {
|
|
1875
2465
|
const { execSync } = await import("node:child_process");
|
|
1876
2466
|
for (const cmd of plan.buildCommands) {
|
|
1877
|
-
log(
|
|
2467
|
+
log(chalk8.gray(` Running: ${cmd}`));
|
|
1878
2468
|
try {
|
|
1879
2469
|
execSync(cmd, { cwd: targetDir, stdio: "pipe", timeout: 300000 });
|
|
1880
2470
|
} catch (buildError) {
|
|
1881
2471
|
buildFailed = true;
|
|
1882
2472
|
const msg = buildError instanceof Error ? buildError.message : String(buildError);
|
|
1883
2473
|
if (msg.includes("rustc") || msg.includes("cargo") || msg.includes("wasm-pack")) {
|
|
1884
|
-
log(
|
|
2474
|
+
log(chalk8.yellow(` ⚠ Build requires Rust toolchain (not available)`));
|
|
1885
2475
|
} else if (msg.includes("rimraf") || msg.includes("command not found")) {
|
|
1886
|
-
log(
|
|
2476
|
+
log(chalk8.yellow(` ⚠ Build failed: missing dependencies`));
|
|
1887
2477
|
} else {
|
|
1888
|
-
log(
|
|
2478
|
+
log(chalk8.yellow(` ⚠ Build failed: ${msg.slice(0, 80)}`));
|
|
1889
2479
|
}
|
|
1890
2480
|
}
|
|
1891
2481
|
}
|
|
1892
2482
|
}
|
|
1893
2483
|
if (plan.notes) {
|
|
1894
|
-
log(
|
|
2484
|
+
log(chalk8.gray(` Note: ${plan.notes}`));
|
|
1895
2485
|
}
|
|
1896
2486
|
return plan.entryPoint;
|
|
1897
2487
|
};
|
|
1898
2488
|
try {
|
|
1899
|
-
const entry = await
|
|
2489
|
+
const entry = await detectEntryPoint5(targetFs);
|
|
1900
2490
|
const buildInfo = await detectBuildInfo(targetFs);
|
|
1901
2491
|
const needsBuildStep = entry.isDeclarationOnly && buildInfo.exoticIndicators.wasm;
|
|
1902
2492
|
if (needsBuildStep) {
|
|
1903
|
-
process.stdout.write(
|
|
2493
|
+
process.stdout.write(chalk8.cyan(`> Detected .d.ts entry with WASM indicators...
|
|
1904
2494
|
`));
|
|
1905
2495
|
const llmEntry = await runLlmFallback("WASM project detected");
|
|
1906
2496
|
if (llmEntry) {
|
|
1907
|
-
entryPath =
|
|
2497
|
+
entryPath = path9.join(targetDir, llmEntry);
|
|
1908
2498
|
if (buildFailed) {
|
|
1909
|
-
process.stdout.write(
|
|
2499
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (using pre-committed declarations)
|
|
1910
2500
|
`));
|
|
1911
|
-
log(
|
|
2501
|
+
log(chalk8.gray(" Coverage may be limited - generated .d.ts files typically lack JSDoc"));
|
|
1912
2502
|
} else {
|
|
1913
|
-
process.stdout.write(
|
|
2503
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (from LLM fallback - WASM project)
|
|
1914
2504
|
`));
|
|
1915
2505
|
}
|
|
1916
2506
|
} else {
|
|
1917
|
-
entryPath =
|
|
1918
|
-
process.stdout.write(
|
|
2507
|
+
entryPath = path9.join(targetDir, entry.path);
|
|
2508
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${entry.path} (from ${entry.source})
|
|
1919
2509
|
`));
|
|
1920
|
-
log(
|
|
2510
|
+
log(chalk8.yellow(" ⚠ WASM project detected but no API key - analysis may be limited"));
|
|
1921
2511
|
}
|
|
1922
2512
|
} else {
|
|
1923
|
-
entryPath =
|
|
1924
|
-
process.stdout.write(
|
|
2513
|
+
entryPath = path9.join(targetDir, entry.path);
|
|
2514
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${entry.path} (from ${entry.source})
|
|
1925
2515
|
`));
|
|
1926
2516
|
}
|
|
1927
2517
|
} catch (entryError) {
|
|
1928
2518
|
const llmEntry = await runLlmFallback("Heuristics failed");
|
|
1929
2519
|
if (llmEntry) {
|
|
1930
|
-
entryPath =
|
|
1931
|
-
process.stdout.write(
|
|
2520
|
+
entryPath = path9.join(targetDir, llmEntry);
|
|
2521
|
+
process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (from LLM fallback)
|
|
1932
2522
|
`));
|
|
1933
2523
|
} else {
|
|
1934
|
-
process.stdout.write(
|
|
2524
|
+
process.stdout.write(chalk8.red(`✗ Could not detect entry point (set OPENAI_API_KEY for smart fallback)
|
|
1935
2525
|
`));
|
|
1936
2526
|
throw entryError;
|
|
1937
2527
|
}
|
|
1938
2528
|
}
|
|
1939
|
-
process.stdout.write(
|
|
2529
|
+
process.stdout.write(chalk8.cyan(`> Analyzing documentation coverage...
|
|
1940
2530
|
`));
|
|
1941
2531
|
let result;
|
|
1942
2532
|
try {
|
|
1943
2533
|
const resolveExternalTypes = !options.skipResolve;
|
|
1944
2534
|
const doccov = createDocCov({ resolveExternalTypes });
|
|
1945
2535
|
result = await doccov.analyzeFileWithDiagnostics(entryPath);
|
|
1946
|
-
process.stdout.write(
|
|
2536
|
+
process.stdout.write(chalk8.green(`✓ Analysis complete
|
|
1947
2537
|
`));
|
|
1948
2538
|
} catch (analysisError) {
|
|
1949
|
-
process.stdout.write(
|
|
2539
|
+
process.stdout.write(chalk8.red(`✗ Analysis failed
|
|
1950
2540
|
`));
|
|
1951
2541
|
throw analysisError;
|
|
1952
2542
|
}
|
|
1953
2543
|
const spec = result.spec;
|
|
1954
2544
|
const coverageScore = spec.docs?.coverageScore ?? 0;
|
|
1955
2545
|
if (options.saveSpec) {
|
|
1956
|
-
const specPath =
|
|
1957
|
-
|
|
1958
|
-
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}`));
|
|
1959
2549
|
}
|
|
1960
2550
|
const undocumented = [];
|
|
1961
2551
|
const driftIssues = [];
|
|
@@ -1992,7 +2582,7 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
1992
2582
|
printTextResult(scanResult, log);
|
|
1993
2583
|
}
|
|
1994
2584
|
} catch (commandError) {
|
|
1995
|
-
error(
|
|
2585
|
+
error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
1996
2586
|
process.exitCode = 1;
|
|
1997
2587
|
} finally {
|
|
1998
2588
|
if (tempDir && options.cleanup !== false) {
|
|
@@ -2002,63 +2592,180 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2002
2592
|
stdio: "ignore"
|
|
2003
2593
|
}).unref();
|
|
2004
2594
|
} else if (tempDir) {
|
|
2005
|
-
log(
|
|
2595
|
+
log(chalk8.gray(`Repo preserved at: ${tempDir}`));
|
|
2006
2596
|
}
|
|
2007
2597
|
}
|
|
2008
2598
|
});
|
|
2009
2599
|
}
|
|
2010
2600
|
function printTextResult(result, log) {
|
|
2011
2601
|
log("");
|
|
2012
|
-
log(
|
|
2602
|
+
log(chalk8.bold("DocCov Scan Results"));
|
|
2013
2603
|
log("─".repeat(40));
|
|
2014
2604
|
const repoName = result.packageName ? `${result.owner}/${result.repo} (${result.packageName})` : `${result.owner}/${result.repo}`;
|
|
2015
|
-
log(`Repository: ${
|
|
2016
|
-
log(`Branch: ${
|
|
2605
|
+
log(`Repository: ${chalk8.cyan(repoName)}`);
|
|
2606
|
+
log(`Branch: ${chalk8.gray(result.ref)}`);
|
|
2017
2607
|
log("");
|
|
2018
|
-
const coverageColor = result.coverage >= 80 ?
|
|
2019
|
-
log(
|
|
2608
|
+
const coverageColor = result.coverage >= 80 ? chalk8.green : result.coverage >= 50 ? chalk8.yellow : chalk8.red;
|
|
2609
|
+
log(chalk8.bold("Coverage"));
|
|
2020
2610
|
log(` ${coverageColor(`${result.coverage}%`)}`);
|
|
2021
2611
|
log("");
|
|
2022
|
-
log(
|
|
2612
|
+
log(chalk8.bold("Stats"));
|
|
2023
2613
|
log(` ${result.exportCount} exports`);
|
|
2024
2614
|
log(` ${result.typeCount} types`);
|
|
2025
2615
|
log(` ${result.undocumented.length} undocumented`);
|
|
2026
2616
|
log(` ${result.driftCount} drift issues`);
|
|
2027
2617
|
if (result.undocumented.length > 0) {
|
|
2028
2618
|
log("");
|
|
2029
|
-
log(
|
|
2619
|
+
log(chalk8.bold("Undocumented Exports"));
|
|
2030
2620
|
for (const name of result.undocumented.slice(0, 10)) {
|
|
2031
|
-
log(
|
|
2621
|
+
log(chalk8.yellow(` ! ${name}`));
|
|
2032
2622
|
}
|
|
2033
2623
|
if (result.undocumented.length > 10) {
|
|
2034
|
-
log(
|
|
2624
|
+
log(chalk8.gray(` ... and ${result.undocumented.length - 10} more`));
|
|
2035
2625
|
}
|
|
2036
2626
|
}
|
|
2037
2627
|
if (result.drift.length > 0) {
|
|
2038
2628
|
log("");
|
|
2039
|
-
log(
|
|
2629
|
+
log(chalk8.bold("Drift Issues"));
|
|
2040
2630
|
for (const d of result.drift.slice(0, 5)) {
|
|
2041
|
-
log(
|
|
2631
|
+
log(chalk8.red(` • ${d.export}: ${d.issue}`));
|
|
2042
2632
|
}
|
|
2043
2633
|
if (result.drift.length > 5) {
|
|
2044
|
-
log(
|
|
2634
|
+
log(chalk8.gray(` ... and ${result.drift.length - 5} more`));
|
|
2045
2635
|
}
|
|
2046
2636
|
}
|
|
2047
2637
|
log("");
|
|
2048
2638
|
}
|
|
2049
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
|
+
|
|
2050
2755
|
// src/cli.ts
|
|
2051
2756
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2052
|
-
var __dirname2 =
|
|
2053
|
-
var packageJson = JSON.parse(readFileSync5(
|
|
2757
|
+
var __dirname2 = path11.dirname(__filename2);
|
|
2758
|
+
var packageJson = JSON.parse(readFileSync5(path11.join(__dirname2, "../package.json"), "utf-8"));
|
|
2054
2759
|
var program = new Command;
|
|
2055
2760
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
2056
2761
|
registerGenerateCommand(program);
|
|
2057
2762
|
registerCheckCommand(program);
|
|
2058
2763
|
registerDiffCommand(program);
|
|
2059
2764
|
registerInitCommand(program);
|
|
2765
|
+
registerLintCommand(program);
|
|
2060
2766
|
registerReportCommand(program);
|
|
2061
2767
|
registerScanCommand(program);
|
|
2768
|
+
registerTypecheckCommand(program);
|
|
2062
2769
|
program.command("*", { hidden: true }).action(() => {
|
|
2063
2770
|
program.outputHelp();
|
|
2064
2771
|
});
|