@doccov/cli 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +203 -162
  2. package/package.json +2 -3
package/dist/cli.js CHANGED
@@ -188,7 +188,7 @@ import {
188
188
  import {
189
189
  DRIFT_CATEGORIES as DRIFT_CATEGORIES2
190
190
  } from "@openpkg-ts/spec";
191
- import chalk2 from "chalk";
191
+ import chalk3 from "chalk";
192
192
 
193
193
  // src/reports/diff-markdown.ts
194
194
  import * as path2 from "node:path";
@@ -770,6 +770,57 @@ async function parseAssertionsWithLLM(code) {
770
770
  }
771
771
  }
772
772
 
773
+ // src/utils/progress.ts
774
+ import chalk2 from "chalk";
775
+ class StepProgress {
776
+ steps;
777
+ currentStep = 0;
778
+ startTime;
779
+ stepStartTime;
780
+ constructor(steps) {
781
+ this.steps = steps;
782
+ this.startTime = Date.now();
783
+ this.stepStartTime = Date.now();
784
+ }
785
+ start(stepIndex) {
786
+ this.currentStep = stepIndex ?? 0;
787
+ this.stepStartTime = Date.now();
788
+ this.render();
789
+ }
790
+ next() {
791
+ this.completeCurrentStep();
792
+ this.currentStep++;
793
+ if (this.currentStep < this.steps.length) {
794
+ this.stepStartTime = Date.now();
795
+ this.render();
796
+ }
797
+ }
798
+ complete(message) {
799
+ this.completeCurrentStep();
800
+ if (message) {
801
+ const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
802
+ console.log(`${chalk2.green("✓")} ${message} ${chalk2.dim(`(${elapsed}s)`)}`);
803
+ }
804
+ }
805
+ render() {
806
+ const step = this.steps[this.currentStep];
807
+ if (!step)
808
+ return;
809
+ const label = step.activeLabel ?? step.label;
810
+ const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
811
+ process.stdout.write(`\r${prefix} ${chalk2.cyan(label)}...`);
812
+ }
813
+ completeCurrentStep() {
814
+ const step = this.steps[this.currentStep];
815
+ if (!step)
816
+ return;
817
+ const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
818
+ const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
819
+ process.stdout.write(`\r${" ".repeat(80)}\r`);
820
+ console.log(`${prefix} ${step.label} ${chalk2.green("✓")} ${chalk2.dim(`(${elapsed}s)`)}`);
821
+ }
822
+ }
823
+
773
824
  // src/utils/validation.ts
774
825
  function clampPercentage(value, fallback = 80) {
775
826
  if (Number.isNaN(value))
@@ -812,24 +863,32 @@ function registerCheckCommand(program, dependencies = {}) {
812
863
  };
813
864
  program.command("check [entry]").description("Check documentation coverage and output reports").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("--max-drift <percentage>", "Maximum drift percentage allowed (0-100)", (value) => Number(value)).option("--examples [mode]", "Example validation: presence, typecheck, run (comma-separated). Bare flag runs all.").option("--skip-resolve", "Skip external type resolution from node_modules").option("--fix", "Auto-fix drift issues").option("--write", "Alias for --fix").option("--dry-run", "Preview fixes without writing (requires --fix)").option("--format <format>", "Output format: text, json, markdown, html, github", "text").option("-o, --output <file>", "Custom output path (overrides default .doccov/ path)").option("--stdout", "Output to stdout instead of writing to .doccov/").option("--update-snapshot", "Force regenerate .doccov/report.json").option("--limit <n>", "Max exports to show in report tables", "20").option("--max-type-depth <number>", "Maximum depth for type conversion (default: 20)").option("--no-cache", "Bypass spec cache and force regeneration").action(async (entry, options) => {
814
865
  try {
866
+ const validations = parseExamplesFlag(options.examples);
867
+ const hasExamples = validations.length > 0;
868
+ const stepList = [
869
+ { label: "Resolved target", activeLabel: "Resolving target" },
870
+ { label: "Loaded config", activeLabel: "Loading config" },
871
+ { label: "Generated spec", activeLabel: "Generating spec" },
872
+ { label: "Enriched spec", activeLabel: "Enriching spec" },
873
+ ...hasExamples ? [{ label: "Validated examples", activeLabel: "Validating examples" }] : [],
874
+ { label: "Processed results", activeLabel: "Processing results" }
875
+ ];
876
+ const steps = new StepProgress(stepList);
877
+ steps.start();
815
878
  const fileSystem = new NodeFileSystem(options.cwd);
816
879
  const resolved = await resolveTarget(fileSystem, {
817
880
  cwd: options.cwd,
818
881
  package: options.package,
819
882
  entry
820
883
  });
821
- const { targetDir, entryFile, packageInfo, entryPointInfo } = resolved;
822
- if (packageInfo) {
823
- log(chalk2.gray(`Found package at ${packageInfo.path}`));
824
- }
825
- if (!entry) {
826
- log(chalk2.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
827
- }
884
+ const { targetDir, entryFile } = resolved;
885
+ steps.next();
828
886
  const config = await loadDocCovConfig(targetDir);
829
887
  const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage;
830
888
  const minCoverage = minCoverageRaw !== undefined ? clampPercentage(minCoverageRaw) : undefined;
831
889
  const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
832
890
  const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
891
+ steps.next();
833
892
  const resolveExternalTypes = !options.skipResolve;
834
893
  let specResult;
835
894
  const doccov = createDocCov({
@@ -839,14 +898,13 @@ function registerCheckCommand(program, dependencies = {}) {
839
898
  cwd: options.cwd
840
899
  });
841
900
  specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
842
- if (specResult.fromCache) {
843
- log(chalk2.gray("Using cached spec"));
844
- }
845
901
  if (!specResult) {
846
902
  throw new Error("Failed to analyze documentation coverage.");
847
903
  }
904
+ steps.next();
848
905
  const spec = enrichSpec(specResult.spec);
849
906
  const format = options.format ?? "text";
907
+ steps.next();
850
908
  const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
851
909
  const specInfos = specResult.diagnostics.filter((d) => d.severity === "info");
852
910
  const shouldFix = options.fix || options.write;
@@ -856,11 +914,10 @@ function registerCheckCommand(program, dependencies = {}) {
856
914
  violations.push({ exportName: exp.name, violation: v });
857
915
  }
858
916
  }
859
- const validations = parseExamplesFlag(options.examples);
860
917
  let exampleResult;
861
918
  const typecheckErrors = [];
862
919
  const runtimeDrifts = [];
863
- if (validations.length > 0) {
920
+ if (hasExamples) {
864
921
  exampleResult = await validateExamples(spec.exports ?? [], {
865
922
  validations,
866
923
  packagePath: targetDir,
@@ -891,22 +948,23 @@ function registerCheckCommand(program, dependencies = {}) {
891
948
  });
892
949
  }
893
950
  }
951
+ steps.next();
894
952
  }
895
953
  const coverageScore = spec.docs?.coverageScore ?? 0;
896
954
  const allDriftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
897
- let driftExports = validations.length > 0 ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
955
+ let driftExports = hasExamples ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
898
956
  const fixedDriftKeys = new Set;
899
957
  if (shouldFix && driftExports.length > 0) {
900
958
  const allDrifts = collectDriftsFromExports(spec.exports ?? []);
901
959
  if (allDrifts.length > 0) {
902
960
  const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
903
961
  if (fixable.length === 0) {
904
- log(chalk2.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
962
+ log(chalk3.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
905
963
  } else {
906
964
  log("");
907
- log(chalk2.bold(`Found ${fixable.length} fixable issue(s)`));
965
+ log(chalk3.bold(`Found ${fixable.length} fixable issue(s)`));
908
966
  if (nonFixable.length > 0) {
909
- log(chalk2.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
967
+ log(chalk3.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
910
968
  }
911
969
  log("");
912
970
  const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
@@ -914,22 +972,22 @@ function registerCheckCommand(program, dependencies = {}) {
914
972
  const editsByFile = new Map;
915
973
  for (const [exp, drifts] of groupedDrifts) {
916
974
  if (!exp.source?.file) {
917
- log(chalk2.gray(` Skipping ${exp.name}: no source location`));
975
+ log(chalk3.gray(` Skipping ${exp.name}: no source location`));
918
976
  continue;
919
977
  }
920
978
  if (exp.source.file.endsWith(".d.ts")) {
921
- log(chalk2.gray(` Skipping ${exp.name}: declaration file`));
979
+ log(chalk3.gray(` Skipping ${exp.name}: declaration file`));
922
980
  continue;
923
981
  }
924
982
  const filePath = path4.resolve(targetDir, exp.source.file);
925
983
  if (!fs2.existsSync(filePath)) {
926
- log(chalk2.gray(` Skipping ${exp.name}: file not found`));
984
+ log(chalk3.gray(` Skipping ${exp.name}: file not found`));
927
985
  continue;
928
986
  }
929
987
  const sourceFile = createSourceFile(filePath);
930
988
  const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
931
989
  if (!location) {
932
- log(chalk2.gray(` Skipping ${exp.name}: could not find declaration`));
990
+ log(chalk3.gray(` Skipping ${exp.name}: could not find declaration`));
933
991
  continue;
934
992
  }
935
993
  let existingPatch = {};
@@ -962,26 +1020,26 @@ function registerCheckCommand(program, dependencies = {}) {
962
1020
  }
963
1021
  if (edits.length > 0) {
964
1022
  if (options.dryRun) {
965
- log(chalk2.bold("Dry run - changes that would be made:"));
1023
+ log(chalk3.bold("Dry run - changes that would be made:"));
966
1024
  log("");
967
1025
  for (const [filePath, fileEdits] of editsByFile) {
968
1026
  const relativePath = path4.relative(targetDir, filePath);
969
- log(chalk2.cyan(` ${relativePath}:`));
1027
+ log(chalk3.cyan(` ${relativePath}:`));
970
1028
  for (const { export: exp, edit, fixes } of fileEdits) {
971
1029
  const lineInfo = edit.hasExisting ? `lines ${edit.startLine + 1}-${edit.endLine + 1}` : `line ${edit.startLine + 1}`;
972
- log(` ${chalk2.bold(exp.name)} [${lineInfo}]`);
1030
+ log(` ${chalk3.bold(exp.name)} [${lineInfo}]`);
973
1031
  for (const fix of fixes) {
974
- log(chalk2.green(` + ${fix.description}`));
1032
+ log(chalk3.green(` + ${fix.description}`));
975
1033
  }
976
1034
  }
977
1035
  log("");
978
1036
  }
979
- log(chalk2.gray("Run without --dry-run to apply these changes."));
1037
+ log(chalk3.gray("Run without --dry-run to apply these changes."));
980
1038
  } else {
981
1039
  const applyResult = await applyEdits(edits);
982
1040
  if (applyResult.errors.length > 0) {
983
1041
  for (const err of applyResult.errors) {
984
- error(chalk2.red(` ${err.file}: ${err.error}`));
1042
+ error(chalk3.red(` ${err.file}: ${err.error}`));
985
1043
  }
986
1044
  }
987
1045
  }
@@ -992,6 +1050,7 @@ function registerCheckCommand(program, dependencies = {}) {
992
1050
  driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
993
1051
  }
994
1052
  }
1053
+ steps.complete("Check complete");
995
1054
  if (format !== "text") {
996
1055
  const limit = parseInt(options.limit, 10) || 20;
997
1056
  const stats = computeStats(spec);
@@ -1051,15 +1110,15 @@ function registerCheckCommand(program, dependencies = {}) {
1051
1110
  if (specWarnings.length > 0 || specInfos.length > 0) {
1052
1111
  log("");
1053
1112
  for (const diag of specWarnings) {
1054
- log(chalk2.yellow(`⚠ ${diag.message}`));
1113
+ log(chalk3.yellow(`⚠ ${diag.message}`));
1055
1114
  if (diag.suggestion) {
1056
- log(chalk2.gray(` ${diag.suggestion}`));
1115
+ log(chalk3.gray(` ${diag.suggestion}`));
1057
1116
  }
1058
1117
  }
1059
1118
  for (const diag of specInfos) {
1060
- log(chalk2.cyan(`ℹ ${diag.message}`));
1119
+ log(chalk3.cyan(`ℹ ${diag.message}`));
1061
1120
  if (diag.suggestion) {
1062
- log(chalk2.gray(` ${diag.suggestion}`));
1121
+ log(chalk3.gray(` ${diag.suggestion}`));
1063
1122
  }
1064
1123
  }
1065
1124
  }
@@ -1069,23 +1128,23 @@ function registerCheckCommand(program, dependencies = {}) {
1069
1128
  const errorCount = violations.filter((v) => v.violation.severity === "error").length;
1070
1129
  const warnCount = violations.filter((v) => v.violation.severity === "warn").length;
1071
1130
  log("");
1072
- log(chalk2.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1131
+ log(chalk3.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1073
1132
  log("");
1074
1133
  log(` Exports: ${totalExports}`);
1075
1134
  if (minCoverage !== undefined) {
1076
1135
  if (coverageFailed) {
1077
- log(chalk2.red(` Coverage: ✗ ${coverageScore}%`) + chalk2.dim(` (min ${minCoverage}%)`));
1136
+ log(chalk3.red(` Coverage: ✗ ${coverageScore}%`) + chalk3.dim(` (min ${minCoverage}%)`));
1078
1137
  } else {
1079
- log(chalk2.green(` Coverage: ✓ ${coverageScore}%`) + chalk2.dim(` (min ${minCoverage}%)`));
1138
+ log(chalk3.green(` Coverage: ✓ ${coverageScore}%`) + chalk3.dim(` (min ${minCoverage}%)`));
1080
1139
  }
1081
1140
  } else {
1082
1141
  log(` Coverage: ${coverageScore}%`);
1083
1142
  }
1084
1143
  if (maxDrift !== undefined) {
1085
1144
  if (driftFailed) {
1086
- log(chalk2.red(` Drift: ✗ ${driftScore}%`) + chalk2.dim(` (max ${maxDrift}%)`));
1145
+ log(chalk3.red(` Drift: ✗ ${driftScore}%`) + chalk3.dim(` (max ${maxDrift}%)`));
1087
1146
  } else {
1088
- log(chalk2.green(` Drift: ✓ ${driftScore}%`) + chalk2.dim(` (max ${maxDrift}%)`));
1147
+ log(chalk3.green(` Drift: ✓ ${driftScore}%`) + chalk3.dim(` (max ${maxDrift}%)`));
1089
1148
  }
1090
1149
  } else {
1091
1150
  log(` Drift: ${driftScore}%`);
@@ -1095,7 +1154,7 @@ function registerCheckCommand(program, dependencies = {}) {
1095
1154
  if (typecheckCount > 0) {
1096
1155
  log(` Examples: ${typecheckCount} type errors`);
1097
1156
  } else {
1098
- log(chalk2.green(` Examples: ✓ validated`));
1157
+ log(chalk3.green(` Examples: ✓ validated`));
1099
1158
  }
1100
1159
  }
1101
1160
  if (errorCount > 0 || warnCount > 0) {
@@ -1117,24 +1176,24 @@ function registerCheckCommand(program, dependencies = {}) {
1117
1176
  thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
1118
1177
  }
1119
1178
  if (thresholdParts.length > 0) {
1120
- log(chalk2.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1179
+ log(chalk3.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1121
1180
  } else {
1122
- log(chalk2.green("✓ Check passed"));
1123
- log(chalk2.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
1181
+ log(chalk3.green("✓ Check passed"));
1182
+ log(chalk3.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
1124
1183
  }
1125
1184
  return;
1126
1185
  }
1127
1186
  if (hasQualityErrors) {
1128
- log(chalk2.red(`✗ ${errorCount} quality errors`));
1187
+ log(chalk3.red(`✗ ${errorCount} quality errors`));
1129
1188
  }
1130
1189
  if (hasTypecheckErrors) {
1131
- log(chalk2.red(`✗ ${typecheckErrors.length} example type errors`));
1190
+ log(chalk3.red(`✗ ${typecheckErrors.length} example type errors`));
1132
1191
  }
1133
1192
  log("");
1134
- log(chalk2.dim("Use --format json or --format markdown for detailed reports"));
1193
+ log(chalk3.dim("Use --format json or --format markdown for detailed reports"));
1135
1194
  process.exit(1);
1136
1195
  } catch (commandError) {
1137
- error(chalk2.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1196
+ error(chalk3.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1138
1197
  process.exit(1);
1139
1198
  }
1140
1199
  });
@@ -1171,7 +1230,7 @@ import {
1171
1230
  hashString,
1172
1231
  parseMarkdownFiles
1173
1232
  } from "@doccov/sdk";
1174
- import chalk3 from "chalk";
1233
+ import chalk4 from "chalk";
1175
1234
  import { glob } from "glob";
1176
1235
 
1177
1236
  // src/utils/docs-impact-ai.ts
@@ -1317,8 +1376,8 @@ function registerDiffCommand(program, dependencies = {}) {
1317
1376
  silent: true
1318
1377
  });
1319
1378
  }
1320
- const cacheNote = fromCache ? chalk3.cyan(" (cached)") : "";
1321
- log(chalk3.dim(`Report: ${jsonPath}`) + cacheNote);
1379
+ const cacheNote = fromCache ? chalk4.cyan(" (cached)") : "";
1380
+ log(chalk4.dim(`Report: ${jsonPath}`) + cacheNote);
1322
1381
  }
1323
1382
  break;
1324
1383
  case "json": {
@@ -1376,18 +1435,18 @@ function registerDiffCommand(program, dependencies = {}) {
1376
1435
  checks
1377
1436
  });
1378
1437
  if (failures.length > 0) {
1379
- log(chalk3.red(`
1438
+ log(chalk4.red(`
1380
1439
  ✗ Check failed`));
1381
1440
  for (const f of failures) {
1382
- log(chalk3.red(` - ${f}`));
1441
+ log(chalk4.red(` - ${f}`));
1383
1442
  }
1384
1443
  process.exitCode = 1;
1385
1444
  } else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
1386
- log(chalk3.green(`
1445
+ log(chalk4.green(`
1387
1446
  ✓ All checks passed`));
1388
1447
  }
1389
1448
  } catch (commandError) {
1390
- error(chalk3.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1449
+ error(chalk4.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1391
1450
  process.exitCode = 1;
1392
1451
  }
1393
1452
  });
@@ -1414,7 +1473,7 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
1414
1473
  if (!docsPatterns || docsPatterns.length === 0) {
1415
1474
  if (config?.docs?.include) {
1416
1475
  docsPatterns = config.docs.include;
1417
- log(chalk3.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
1476
+ log(chalk4.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
1418
1477
  }
1419
1478
  }
1420
1479
  if (docsPatterns && docsPatterns.length > 0) {
@@ -1437,46 +1496,46 @@ function loadSpec(filePath, readFileSync2) {
1437
1496
  }
1438
1497
  function printSummary(diff, baseName, headName, fromCache, log) {
1439
1498
  log("");
1440
- const cacheIndicator = fromCache ? chalk3.cyan(" (cached)") : "";
1441
- log(chalk3.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
1499
+ const cacheIndicator = fromCache ? chalk4.cyan(" (cached)") : "";
1500
+ log(chalk4.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
1442
1501
  log("─".repeat(40));
1443
1502
  log("");
1444
- const coverageColor = diff.coverageDelta > 0 ? chalk3.green : diff.coverageDelta < 0 ? chalk3.red : chalk3.gray;
1503
+ const coverageColor = diff.coverageDelta > 0 ? chalk4.green : diff.coverageDelta < 0 ? chalk4.red : chalk4.gray;
1445
1504
  const coverageSign = diff.coverageDelta > 0 ? "+" : "";
1446
1505
  log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
1447
1506
  const breakingCount = diff.breaking.length;
1448
1507
  const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
1449
1508
  if (breakingCount > 0) {
1450
- const severityNote = highSeverity > 0 ? chalk3.red(` (${highSeverity} high severity)`) : "";
1451
- log(` Breaking: ${chalk3.red(breakingCount)} changes${severityNote}`);
1509
+ const severityNote = highSeverity > 0 ? chalk4.red(` (${highSeverity} high severity)`) : "";
1510
+ log(` Breaking: ${chalk4.red(breakingCount)} changes${severityNote}`);
1452
1511
  } else {
1453
- log(` Breaking: ${chalk3.green("0")} changes`);
1512
+ log(` Breaking: ${chalk4.green("0")} changes`);
1454
1513
  }
1455
1514
  const newCount = diff.nonBreaking.length;
1456
1515
  const undocCount = diff.newUndocumented.length;
1457
1516
  if (newCount > 0) {
1458
- const undocNote = undocCount > 0 ? chalk3.yellow(` (${undocCount} undocumented)`) : "";
1459
- log(` New: ${chalk3.green(newCount)} exports${undocNote}`);
1517
+ const undocNote = undocCount > 0 ? chalk4.yellow(` (${undocCount} undocumented)`) : "";
1518
+ log(` New: ${chalk4.green(newCount)} exports${undocNote}`);
1460
1519
  }
1461
1520
  if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
1462
1521
  const parts = [];
1463
1522
  if (diff.driftIntroduced > 0)
1464
- parts.push(chalk3.red(`+${diff.driftIntroduced}`));
1523
+ parts.push(chalk4.red(`+${diff.driftIntroduced}`));
1465
1524
  if (diff.driftResolved > 0)
1466
- parts.push(chalk3.green(`-${diff.driftResolved}`));
1525
+ parts.push(chalk4.green(`-${diff.driftResolved}`));
1467
1526
  log(` Drift: ${parts.join(", ")}`);
1468
1527
  }
1469
1528
  log("");
1470
1529
  }
1471
1530
  async function printAISummary(diff, log) {
1472
1531
  if (!isAIDocsAnalysisAvailable()) {
1473
- log(chalk3.yellow(`
1532
+ log(chalk4.yellow(`
1474
1533
  ⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
1475
1534
  return;
1476
1535
  }
1477
1536
  if (!diff.docsImpact)
1478
1537
  return;
1479
- log(chalk3.gray(`
1538
+ log(chalk4.gray(`
1480
1539
  Generating AI summary...`));
1481
1540
  const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
1482
1541
  file: f.file,
@@ -1487,8 +1546,8 @@ Generating AI summary...`));
1487
1546
  const summary = await generateImpactSummary(impacts);
1488
1547
  if (summary) {
1489
1548
  log("");
1490
- log(chalk3.bold("AI Summary"));
1491
- log(chalk3.cyan(` ${summary}`));
1549
+ log(chalk4.bold("AI Summary"));
1550
+ log(chalk4.cyan(` ${summary}`));
1492
1551
  }
1493
1552
  }
1494
1553
  function validateDiff(diff, headSpec, options) {
@@ -1569,7 +1628,7 @@ function printGitHubAnnotations(diff, log) {
1569
1628
 
1570
1629
  // src/commands/info.ts
1571
1630
  import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
1572
- import chalk4 from "chalk";
1631
+ import chalk5 from "chalk";
1573
1632
  function registerInfoCommand(program) {
1574
1633
  program.command("info [entry]").description("Show brief documentation coverage summary").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--skip-resolve", "Skip external type resolution from node_modules").action(async (entry, options) => {
1575
1634
  try {
@@ -1591,14 +1650,14 @@ function registerInfoCommand(program) {
1591
1650
  const spec = enrichSpec2(specResult.spec);
1592
1651
  const stats = computeStats(spec);
1593
1652
  console.log("");
1594
- console.log(chalk4.bold(`${stats.packageName}@${stats.version}`));
1653
+ console.log(chalk5.bold(`${stats.packageName}@${stats.version}`));
1595
1654
  console.log("");
1596
- console.log(` Exports: ${chalk4.bold(stats.totalExports.toString())}`);
1597
- console.log(` Coverage: ${chalk4.bold(`${stats.coverageScore}%`)}`);
1598
- console.log(` Drift: ${chalk4.bold(`${stats.driftScore}%`)}`);
1655
+ console.log(` Exports: ${chalk5.bold(stats.totalExports.toString())}`);
1656
+ console.log(` Coverage: ${chalk5.bold(`${stats.coverageScore}%`)}`);
1657
+ console.log(` Drift: ${chalk5.bold(`${stats.driftScore}%`)}`);
1599
1658
  console.log("");
1600
1659
  } catch (err) {
1601
- console.error(chalk4.red("Error:"), err instanceof Error ? err.message : err);
1660
+ console.error(chalk5.red("Error:"), err instanceof Error ? err.message : err);
1602
1661
  process.exit(1);
1603
1662
  }
1604
1663
  });
@@ -1607,7 +1666,7 @@ function registerInfoCommand(program) {
1607
1666
  // src/commands/init.ts
1608
1667
  import * as fs4 from "node:fs";
1609
1668
  import * as path6 from "node:path";
1610
- import chalk5 from "chalk";
1669
+ import chalk6 from "chalk";
1611
1670
  var defaultDependencies3 = {
1612
1671
  fileExists: fs4.existsSync,
1613
1672
  writeFileSync: fs4.writeFileSync,
@@ -1624,31 +1683,31 @@ function registerInitCommand(program, dependencies = {}) {
1624
1683
  const cwd = path6.resolve(options.cwd);
1625
1684
  const formatOption = String(options.format ?? "auto").toLowerCase();
1626
1685
  if (!isValidFormat(formatOption)) {
1627
- error(chalk5.red(`Invalid format "${formatOption}". Use auto, mjs, js, or cjs.`));
1686
+ error(chalk6.red(`Invalid format "${formatOption}". Use auto, mjs, js, or cjs.`));
1628
1687
  process.exitCode = 1;
1629
1688
  return;
1630
1689
  }
1631
1690
  const existing = findExistingConfig(cwd, fileExists2);
1632
1691
  if (existing) {
1633
- error(chalk5.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
1692
+ error(chalk6.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
1634
1693
  process.exitCode = 1;
1635
1694
  return;
1636
1695
  }
1637
1696
  const packageType = detectPackageType(cwd, fileExists2, readFileSync3);
1638
1697
  const targetFormat = resolveFormat(formatOption, packageType);
1639
1698
  if (targetFormat === "js" && packageType !== "module") {
1640
- log(chalk5.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
1699
+ log(chalk6.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
1641
1700
  }
1642
1701
  const fileName = `doccov.config.${targetFormat}`;
1643
1702
  const outputPath = path6.join(cwd, fileName);
1644
1703
  if (fileExists2(outputPath)) {
1645
- error(chalk5.red(`Cannot create ${fileName}; file already exists.`));
1704
+ error(chalk6.red(`Cannot create ${fileName}; file already exists.`));
1646
1705
  process.exitCode = 1;
1647
1706
  return;
1648
1707
  }
1649
1708
  const template = buildTemplate(targetFormat);
1650
1709
  writeFileSync3(outputPath, template, { encoding: "utf8" });
1651
- log(chalk5.green(`✓ Created ${path6.relative(process.cwd(), outputPath)}`));
1710
+ log(chalk6.green(`✓ Created ${path6.relative(process.cwd(), outputPath)}`));
1652
1711
  });
1653
1712
  }
1654
1713
  var isValidFormat = (value) => {
@@ -1762,16 +1821,14 @@ import * as fs5 from "node:fs";
1762
1821
  import * as path7 from "node:path";
1763
1822
  import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, resolveTarget as resolveTarget3 } from "@doccov/sdk";
1764
1823
  import { normalize, validateSpec } from "@openpkg-ts/spec";
1824
+ import chalk8 from "chalk";
1765
1825
  // package.json
1766
- var version = "0.13.0";
1767
-
1768
- // src/commands/spec.ts
1769
- import chalk7 from "chalk";
1826
+ var version = "0.14.0";
1770
1827
 
1771
1828
  // src/utils/filter-options.ts
1772
1829
  import { mergeFilters, parseListFlag } from "@doccov/sdk";
1773
- import chalk6 from "chalk";
1774
- var formatList = (label, values) => `${label}: ${values.map((value) => chalk6.cyan(value)).join(", ")}`;
1830
+ import chalk7 from "chalk";
1831
+ var formatList = (label, values) => `${label}: ${values.map((value) => chalk7.cyan(value)).join(", ")}`;
1775
1832
  var mergeFilterOptions = (config, cliOptions) => {
1776
1833
  const messages = [];
1777
1834
  if (config?.include) {
@@ -1812,7 +1869,7 @@ function getArrayLength(value) {
1812
1869
  function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
1813
1870
  const location = diagnostic.location;
1814
1871
  const relativePath = location?.file ? path7.relative(baseDir, location.file) || location.file : undefined;
1815
- const locationText = location && relativePath ? chalk7.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
1872
+ const locationText = location && relativePath ? chalk8.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
1816
1873
  const locationPrefix = locationText ? `${locationText} ` : "";
1817
1874
  return `${prefix} ${locationPrefix}${diagnostic.message}`;
1818
1875
  }
@@ -1823,6 +1880,14 @@ function registerSpecCommand(program, dependencies = {}) {
1823
1880
  };
1824
1881
  program.command("spec [entry]").description("Generate OpenPkg specification (JSON)").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <file>", "Output file path", "openpkg.json").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").option("--no-cache", "Bypass spec cache and force regeneration").option("--show-diagnostics", "Show TypeScript compiler diagnostics").option("--verbose", "Show detailed generation metadata").action(async (entry, options) => {
1825
1882
  try {
1883
+ const steps = new StepProgress([
1884
+ { label: "Resolved target", activeLabel: "Resolving target" },
1885
+ { label: "Loaded config", activeLabel: "Loading config" },
1886
+ { label: "Generated spec", activeLabel: "Generating spec" },
1887
+ { label: "Validated schema", activeLabel: "Validating schema" },
1888
+ { label: "Wrote output", activeLabel: "Writing output" }
1889
+ ]);
1890
+ steps.start();
1826
1891
  const fileSystem = new NodeFileSystem3(options.cwd);
1827
1892
  const resolved = await resolveTarget3(fileSystem, {
1828
1893
  cwd: options.cwd,
@@ -1830,135 +1895,111 @@ function registerSpecCommand(program, dependencies = {}) {
1830
1895
  entry
1831
1896
  });
1832
1897
  const { targetDir, entryFile, packageInfo, entryPointInfo } = resolved;
1833
- if (packageInfo) {
1834
- log(chalk7.gray(`Found package at ${packageInfo.path}`));
1835
- }
1836
- if (!entry) {
1837
- log(chalk7.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
1838
- }
1898
+ steps.next();
1839
1899
  let config = null;
1840
1900
  try {
1841
1901
  config = await loadDocCovConfig(targetDir);
1842
- if (config?.filePath) {
1843
- log(chalk7.gray(`Loaded configuration from ${path7.relative(targetDir, config.filePath)}`));
1844
- }
1845
1902
  } catch (configError) {
1846
- error(chalk7.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
1903
+ error(chalk8.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
1847
1904
  process.exit(1);
1848
1905
  }
1906
+ steps.next();
1849
1907
  const cliFilters = {
1850
1908
  include: parseListFlag(options.include),
1851
1909
  exclude: parseListFlag(options.exclude)
1852
1910
  };
1853
1911
  const resolvedFilters = mergeFilterOptions(config, cliFilters);
1854
- for (const message of resolvedFilters.messages) {
1855
- log(chalk7.gray(`${message}`));
1856
- }
1857
1912
  const resolveExternalTypes = !options.skipResolve;
1858
- process.stdout.write(chalk7.cyan(`> Generating OpenPkg spec...
1859
- `));
1860
- let result;
1861
- try {
1862
- const doccov = createDocCov({
1863
- resolveExternalTypes,
1864
- maxDepth: options.maxTypeDepth ? parseInt(options.maxTypeDepth, 10) : undefined,
1865
- useCache: options.cache !== false,
1866
- cwd: options.cwd
1867
- });
1868
- const generationInput = {
1869
- entryPoint: path7.relative(targetDir, entryFile),
1870
- entryPointSource: entryPointInfo.source,
1871
- isDeclarationOnly: entryPointInfo.isDeclarationOnly ?? false,
1872
- generatorName: "@doccov/cli",
1873
- generatorVersion: version,
1874
- packageManager: packageInfo?.packageManager,
1875
- isMonorepo: resolved.isMonorepo,
1876
- targetPackage: packageInfo?.name
1877
- };
1878
- const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
1879
- filters: {
1880
- include: resolvedFilters.include,
1881
- exclude: resolvedFilters.exclude
1882
- },
1883
- generationInput
1884
- } : { generationInput };
1885
- result = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
1886
- if (result.fromCache) {
1887
- process.stdout.write(chalk7.gray(`> Using cached spec
1888
- `));
1889
- } else {
1890
- process.stdout.write(chalk7.green(`> Generated OpenPkg spec
1891
- `));
1892
- }
1893
- } catch (generationError) {
1894
- process.stdout.write(chalk7.red(`> Failed to generate spec
1895
- `));
1896
- throw generationError;
1897
- }
1913
+ const doccov = createDocCov({
1914
+ resolveExternalTypes,
1915
+ maxDepth: options.maxTypeDepth ? parseInt(options.maxTypeDepth, 10) : undefined,
1916
+ useCache: options.cache !== false,
1917
+ cwd: options.cwd
1918
+ });
1919
+ const generationInput = {
1920
+ entryPoint: path7.relative(targetDir, entryFile),
1921
+ entryPointSource: entryPointInfo.source,
1922
+ isDeclarationOnly: entryPointInfo.isDeclarationOnly ?? false,
1923
+ generatorName: "@doccov/cli",
1924
+ generatorVersion: version,
1925
+ packageManager: packageInfo?.packageManager,
1926
+ isMonorepo: resolved.isMonorepo,
1927
+ targetPackage: packageInfo?.name
1928
+ };
1929
+ const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
1930
+ filters: {
1931
+ include: resolvedFilters.include,
1932
+ exclude: resolvedFilters.exclude
1933
+ },
1934
+ generationInput
1935
+ } : { generationInput };
1936
+ const result = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
1898
1937
  if (!result) {
1899
1938
  throw new Error("Failed to produce an OpenPkg spec.");
1900
1939
  }
1940
+ steps.next();
1901
1941
  const normalized = normalize(result.spec);
1902
1942
  const validation = validateSpec(normalized);
1903
1943
  if (!validation.ok) {
1904
- error(chalk7.red("Spec failed schema validation"));
1944
+ error(chalk8.red("Spec failed schema validation"));
1905
1945
  for (const err of validation.errors) {
1906
- error(chalk7.red(`schema: ${err.instancePath || "/"} ${err.message}`));
1946
+ error(chalk8.red(`schema: ${err.instancePath || "/"} ${err.message}`));
1907
1947
  }
1908
1948
  process.exit(1);
1909
1949
  }
1950
+ steps.next();
1910
1951
  const outputPath = path7.resolve(process.cwd(), options.output);
1911
1952
  writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
1912
- log(chalk7.green(`> Wrote ${options.output}`));
1913
- log(chalk7.gray(` ${getArrayLength(normalized.exports)} exports`));
1914
- log(chalk7.gray(` ${getArrayLength(normalized.types)} types`));
1953
+ steps.complete(`Generated ${options.output}`);
1954
+ log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
1955
+ log(chalk8.gray(` ${getArrayLength(normalized.types)} types`));
1915
1956
  if (options.verbose && normalized.generation) {
1916
1957
  const gen = normalized.generation;
1917
1958
  log("");
1918
- log(chalk7.bold("Generation Info"));
1919
- log(chalk7.gray(` Timestamp: ${gen.timestamp}`));
1920
- log(chalk7.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
1921
- log(chalk7.gray(` Entry point: ${gen.analysis.entryPoint}`));
1922
- log(chalk7.gray(` Detected via: ${gen.analysis.entryPointSource}`));
1923
- log(chalk7.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
1924
- log(chalk7.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
1959
+ log(chalk8.bold("Generation Info"));
1960
+ log(chalk8.gray(` Timestamp: ${gen.timestamp}`));
1961
+ log(chalk8.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
1962
+ log(chalk8.gray(` Entry point: ${gen.analysis.entryPoint}`));
1963
+ log(chalk8.gray(` Detected via: ${gen.analysis.entryPointSource}`));
1964
+ log(chalk8.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
1965
+ log(chalk8.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
1925
1966
  if (gen.analysis.maxTypeDepth) {
1926
- log(chalk7.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
1967
+ log(chalk8.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
1927
1968
  }
1928
1969
  log("");
1929
- log(chalk7.bold("Environment"));
1930
- log(chalk7.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
1970
+ log(chalk8.bold("Environment"));
1971
+ log(chalk8.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
1931
1972
  if (gen.environment.packageManager) {
1932
- log(chalk7.gray(` Package manager: ${gen.environment.packageManager}`));
1973
+ log(chalk8.gray(` Package manager: ${gen.environment.packageManager}`));
1933
1974
  }
1934
1975
  if (gen.environment.isMonorepo) {
1935
- log(chalk7.gray(` Monorepo: yes`));
1976
+ log(chalk8.gray(` Monorepo: yes`));
1936
1977
  }
1937
1978
  if (gen.environment.targetPackage) {
1938
- log(chalk7.gray(` Target package: ${gen.environment.targetPackage}`));
1979
+ log(chalk8.gray(` Target package: ${gen.environment.targetPackage}`));
1939
1980
  }
1940
1981
  if (gen.issues.length > 0) {
1941
1982
  log("");
1942
- log(chalk7.bold("Issues"));
1983
+ log(chalk8.bold("Issues"));
1943
1984
  for (const issue of gen.issues) {
1944
- const prefix = issue.severity === "error" ? chalk7.red(">") : issue.severity === "warning" ? chalk7.yellow(">") : chalk7.cyan(">");
1985
+ const prefix = issue.severity === "error" ? chalk8.red(">") : issue.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
1945
1986
  log(`${prefix} [${issue.code}] ${issue.message}`);
1946
1987
  if (issue.suggestion) {
1947
- log(chalk7.gray(` ${issue.suggestion}`));
1988
+ log(chalk8.gray(` ${issue.suggestion}`));
1948
1989
  }
1949
1990
  }
1950
1991
  }
1951
1992
  }
1952
1993
  if (options.showDiagnostics && result.diagnostics.length > 0) {
1953
1994
  log("");
1954
- log(chalk7.bold("Diagnostics"));
1995
+ log(chalk8.bold("Diagnostics"));
1955
1996
  for (const diagnostic of result.diagnostics) {
1956
- const prefix = diagnostic.severity === "error" ? chalk7.red(">") : diagnostic.severity === "warning" ? chalk7.yellow(">") : chalk7.cyan(">");
1997
+ const prefix = diagnostic.severity === "error" ? chalk8.red(">") : diagnostic.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
1957
1998
  log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
1958
1999
  }
1959
2000
  }
1960
2001
  } catch (commandError) {
1961
- error(chalk7.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2002
+ error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1962
2003
  process.exit(1);
1963
2004
  }
1964
2005
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/cli",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "DocCov CLI - Documentation coverage and drift detection for TypeScript",
5
5
  "keywords": [
6
6
  "typescript",
@@ -48,14 +48,13 @@
48
48
  "dependencies": {
49
49
  "@ai-sdk/anthropic": "^1.0.0",
50
50
  "@ai-sdk/openai": "^1.0.0",
51
- "@doccov/sdk": "^0.13.0",
51
+ "@doccov/sdk": "^0.15.0",
52
52
  "@inquirer/prompts": "^7.8.0",
53
53
  "@openpkg-ts/spec": "^0.9.0",
54
54
  "ai": "^4.0.0",
55
55
  "chalk": "^5.4.1",
56
56
  "commander": "^14.0.0",
57
57
  "glob": "^11.0.0",
58
- "ora": "^9.0.0",
59
58
  "simple-git": "^3.27.0",
60
59
  "zod": "^3.25.0"
61
60
  },