@doccov/cli 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +806 -656
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -147,36 +147,358 @@ ${formatIssues(issues)}`);
|
|
|
147
147
|
var defineConfig = (config) => config;
|
|
148
148
|
// src/cli.ts
|
|
149
149
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
150
|
-
import * as
|
|
150
|
+
import * as path10 from "node:path";
|
|
151
151
|
import { fileURLToPath } from "node:url";
|
|
152
152
|
import { Command } from "commander";
|
|
153
153
|
|
|
154
|
-
// src/commands/check.ts
|
|
154
|
+
// src/commands/check/index.ts
|
|
155
|
+
import {
|
|
156
|
+
DocCov,
|
|
157
|
+
enrichSpec,
|
|
158
|
+
NodeFileSystem,
|
|
159
|
+
parseExamplesFlag,
|
|
160
|
+
resolveTarget
|
|
161
|
+
} from "@doccov/sdk";
|
|
162
|
+
import chalk6 from "chalk";
|
|
163
|
+
|
|
164
|
+
// src/utils/filter-options.ts
|
|
165
|
+
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|
|
166
|
+
import chalk from "chalk";
|
|
167
|
+
var parseVisibilityFlag = (value) => {
|
|
168
|
+
if (!value)
|
|
169
|
+
return;
|
|
170
|
+
const validTags = ["public", "beta", "alpha", "internal"];
|
|
171
|
+
const parsed = parseListFlag(value);
|
|
172
|
+
if (!parsed)
|
|
173
|
+
return;
|
|
174
|
+
const result = [];
|
|
175
|
+
for (const tag of parsed) {
|
|
176
|
+
const lower = tag.toLowerCase();
|
|
177
|
+
if (validTags.includes(lower)) {
|
|
178
|
+
result.push(lower);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return result.length > 0 ? result : undefined;
|
|
182
|
+
};
|
|
183
|
+
var formatList = (label, values) => `${label}: ${values.map((value) => chalk.cyan(value)).join(", ")}`;
|
|
184
|
+
var mergeFilterOptions = (config, cliOptions) => {
|
|
185
|
+
const messages = [];
|
|
186
|
+
if (config?.include) {
|
|
187
|
+
messages.push(formatList("include filters from config", config.include));
|
|
188
|
+
}
|
|
189
|
+
if (config?.exclude) {
|
|
190
|
+
messages.push(formatList("exclude filters from config", config.exclude));
|
|
191
|
+
}
|
|
192
|
+
if (cliOptions.include) {
|
|
193
|
+
messages.push(formatList("apply include filters from CLI", cliOptions.include));
|
|
194
|
+
}
|
|
195
|
+
if (cliOptions.exclude) {
|
|
196
|
+
messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
|
|
197
|
+
}
|
|
198
|
+
if (cliOptions.visibility) {
|
|
199
|
+
messages.push(formatList("apply visibility filter from CLI", cliOptions.visibility));
|
|
200
|
+
}
|
|
201
|
+
const resolved = mergeFilters(config, cliOptions);
|
|
202
|
+
if (!resolved.include && !resolved.exclude && !cliOptions.visibility) {
|
|
203
|
+
return { messages };
|
|
204
|
+
}
|
|
205
|
+
const source = resolved.source === "override" ? "cli" : resolved.source;
|
|
206
|
+
return {
|
|
207
|
+
include: resolved.include,
|
|
208
|
+
exclude: resolved.exclude,
|
|
209
|
+
visibility: cliOptions.visibility,
|
|
210
|
+
source,
|
|
211
|
+
messages
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/utils/progress.ts
|
|
216
|
+
import chalk2 from "chalk";
|
|
217
|
+
class StepProgress {
|
|
218
|
+
steps;
|
|
219
|
+
currentStep = 0;
|
|
220
|
+
startTime;
|
|
221
|
+
stepStartTime;
|
|
222
|
+
constructor(steps) {
|
|
223
|
+
this.steps = steps;
|
|
224
|
+
this.startTime = Date.now();
|
|
225
|
+
this.stepStartTime = Date.now();
|
|
226
|
+
}
|
|
227
|
+
start(stepIndex) {
|
|
228
|
+
this.currentStep = stepIndex ?? 0;
|
|
229
|
+
this.stepStartTime = Date.now();
|
|
230
|
+
this.render();
|
|
231
|
+
}
|
|
232
|
+
next() {
|
|
233
|
+
this.completeCurrentStep();
|
|
234
|
+
this.currentStep++;
|
|
235
|
+
if (this.currentStep < this.steps.length) {
|
|
236
|
+
this.stepStartTime = Date.now();
|
|
237
|
+
this.render();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
complete(message) {
|
|
241
|
+
this.completeCurrentStep();
|
|
242
|
+
if (message) {
|
|
243
|
+
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
244
|
+
console.log(`${chalk2.green("✓")} ${message} ${chalk2.dim(`(${elapsed}s)`)}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
render() {
|
|
248
|
+
const step = this.steps[this.currentStep];
|
|
249
|
+
if (!step)
|
|
250
|
+
return;
|
|
251
|
+
const label = step.activeLabel ?? step.label;
|
|
252
|
+
const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
|
|
253
|
+
process.stdout.write(`\r${prefix} ${chalk2.cyan(label)}...`);
|
|
254
|
+
}
|
|
255
|
+
completeCurrentStep() {
|
|
256
|
+
const step = this.steps[this.currentStep];
|
|
257
|
+
if (!step)
|
|
258
|
+
return;
|
|
259
|
+
const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
|
|
260
|
+
const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
|
|
261
|
+
process.stdout.write(`\r${" ".repeat(80)}\r`);
|
|
262
|
+
console.log(`${prefix} ${step.label} ${chalk2.green("✓")} ${chalk2.dim(`(${elapsed}s)`)}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/utils/validation.ts
|
|
267
|
+
function clampPercentage(value, fallback = 80) {
|
|
268
|
+
if (Number.isNaN(value))
|
|
269
|
+
return fallback;
|
|
270
|
+
return Math.min(100, Math.max(0, Math.round(value)));
|
|
271
|
+
}
|
|
272
|
+
function resolveThreshold(cliValue, configValue) {
|
|
273
|
+
const raw = cliValue ?? configValue;
|
|
274
|
+
return raw !== undefined ? clampPercentage(Number(raw)) : undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/commands/check/fix-handler.ts
|
|
155
278
|
import * as fs2 from "node:fs";
|
|
156
|
-
import * as
|
|
279
|
+
import * as path3 from "node:path";
|
|
157
280
|
import {
|
|
158
281
|
applyEdits,
|
|
159
282
|
categorizeDrifts,
|
|
160
283
|
createSourceFile,
|
|
161
|
-
DocCov,
|
|
162
|
-
enrichSpec,
|
|
163
284
|
findJSDocLocation,
|
|
164
285
|
generateFixesForExport,
|
|
165
|
-
generateReport,
|
|
166
286
|
mergeFixes,
|
|
167
|
-
NodeFileSystem,
|
|
168
|
-
parseExamplesFlag,
|
|
169
287
|
parseJSDocToPatch,
|
|
170
|
-
|
|
171
|
-
resolveTarget,
|
|
172
|
-
serializeJSDoc,
|
|
173
|
-
validateExamples
|
|
288
|
+
serializeJSDoc
|
|
174
289
|
} from "@doccov/sdk";
|
|
175
|
-
import
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
import
|
|
290
|
+
import chalk3 from "chalk";
|
|
291
|
+
|
|
292
|
+
// src/commands/check/utils.ts
|
|
293
|
+
import * as fs from "node:fs";
|
|
294
|
+
import * as path2 from "node:path";
|
|
295
|
+
import { parseMarkdownFiles } from "@doccov/sdk";
|
|
296
|
+
import { DRIFT_CATEGORIES } from "@openpkg-ts/spec";
|
|
179
297
|
import { glob } from "glob";
|
|
298
|
+
function collectDriftsFromExports(exports) {
|
|
299
|
+
const results = [];
|
|
300
|
+
for (const exp of exports) {
|
|
301
|
+
for (const drift of exp.docs?.drift ?? []) {
|
|
302
|
+
results.push({ export: exp, drift });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return results;
|
|
306
|
+
}
|
|
307
|
+
function groupByExport(drifts) {
|
|
308
|
+
const map = new Map;
|
|
309
|
+
for (const { export: exp, drift } of drifts) {
|
|
310
|
+
const existing = map.get(exp) ?? [];
|
|
311
|
+
existing.push(drift);
|
|
312
|
+
map.set(exp, existing);
|
|
313
|
+
}
|
|
314
|
+
return map;
|
|
315
|
+
}
|
|
316
|
+
function collectDrift(exportsList) {
|
|
317
|
+
const drifts = [];
|
|
318
|
+
for (const entry of exportsList) {
|
|
319
|
+
const drift = entry.docs?.drift;
|
|
320
|
+
if (!drift || drift.length === 0) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
for (const d of drift) {
|
|
324
|
+
drifts.push({
|
|
325
|
+
name: entry.name,
|
|
326
|
+
type: d.type,
|
|
327
|
+
issue: d.issue ?? "Documentation drift detected.",
|
|
328
|
+
suggestion: d.suggestion,
|
|
329
|
+
category: DRIFT_CATEGORIES[d.type]
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return drifts;
|
|
334
|
+
}
|
|
335
|
+
function collect(value, previous) {
|
|
336
|
+
return previous.concat([value]);
|
|
337
|
+
}
|
|
338
|
+
async function loadMarkdownFiles(patterns, cwd) {
|
|
339
|
+
const files = [];
|
|
340
|
+
for (const pattern of patterns) {
|
|
341
|
+
const matches = await glob(pattern, { nodir: true, cwd });
|
|
342
|
+
for (const filePath of matches) {
|
|
343
|
+
try {
|
|
344
|
+
const fullPath = path2.resolve(cwd, filePath);
|
|
345
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
346
|
+
files.push({ path: filePath, content });
|
|
347
|
+
} catch {}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return parseMarkdownFiles(files);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/commands/check/fix-handler.ts
|
|
354
|
+
async function handleFixes(spec, options, deps) {
|
|
355
|
+
const { isPreview, targetDir } = options;
|
|
356
|
+
const { log, error } = deps;
|
|
357
|
+
const fixedDriftKeys = new Set;
|
|
358
|
+
const allDrifts = collectDriftsFromExports(spec.exports ?? []);
|
|
359
|
+
if (allDrifts.length === 0) {
|
|
360
|
+
return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
|
|
361
|
+
}
|
|
362
|
+
const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
|
|
363
|
+
if (fixable.length === 0) {
|
|
364
|
+
log(chalk3.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
|
|
365
|
+
return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
|
|
366
|
+
}
|
|
367
|
+
log("");
|
|
368
|
+
log(chalk3.bold(`Found ${fixable.length} fixable issue(s)`));
|
|
369
|
+
if (nonFixable.length > 0) {
|
|
370
|
+
log(chalk3.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
|
|
371
|
+
}
|
|
372
|
+
log("");
|
|
373
|
+
const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
|
|
374
|
+
const edits = [];
|
|
375
|
+
const editsByFile = new Map;
|
|
376
|
+
for (const [exp, drifts] of groupedDrifts) {
|
|
377
|
+
const edit = generateEditForExport(exp, drifts, targetDir, log);
|
|
378
|
+
if (!edit)
|
|
379
|
+
continue;
|
|
380
|
+
for (const drift of drifts) {
|
|
381
|
+
fixedDriftKeys.add(`${exp.name}:${drift.issue}`);
|
|
382
|
+
}
|
|
383
|
+
edits.push(edit.edit);
|
|
384
|
+
const fileEdits = editsByFile.get(edit.filePath) ?? [];
|
|
385
|
+
fileEdits.push({
|
|
386
|
+
export: exp,
|
|
387
|
+
edit: edit.edit,
|
|
388
|
+
fixes: edit.fixes,
|
|
389
|
+
existingPatch: edit.existingPatch
|
|
390
|
+
});
|
|
391
|
+
editsByFile.set(edit.filePath, fileEdits);
|
|
392
|
+
}
|
|
393
|
+
if (edits.length === 0) {
|
|
394
|
+
return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
|
|
395
|
+
}
|
|
396
|
+
if (isPreview) {
|
|
397
|
+
displayPreview(editsByFile, targetDir, log);
|
|
398
|
+
return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
|
|
399
|
+
}
|
|
400
|
+
const applyResult = await applyEdits(edits);
|
|
401
|
+
if (applyResult.errors.length > 0) {
|
|
402
|
+
for (const err of applyResult.errors) {
|
|
403
|
+
error(chalk3.red(` ${err.file}: ${err.error}`));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const totalFixes = Array.from(editsByFile.values()).reduce((sum, fileEdits) => sum + fileEdits.reduce((s, e) => s + e.fixes.length, 0), 0);
|
|
407
|
+
log("");
|
|
408
|
+
log(chalk3.green(`✓ Applied ${totalFixes} fix(es) to ${applyResult.filesModified} file(s)`));
|
|
409
|
+
for (const [filePath, fileEdits] of editsByFile) {
|
|
410
|
+
const relativePath = path3.relative(targetDir, filePath);
|
|
411
|
+
const fixCount = fileEdits.reduce((s, e) => s + e.fixes.length, 0);
|
|
412
|
+
log(chalk3.dim(` ${relativePath} (${fixCount} fixes)`));
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
fixedDriftKeys,
|
|
416
|
+
editsApplied: totalFixes,
|
|
417
|
+
filesModified: applyResult.filesModified
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function generateEditForExport(exp, drifts, targetDir, log) {
|
|
421
|
+
if (!exp.source?.file) {
|
|
422
|
+
log(chalk3.gray(` Skipping ${exp.name}: no source location`));
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
if (exp.source.file.endsWith(".d.ts")) {
|
|
426
|
+
log(chalk3.gray(` Skipping ${exp.name}: declaration file`));
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
const filePath = path3.resolve(targetDir, exp.source.file);
|
|
430
|
+
if (!fs2.existsSync(filePath)) {
|
|
431
|
+
log(chalk3.gray(` Skipping ${exp.name}: file not found`));
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const sourceFile = createSourceFile(filePath);
|
|
435
|
+
const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
|
|
436
|
+
if (!location) {
|
|
437
|
+
log(chalk3.gray(` Skipping ${exp.name}: could not find declaration`));
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
let existingPatch = {};
|
|
441
|
+
if (location.hasExisting && location.existingJSDoc) {
|
|
442
|
+
existingPatch = parseJSDocToPatch(location.existingJSDoc);
|
|
443
|
+
}
|
|
444
|
+
const expWithDrift = { ...exp, docs: { ...exp.docs, drift: drifts } };
|
|
445
|
+
const fixes = generateFixesForExport(expWithDrift, existingPatch);
|
|
446
|
+
if (fixes.length === 0)
|
|
447
|
+
return null;
|
|
448
|
+
const mergedPatch = mergeFixes(fixes, existingPatch);
|
|
449
|
+
const newJSDoc = serializeJSDoc(mergedPatch, location.indent);
|
|
450
|
+
const edit = {
|
|
451
|
+
filePath,
|
|
452
|
+
symbolName: exp.name,
|
|
453
|
+
startLine: location.startLine,
|
|
454
|
+
endLine: location.endLine,
|
|
455
|
+
hasExisting: location.hasExisting,
|
|
456
|
+
existingJSDoc: location.existingJSDoc,
|
|
457
|
+
newJSDoc,
|
|
458
|
+
indent: location.indent
|
|
459
|
+
};
|
|
460
|
+
return { filePath, edit, fixes, existingPatch };
|
|
461
|
+
}
|
|
462
|
+
function displayPreview(editsByFile, targetDir, log) {
|
|
463
|
+
log(chalk3.bold("Preview - changes that would be made:"));
|
|
464
|
+
log("");
|
|
465
|
+
for (const [filePath, fileEdits] of editsByFile) {
|
|
466
|
+
const relativePath = path3.relative(targetDir, filePath);
|
|
467
|
+
for (const { export: exp, edit, fixes } of fileEdits) {
|
|
468
|
+
log(chalk3.cyan(`${relativePath}:${edit.startLine + 1}`));
|
|
469
|
+
log(chalk3.bold(` ${exp.name}`));
|
|
470
|
+
log("");
|
|
471
|
+
if (edit.hasExisting && edit.existingJSDoc) {
|
|
472
|
+
const oldLines = edit.existingJSDoc.split(`
|
|
473
|
+
`);
|
|
474
|
+
const newLines = edit.newJSDoc.split(`
|
|
475
|
+
`);
|
|
476
|
+
for (const line of oldLines) {
|
|
477
|
+
log(chalk3.red(` - ${line}`));
|
|
478
|
+
}
|
|
479
|
+
for (const line of newLines) {
|
|
480
|
+
log(chalk3.green(` + ${line}`));
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
const newLines = edit.newJSDoc.split(`
|
|
484
|
+
`);
|
|
485
|
+
for (const line of newLines) {
|
|
486
|
+
log(chalk3.green(` + ${line}`));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
log("");
|
|
490
|
+
log(chalk3.dim(` Fixes: ${fixes.map((f) => f.description).join(", ")}`));
|
|
491
|
+
log("");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const totalFixes = Array.from(editsByFile.values()).reduce((sum, fileEdits) => sum + fileEdits.reduce((s, e) => s + e.fixes.length, 0), 0);
|
|
495
|
+
log(chalk3.yellow(`${totalFixes} fix(es) across ${editsByFile.size} file(s) would be applied.`));
|
|
496
|
+
log(chalk3.gray("Run with --fix to apply these changes."));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/commands/check/output.ts
|
|
500
|
+
import { generateReport } from "@doccov/sdk";
|
|
501
|
+
import chalk5 from "chalk";
|
|
180
502
|
|
|
181
503
|
// src/reports/changelog-renderer.ts
|
|
182
504
|
function renderChangelog(data, options = {}) {
|
|
@@ -246,7 +568,7 @@ function renderChangelog(data, options = {}) {
|
|
|
246
568
|
`);
|
|
247
569
|
}
|
|
248
570
|
// src/reports/diff-markdown.ts
|
|
249
|
-
import * as
|
|
571
|
+
import * as path4 from "node:path";
|
|
250
572
|
function bar(pct, width = 10) {
|
|
251
573
|
const filled = Math.round(pct / 100 * width);
|
|
252
574
|
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
@@ -397,7 +719,7 @@ function renderDocsImpactSection(lines, docsImpact, limit) {
|
|
|
397
719
|
lines.push("| File | Issues | Details |");
|
|
398
720
|
lines.push("|------|--------|---------|");
|
|
399
721
|
for (const file of impactedFiles.slice(0, limit)) {
|
|
400
|
-
const filename =
|
|
722
|
+
const filename = path4.basename(file.file);
|
|
401
723
|
const issueCount = file.references.length;
|
|
402
724
|
const firstRef = file.references[0];
|
|
403
725
|
const detail = firstRef ? `L${firstRef.line}: ${firstRef.memberName ?? firstRef.exportName}` : "-";
|
|
@@ -784,8 +1106,20 @@ function buildFileLink(file, opts) {
|
|
|
784
1106
|
}
|
|
785
1107
|
return `\`${file}\``;
|
|
786
1108
|
}
|
|
1109
|
+
function getExportKeyword(kind) {
|
|
1110
|
+
switch (kind) {
|
|
1111
|
+
case "type":
|
|
1112
|
+
return "type";
|
|
1113
|
+
case "interface":
|
|
1114
|
+
return "interface";
|
|
1115
|
+
case "class":
|
|
1116
|
+
return "class";
|
|
1117
|
+
default:
|
|
1118
|
+
return "function";
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
787
1121
|
function formatExportSignature(exp) {
|
|
788
|
-
const prefix = `export ${exp.kind
|
|
1122
|
+
const prefix = `export ${getExportKeyword(exp.kind)}`;
|
|
789
1123
|
if (exp.kind === "function" && exp.signatures?.[0]) {
|
|
790
1124
|
const sig = exp.signatures[0];
|
|
791
1125
|
const params = sig.parameters?.map((p) => `${p.name}${p.required === false ? "?" : ""}`).join(", ") ?? "";
|
|
@@ -853,7 +1187,7 @@ function renderFixGuidance(diff, opts) {
|
|
|
853
1187
|
|
|
854
1188
|
**Quick fix:** Run \`npx doccov check --fix\` to auto-fix ${opts.fixableDriftCount} issue(s).` : "";
|
|
855
1189
|
sections.push(`**For doc drift:**
|
|
856
|
-
|
|
1190
|
+
Update JSDoc to match current code signatures.${fixableNote}`);
|
|
857
1191
|
}
|
|
858
1192
|
if (opts.staleDocsRefs && opts.staleDocsRefs.length > 0) {
|
|
859
1193
|
sections.push(`**For stale docs:**
|
|
@@ -888,7 +1222,7 @@ function renderDetailsTable(lines, diff) {
|
|
|
888
1222
|
// src/reports/stats.ts
|
|
889
1223
|
import { isFixableDrift } from "@doccov/sdk";
|
|
890
1224
|
import {
|
|
891
|
-
DRIFT_CATEGORIES
|
|
1225
|
+
DRIFT_CATEGORIES as DRIFT_CATEGORIES2
|
|
892
1226
|
} from "@openpkg-ts/spec";
|
|
893
1227
|
function computeStats(spec) {
|
|
894
1228
|
const exports = spec.exports ?? [];
|
|
@@ -934,7 +1268,7 @@ function computeStats(spec) {
|
|
|
934
1268
|
suggestion: d.suggestion
|
|
935
1269
|
};
|
|
936
1270
|
driftIssues.push(item);
|
|
937
|
-
const category =
|
|
1271
|
+
const category = DRIFT_CATEGORIES2[d.type] ?? "semantic";
|
|
938
1272
|
driftByCategory[category].push(item);
|
|
939
1273
|
}
|
|
940
1274
|
}
|
|
@@ -983,21 +1317,21 @@ function computeStats(spec) {
|
|
|
983
1317
|
};
|
|
984
1318
|
}
|
|
985
1319
|
// src/reports/writer.ts
|
|
986
|
-
import * as
|
|
987
|
-
import * as
|
|
1320
|
+
import * as fs3 from "node:fs";
|
|
1321
|
+
import * as path5 from "node:path";
|
|
988
1322
|
import { DEFAULT_REPORT_DIR, getReportPath } from "@doccov/sdk";
|
|
989
|
-
import
|
|
1323
|
+
import chalk4 from "chalk";
|
|
990
1324
|
function writeReport(options) {
|
|
991
1325
|
const { format, content, outputPath, cwd = process.cwd(), silent = false } = options;
|
|
992
|
-
const reportPath = outputPath ?
|
|
993
|
-
const dir =
|
|
994
|
-
if (!
|
|
995
|
-
|
|
1326
|
+
const reportPath = outputPath ? path5.resolve(cwd, outputPath) : path5.resolve(cwd, getReportPath(format));
|
|
1327
|
+
const dir = path5.dirname(reportPath);
|
|
1328
|
+
if (!fs3.existsSync(dir)) {
|
|
1329
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
996
1330
|
}
|
|
997
|
-
|
|
998
|
-
const relativePath =
|
|
1331
|
+
fs3.writeFileSync(reportPath, content);
|
|
1332
|
+
const relativePath = path5.relative(cwd, reportPath);
|
|
999
1333
|
if (!silent) {
|
|
1000
|
-
console.log(
|
|
1334
|
+
console.log(chalk4.green(`✓ Wrote ${format} report to ${relativePath}`));
|
|
1001
1335
|
}
|
|
1002
1336
|
return { path: reportPath, format, relativePath };
|
|
1003
1337
|
}
|
|
@@ -1020,149 +1354,257 @@ function writeReports(options) {
|
|
|
1020
1354
|
}));
|
|
1021
1355
|
return results;
|
|
1022
1356
|
}
|
|
1023
|
-
// src/
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1357
|
+
// src/commands/check/output.ts
|
|
1358
|
+
function displayTextOutput(options, deps) {
|
|
1359
|
+
const {
|
|
1360
|
+
spec,
|
|
1361
|
+
coverageScore,
|
|
1362
|
+
minCoverage,
|
|
1363
|
+
maxDrift,
|
|
1364
|
+
driftExports,
|
|
1365
|
+
typecheckErrors,
|
|
1366
|
+
staleRefs,
|
|
1367
|
+
exampleResult,
|
|
1368
|
+
specWarnings,
|
|
1369
|
+
specInfos
|
|
1370
|
+
} = options;
|
|
1371
|
+
const { log } = deps;
|
|
1372
|
+
const totalExportsForDrift = spec.exports?.length ?? 0;
|
|
1373
|
+
const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
|
|
1374
|
+
const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
|
|
1375
|
+
const coverageFailed = coverageScore < minCoverage;
|
|
1376
|
+
const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
|
|
1377
|
+
const hasTypecheckErrors = typecheckErrors.length > 0;
|
|
1378
|
+
if (specWarnings.length > 0 || specInfos.length > 0) {
|
|
1379
|
+
log("");
|
|
1380
|
+
for (const diag of specWarnings) {
|
|
1381
|
+
log(chalk5.yellow(`⚠ ${diag.message}`));
|
|
1382
|
+
if (diag.suggestion) {
|
|
1383
|
+
log(chalk5.gray(` ${diag.suggestion}`));
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
for (const diag of specInfos) {
|
|
1387
|
+
log(chalk5.cyan(`ℹ ${diag.message}`));
|
|
1388
|
+
if (diag.suggestion) {
|
|
1389
|
+
log(chalk5.gray(` ${diag.suggestion}`));
|
|
1390
|
+
}
|
|
1038
1391
|
}
|
|
1039
1392
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1393
|
+
const pkgName = spec.meta?.name ?? "unknown";
|
|
1394
|
+
const pkgVersion = spec.meta?.version ?? "";
|
|
1395
|
+
const totalExports = spec.exports?.length ?? 0;
|
|
1396
|
+
log("");
|
|
1397
|
+
log(chalk5.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
|
|
1398
|
+
log("");
|
|
1399
|
+
log(` Exports: ${totalExports}`);
|
|
1400
|
+
if (coverageFailed) {
|
|
1401
|
+
log(chalk5.red(` Coverage: ✗ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
|
|
1402
|
+
} else {
|
|
1403
|
+
log(chalk5.green(` Coverage: ✓ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
|
|
1050
1404
|
}
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1405
|
+
if (maxDrift !== undefined) {
|
|
1406
|
+
if (driftFailed) {
|
|
1407
|
+
log(chalk5.red(` Drift: ✗ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
|
|
1408
|
+
} else {
|
|
1409
|
+
log(chalk5.green(` Drift: ✓ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
|
|
1410
|
+
}
|
|
1411
|
+
} else {
|
|
1412
|
+
log(` Drift: ${driftScore}%`);
|
|
1413
|
+
}
|
|
1414
|
+
if (exampleResult) {
|
|
1415
|
+
const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
|
|
1416
|
+
if (typecheckCount > 0) {
|
|
1417
|
+
log(chalk5.yellow(` Examples: ${typecheckCount} type error(s)`));
|
|
1418
|
+
for (const err of typecheckErrors.slice(0, 5)) {
|
|
1419
|
+
const loc = `example[${err.error.exampleIndex}]:${err.error.line}:${err.error.column}`;
|
|
1420
|
+
log(chalk5.dim(` ${err.exportName} ${loc}`));
|
|
1421
|
+
log(chalk5.red(` ${err.error.message}`));
|
|
1422
|
+
}
|
|
1423
|
+
if (typecheckErrors.length > 5) {
|
|
1424
|
+
log(chalk5.dim(` ... and ${typecheckErrors.length - 5} more`));
|
|
1425
|
+
}
|
|
1426
|
+
} else {
|
|
1427
|
+
log(chalk5.green(` Examples: ✓ validated`));
|
|
1428
|
+
}
|
|
1053
1429
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1430
|
+
const hasStaleRefs = staleRefs.length > 0;
|
|
1431
|
+
if (hasStaleRefs) {
|
|
1432
|
+
log(chalk5.yellow(` Docs: ${staleRefs.length} stale ref(s)`));
|
|
1433
|
+
for (const ref of staleRefs.slice(0, 5)) {
|
|
1434
|
+
log(chalk5.dim(` ${ref.file}:${ref.line} - "${ref.exportName}"`));
|
|
1435
|
+
}
|
|
1436
|
+
if (staleRefs.length > 5) {
|
|
1437
|
+
log(chalk5.dim(` ... and ${staleRefs.length - 5} more`));
|
|
1438
|
+
}
|
|
1056
1439
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1440
|
+
log("");
|
|
1441
|
+
const failed = coverageFailed || driftFailed || hasTypecheckErrors || hasStaleRefs;
|
|
1442
|
+
if (!failed) {
|
|
1443
|
+
const thresholdParts = [];
|
|
1444
|
+
thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
|
|
1445
|
+
if (maxDrift !== undefined) {
|
|
1446
|
+
thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
|
|
1447
|
+
}
|
|
1448
|
+
log(chalk5.green(`✓ Check passed (${thresholdParts.join(", ")})`));
|
|
1449
|
+
return true;
|
|
1059
1450
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
return { messages };
|
|
1451
|
+
if (hasTypecheckErrors) {
|
|
1452
|
+
log(chalk5.red(`✗ ${typecheckErrors.length} example type errors`));
|
|
1063
1453
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
include: resolved.include,
|
|
1067
|
-
exclude: resolved.exclude,
|
|
1068
|
-
visibility: cliOptions.visibility,
|
|
1069
|
-
source,
|
|
1070
|
-
messages
|
|
1071
|
-
};
|
|
1072
|
-
};
|
|
1073
|
-
|
|
1074
|
-
// src/utils/progress.ts
|
|
1075
|
-
import chalk3 from "chalk";
|
|
1076
|
-
class StepProgress {
|
|
1077
|
-
steps;
|
|
1078
|
-
currentStep = 0;
|
|
1079
|
-
startTime;
|
|
1080
|
-
stepStartTime;
|
|
1081
|
-
constructor(steps) {
|
|
1082
|
-
this.steps = steps;
|
|
1083
|
-
this.startTime = Date.now();
|
|
1084
|
-
this.stepStartTime = Date.now();
|
|
1454
|
+
if (hasStaleRefs) {
|
|
1455
|
+
log(chalk5.red(`✗ ${staleRefs.length} stale references in docs`));
|
|
1085
1456
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1457
|
+
log("");
|
|
1458
|
+
log(chalk5.dim("Use --format json or --format markdown for detailed reports"));
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
function handleNonTextOutput(options, deps) {
|
|
1462
|
+
const {
|
|
1463
|
+
format,
|
|
1464
|
+
spec,
|
|
1465
|
+
rawSpec,
|
|
1466
|
+
coverageScore,
|
|
1467
|
+
minCoverage,
|
|
1468
|
+
maxDrift,
|
|
1469
|
+
driftExports,
|
|
1470
|
+
typecheckErrors,
|
|
1471
|
+
limit,
|
|
1472
|
+
stdout,
|
|
1473
|
+
outputPath,
|
|
1474
|
+
cwd
|
|
1475
|
+
} = options;
|
|
1476
|
+
const { log } = deps;
|
|
1477
|
+
const stats = computeStats(spec);
|
|
1478
|
+
const report = generateReport(rawSpec);
|
|
1479
|
+
const jsonContent = JSON.stringify(report, null, 2);
|
|
1480
|
+
let formatContent;
|
|
1481
|
+
switch (format) {
|
|
1482
|
+
case "json":
|
|
1483
|
+
formatContent = jsonContent;
|
|
1484
|
+
break;
|
|
1485
|
+
case "markdown":
|
|
1486
|
+
formatContent = renderMarkdown(stats, { limit });
|
|
1487
|
+
break;
|
|
1488
|
+
case "html":
|
|
1489
|
+
formatContent = renderHtml(stats, { limit });
|
|
1490
|
+
break;
|
|
1491
|
+
case "github":
|
|
1492
|
+
formatContent = renderGithubSummary(stats, {
|
|
1493
|
+
coverageScore,
|
|
1494
|
+
driftCount: driftExports.length
|
|
1495
|
+
});
|
|
1496
|
+
break;
|
|
1497
|
+
default:
|
|
1498
|
+
throw new Error(`Unknown format: ${format}`);
|
|
1090
1499
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1500
|
+
if (stdout) {
|
|
1501
|
+
log(formatContent);
|
|
1502
|
+
} else {
|
|
1503
|
+
writeReports({
|
|
1504
|
+
format,
|
|
1505
|
+
formatContent,
|
|
1506
|
+
jsonContent,
|
|
1507
|
+
outputPath,
|
|
1508
|
+
cwd
|
|
1509
|
+
});
|
|
1098
1510
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1511
|
+
const totalExportsForDrift = spec.exports?.length ?? 0;
|
|
1512
|
+
const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
|
|
1513
|
+
const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
|
|
1514
|
+
const coverageFailed = coverageScore < minCoverage;
|
|
1515
|
+
const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
|
|
1516
|
+
const hasTypecheckErrors = typecheckErrors.length > 0;
|
|
1517
|
+
return !(coverageFailed || driftFailed || hasTypecheckErrors);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/commands/check/validation.ts
|
|
1521
|
+
import { validateExamples } from "@doccov/sdk";
|
|
1522
|
+
async function runExampleValidation(spec, options) {
|
|
1523
|
+
const { validations, targetDir, timeout = 5000, installTimeout = 60000 } = options;
|
|
1524
|
+
const typecheckErrors = [];
|
|
1525
|
+
const runtimeDrifts = [];
|
|
1526
|
+
const result = await validateExamples(spec.exports ?? [], {
|
|
1527
|
+
validations,
|
|
1528
|
+
packagePath: targetDir,
|
|
1529
|
+
exportNames: (spec.exports ?? []).map((e) => e.name),
|
|
1530
|
+
timeout,
|
|
1531
|
+
installTimeout
|
|
1532
|
+
});
|
|
1533
|
+
if (result.typecheck) {
|
|
1534
|
+
for (const err of result.typecheck.errors) {
|
|
1535
|
+
typecheckErrors.push({
|
|
1536
|
+
exportName: err.exportName,
|
|
1537
|
+
error: err.error
|
|
1538
|
+
});
|
|
1104
1539
|
}
|
|
1105
1540
|
}
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
if (!step)
|
|
1117
|
-
return;
|
|
1118
|
-
const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
|
|
1119
|
-
const prefix = chalk3.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
|
|
1120
|
-
process.stdout.write(`\r${" ".repeat(80)}\r`);
|
|
1121
|
-
console.log(`${prefix} ${step.label} ${chalk3.green("✓")} ${chalk3.dim(`(${elapsed}s)`)}`);
|
|
1541
|
+
if (result.run) {
|
|
1542
|
+
for (const drift of result.run.drifts) {
|
|
1543
|
+
runtimeDrifts.push({
|
|
1544
|
+
name: drift.exportName,
|
|
1545
|
+
type: "example-runtime-error",
|
|
1546
|
+
issue: drift.issue,
|
|
1547
|
+
suggestion: drift.suggestion,
|
|
1548
|
+
category: "example"
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1122
1551
|
}
|
|
1552
|
+
return { result, typecheckErrors, runtimeDrifts };
|
|
1123
1553
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
if (
|
|
1128
|
-
return
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1554
|
+
async function validateMarkdownDocs(options) {
|
|
1555
|
+
const { docsPatterns, targetDir, exportNames } = options;
|
|
1556
|
+
const staleRefs = [];
|
|
1557
|
+
if (docsPatterns.length === 0) {
|
|
1558
|
+
return staleRefs;
|
|
1559
|
+
}
|
|
1560
|
+
const markdownFiles = await loadMarkdownFiles(docsPatterns, targetDir);
|
|
1561
|
+
if (markdownFiles.length === 0) {
|
|
1562
|
+
return staleRefs;
|
|
1563
|
+
}
|
|
1564
|
+
const exportSet = new Set(exportNames);
|
|
1565
|
+
for (const mdFile of markdownFiles) {
|
|
1566
|
+
for (const block of mdFile.codeBlocks) {
|
|
1567
|
+
const codeLines = block.code.split(`
|
|
1568
|
+
`);
|
|
1569
|
+
for (let i = 0;i < codeLines.length; i++) {
|
|
1570
|
+
const line = codeLines[i];
|
|
1571
|
+
const importMatch = line.match(/import\s*\{([^}]+)\}\s*from\s*['"][^'"]*['"]/);
|
|
1572
|
+
if (importMatch) {
|
|
1573
|
+
const imports = importMatch[1].split(",").map((s) => s.trim().split(/\s+/)[0]);
|
|
1574
|
+
for (const imp of imports) {
|
|
1575
|
+
if (imp && !exportSet.has(imp)) {
|
|
1576
|
+
staleRefs.push({
|
|
1577
|
+
file: mdFile.path,
|
|
1578
|
+
line: block.lineStart + i,
|
|
1579
|
+
exportName: imp,
|
|
1580
|
+
context: line.trim()
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return staleRefs;
|
|
1134
1589
|
}
|
|
1135
1590
|
|
|
1136
|
-
// src/commands/check.ts
|
|
1591
|
+
// src/commands/check/index.ts
|
|
1137
1592
|
var defaultDependencies = {
|
|
1138
1593
|
createDocCov: (options) => new DocCov(options),
|
|
1139
1594
|
log: console.log,
|
|
1140
1595
|
error: console.error
|
|
1141
1596
|
};
|
|
1142
|
-
function collectDriftsFromExports(exports) {
|
|
1143
|
-
const results = [];
|
|
1144
|
-
for (const exp of exports) {
|
|
1145
|
-
for (const drift of exp.docs?.drift ?? []) {
|
|
1146
|
-
results.push({ export: exp, drift });
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
return results;
|
|
1150
|
-
}
|
|
1151
|
-
function groupByExport(drifts) {
|
|
1152
|
-
const map = new Map;
|
|
1153
|
-
for (const { export: exp, drift } of drifts) {
|
|
1154
|
-
const existing = map.get(exp) ?? [];
|
|
1155
|
-
existing.push(drift);
|
|
1156
|
-
map.set(exp, existing);
|
|
1157
|
-
}
|
|
1158
|
-
return map;
|
|
1159
|
-
}
|
|
1160
1597
|
function registerCheckCommand(program, dependencies = {}) {
|
|
1161
1598
|
const { createDocCov, log, error } = {
|
|
1162
1599
|
...defaultDependencies,
|
|
1163
1600
|
...dependencies
|
|
1164
1601
|
};
|
|
1165
|
-
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("--docs <glob>", "Glob pattern for markdown docs to check for stale refs", collect, []).option("--fix", "Auto-fix drift issues").option("--
|
|
1602
|
+
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("--docs <glob>", "Glob pattern for markdown docs to check for stale refs", collect, []).option("--fix", "Auto-fix drift issues").option("--preview", "Preview fixes with diff output (implies --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)", (value) => {
|
|
1603
|
+
const n = parseInt(value, 10);
|
|
1604
|
+
if (Number.isNaN(n) || n < 1)
|
|
1605
|
+
throw new Error("--max-type-depth must be a positive integer");
|
|
1606
|
+
return n;
|
|
1607
|
+
}).option("--no-cache", "Bypass spec cache and force regeneration").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").action(async (entry, options) => {
|
|
1166
1608
|
try {
|
|
1167
1609
|
let validations = parseExamplesFlag(options.examples);
|
|
1168
1610
|
let hasExamples = validations.length > 0;
|
|
@@ -1206,19 +1648,18 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1206
1648
|
};
|
|
1207
1649
|
const resolvedFilters = mergeFilterOptions(config, cliFilters);
|
|
1208
1650
|
if (resolvedFilters.visibility) {
|
|
1209
|
-
log(
|
|
1651
|
+
log(chalk6.dim(`Filtering by visibility: ${resolvedFilters.visibility.join(", ")}`));
|
|
1210
1652
|
}
|
|
1211
1653
|
steps.next();
|
|
1212
1654
|
const resolveExternalTypes = !options.skipResolve;
|
|
1213
|
-
let specResult;
|
|
1214
1655
|
const doccov = createDocCov({
|
|
1215
1656
|
resolveExternalTypes,
|
|
1216
|
-
maxDepth: options.maxTypeDepth
|
|
1657
|
+
maxDepth: options.maxTypeDepth,
|
|
1217
1658
|
useCache: options.cache !== false,
|
|
1218
1659
|
cwd: options.cwd
|
|
1219
1660
|
});
|
|
1220
1661
|
const analyzeOptions = resolvedFilters.visibility ? { filters: { visibility: resolvedFilters.visibility } } : {};
|
|
1221
|
-
specResult = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
|
|
1662
|
+
const specResult = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
|
|
1222
1663
|
if (!specResult) {
|
|
1223
1664
|
throw new Error("Failed to analyze documentation coverage.");
|
|
1224
1665
|
}
|
|
@@ -1228,384 +1669,84 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1228
1669
|
steps.next();
|
|
1229
1670
|
const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
|
|
1230
1671
|
const specInfos = specResult.diagnostics.filter((d) => d.severity === "info");
|
|
1231
|
-
const isPreview = options.preview
|
|
1232
|
-
const shouldFix = options.fix ||
|
|
1672
|
+
const isPreview = options.preview;
|
|
1673
|
+
const shouldFix = options.fix || isPreview;
|
|
1233
1674
|
let exampleResult;
|
|
1234
|
-
|
|
1235
|
-
|
|
1675
|
+
let typecheckErrors = [];
|
|
1676
|
+
let runtimeDrifts = [];
|
|
1236
1677
|
if (hasExamples) {
|
|
1237
|
-
|
|
1678
|
+
const validation = await runExampleValidation(spec, {
|
|
1238
1679
|
validations,
|
|
1239
|
-
|
|
1240
|
-
exportNames: (spec.exports ?? []).map((e) => e.name),
|
|
1241
|
-
timeout: 5000,
|
|
1242
|
-
installTimeout: 60000
|
|
1680
|
+
targetDir
|
|
1243
1681
|
});
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
exportName: err.exportName,
|
|
1248
|
-
error: err.error
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
if (exampleResult.run) {
|
|
1253
|
-
for (const drift of exampleResult.run.drifts) {
|
|
1254
|
-
runtimeDrifts.push({
|
|
1255
|
-
name: drift.exportName,
|
|
1256
|
-
type: "example-runtime-error",
|
|
1257
|
-
issue: drift.issue,
|
|
1258
|
-
suggestion: drift.suggestion,
|
|
1259
|
-
category: "example"
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1682
|
+
exampleResult = validation.result;
|
|
1683
|
+
typecheckErrors = validation.typecheckErrors;
|
|
1684
|
+
runtimeDrifts = validation.runtimeDrifts;
|
|
1263
1685
|
steps.next();
|
|
1264
1686
|
}
|
|
1265
|
-
const staleRefs = [];
|
|
1266
1687
|
let docsPatterns = options.docs;
|
|
1267
1688
|
if (docsPatterns.length === 0 && config?.docs?.include) {
|
|
1268
1689
|
docsPatterns = config.docs.include;
|
|
1269
1690
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
for (const mdFile of markdownFiles) {
|
|
1276
|
-
for (const block of mdFile.codeBlocks) {
|
|
1277
|
-
const codeLines = block.code.split(`
|
|
1278
|
-
`);
|
|
1279
|
-
for (let i = 0;i < codeLines.length; i++) {
|
|
1280
|
-
const line = codeLines[i];
|
|
1281
|
-
const importMatch = line.match(/import\s*\{([^}]+)\}\s*from\s*['"][^'"]*['"]/);
|
|
1282
|
-
if (importMatch) {
|
|
1283
|
-
const imports = importMatch[1].split(",").map((s) => s.trim().split(/\s+/)[0]);
|
|
1284
|
-
for (const imp of imports) {
|
|
1285
|
-
if (imp && !exportSet.has(imp)) {
|
|
1286
|
-
staleRefs.push({
|
|
1287
|
-
file: mdFile.path,
|
|
1288
|
-
line: block.lineStart + i,
|
|
1289
|
-
exportName: imp,
|
|
1290
|
-
context: line.trim()
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1691
|
+
const staleRefs = await validateMarkdownDocs({
|
|
1692
|
+
docsPatterns,
|
|
1693
|
+
targetDir,
|
|
1694
|
+
exportNames: (spec.exports ?? []).map((e) => e.name)
|
|
1695
|
+
});
|
|
1300
1696
|
const coverageScore = spec.docs?.coverageScore ?? 0;
|
|
1301
1697
|
const allDriftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
|
|
1302
1698
|
let driftExports = hasExamples ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
|
|
1303
|
-
const fixedDriftKeys = new Set;
|
|
1304
1699
|
if (shouldFix && driftExports.length > 0) {
|
|
1305
|
-
const
|
|
1306
|
-
if (allDrifts.length > 0) {
|
|
1307
|
-
const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
|
|
1308
|
-
if (fixable.length === 0) {
|
|
1309
|
-
log(chalk4.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
|
|
1310
|
-
} else {
|
|
1311
|
-
log("");
|
|
1312
|
-
log(chalk4.bold(`Found ${fixable.length} fixable issue(s)`));
|
|
1313
|
-
if (nonFixable.length > 0) {
|
|
1314
|
-
log(chalk4.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
|
|
1315
|
-
}
|
|
1316
|
-
log("");
|
|
1317
|
-
const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
|
|
1318
|
-
const edits = [];
|
|
1319
|
-
const editsByFile = new Map;
|
|
1320
|
-
for (const [exp, drifts] of groupedDrifts) {
|
|
1321
|
-
if (!exp.source?.file) {
|
|
1322
|
-
log(chalk4.gray(` Skipping ${exp.name}: no source location`));
|
|
1323
|
-
continue;
|
|
1324
|
-
}
|
|
1325
|
-
if (exp.source.file.endsWith(".d.ts")) {
|
|
1326
|
-
log(chalk4.gray(` Skipping ${exp.name}: declaration file`));
|
|
1327
|
-
continue;
|
|
1328
|
-
}
|
|
1329
|
-
const filePath = path4.resolve(targetDir, exp.source.file);
|
|
1330
|
-
if (!fs2.existsSync(filePath)) {
|
|
1331
|
-
log(chalk4.gray(` Skipping ${exp.name}: file not found`));
|
|
1332
|
-
continue;
|
|
1333
|
-
}
|
|
1334
|
-
const sourceFile = createSourceFile(filePath);
|
|
1335
|
-
const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
|
|
1336
|
-
if (!location) {
|
|
1337
|
-
log(chalk4.gray(` Skipping ${exp.name}: could not find declaration`));
|
|
1338
|
-
continue;
|
|
1339
|
-
}
|
|
1340
|
-
let existingPatch = {};
|
|
1341
|
-
if (location.hasExisting && location.existingJSDoc) {
|
|
1342
|
-
existingPatch = parseJSDocToPatch(location.existingJSDoc);
|
|
1343
|
-
}
|
|
1344
|
-
const expWithDrift = { ...exp, docs: { ...exp.docs, drift: drifts } };
|
|
1345
|
-
const fixes = generateFixesForExport(expWithDrift, existingPatch);
|
|
1346
|
-
if (fixes.length === 0)
|
|
1347
|
-
continue;
|
|
1348
|
-
for (const drift of drifts) {
|
|
1349
|
-
fixedDriftKeys.add(`${exp.name}:${drift.issue}`);
|
|
1350
|
-
}
|
|
1351
|
-
const mergedPatch = mergeFixes(fixes, existingPatch);
|
|
1352
|
-
const newJSDoc = serializeJSDoc(mergedPatch, location.indent);
|
|
1353
|
-
const edit = {
|
|
1354
|
-
filePath,
|
|
1355
|
-
symbolName: exp.name,
|
|
1356
|
-
startLine: location.startLine,
|
|
1357
|
-
endLine: location.endLine,
|
|
1358
|
-
hasExisting: location.hasExisting,
|
|
1359
|
-
existingJSDoc: location.existingJSDoc,
|
|
1360
|
-
newJSDoc,
|
|
1361
|
-
indent: location.indent
|
|
1362
|
-
};
|
|
1363
|
-
edits.push(edit);
|
|
1364
|
-
const fileEdits = editsByFile.get(filePath) ?? [];
|
|
1365
|
-
fileEdits.push({ export: exp, edit, fixes, existingPatch });
|
|
1366
|
-
editsByFile.set(filePath, fileEdits);
|
|
1367
|
-
}
|
|
1368
|
-
if (edits.length > 0) {
|
|
1369
|
-
if (isPreview) {
|
|
1370
|
-
log(chalk4.bold("Preview - changes that would be made:"));
|
|
1371
|
-
log("");
|
|
1372
|
-
for (const [filePath, fileEdits] of editsByFile) {
|
|
1373
|
-
const relativePath = path4.relative(targetDir, filePath);
|
|
1374
|
-
for (const { export: exp, edit, fixes } of fileEdits) {
|
|
1375
|
-
log(chalk4.cyan(`${relativePath}:${edit.startLine + 1}`));
|
|
1376
|
-
log(chalk4.bold(` ${exp.name}`));
|
|
1377
|
-
log("");
|
|
1378
|
-
if (edit.hasExisting && edit.existingJSDoc) {
|
|
1379
|
-
const oldLines = edit.existingJSDoc.split(`
|
|
1380
|
-
`);
|
|
1381
|
-
const newLines = edit.newJSDoc.split(`
|
|
1382
|
-
`);
|
|
1383
|
-
for (const line of oldLines) {
|
|
1384
|
-
log(chalk4.red(` - ${line}`));
|
|
1385
|
-
}
|
|
1386
|
-
for (const line of newLines) {
|
|
1387
|
-
log(chalk4.green(` + ${line}`));
|
|
1388
|
-
}
|
|
1389
|
-
} else {
|
|
1390
|
-
const newLines = edit.newJSDoc.split(`
|
|
1391
|
-
`);
|
|
1392
|
-
for (const line of newLines) {
|
|
1393
|
-
log(chalk4.green(` + ${line}`));
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
log("");
|
|
1397
|
-
log(chalk4.dim(` Fixes: ${fixes.map((f) => f.description).join(", ")}`));
|
|
1398
|
-
log("");
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
const totalFixes = Array.from(editsByFile.values()).reduce((sum, edits2) => sum + edits2.reduce((s, e) => s + e.fixes.length, 0), 0);
|
|
1402
|
-
log(chalk4.yellow(`${totalFixes} fix(es) across ${editsByFile.size} file(s) would be applied.`));
|
|
1403
|
-
log(chalk4.gray("Run with --fix to apply these changes."));
|
|
1404
|
-
} else {
|
|
1405
|
-
const applyResult = await applyEdits(edits);
|
|
1406
|
-
if (applyResult.errors.length > 0) {
|
|
1407
|
-
for (const err of applyResult.errors) {
|
|
1408
|
-
error(chalk4.red(` ${err.file}: ${err.error}`));
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
const totalFixes = Array.from(editsByFile.values()).reduce((sum, edits2) => sum + edits2.reduce((s, e) => s + e.fixes.length, 0), 0);
|
|
1412
|
-
log("");
|
|
1413
|
-
log(chalk4.green(`✓ Applied ${totalFixes} fix(es) to ${applyResult.filesModified} file(s)`));
|
|
1414
|
-
for (const [filePath, fileEdits] of editsByFile) {
|
|
1415
|
-
const relativePath = path4.relative(targetDir, filePath);
|
|
1416
|
-
const fixCount = fileEdits.reduce((s, e) => s + e.fixes.length, 0);
|
|
1417
|
-
log(chalk4.dim(` ${relativePath} (${fixCount} fixes)`));
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1700
|
+
const fixResult = await handleFixes(spec, { isPreview, targetDir }, { log, error });
|
|
1423
1701
|
if (!isPreview) {
|
|
1424
|
-
driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
|
|
1702
|
+
driftExports = driftExports.filter((d) => !fixResult.fixedDriftKeys.has(`${d.name}:${d.issue}`));
|
|
1425
1703
|
}
|
|
1426
1704
|
}
|
|
1427
1705
|
steps.complete("Check complete");
|
|
1428
1706
|
if (format !== "text") {
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
case "github":
|
|
1445
|
-
formatContent = renderGithubSummary(stats, {
|
|
1446
|
-
coverageScore,
|
|
1447
|
-
driftCount: driftExports.length
|
|
1448
|
-
});
|
|
1449
|
-
break;
|
|
1450
|
-
default:
|
|
1451
|
-
throw new Error(`Unknown format: ${format}`);
|
|
1452
|
-
}
|
|
1453
|
-
if (options.stdout) {
|
|
1454
|
-
log(formatContent);
|
|
1455
|
-
} else {
|
|
1456
|
-
writeReports({
|
|
1457
|
-
format,
|
|
1458
|
-
formatContent,
|
|
1459
|
-
jsonContent,
|
|
1460
|
-
outputPath: options.output,
|
|
1461
|
-
cwd: options.cwd
|
|
1462
|
-
});
|
|
1463
|
-
}
|
|
1464
|
-
const totalExportsForDrift2 = spec.exports?.length ?? 0;
|
|
1465
|
-
const exportsWithDrift2 = new Set(driftExports.map((d) => d.name)).size;
|
|
1466
|
-
const driftScore2 = totalExportsForDrift2 === 0 ? 0 : Math.round(exportsWithDrift2 / totalExportsForDrift2 * 100);
|
|
1467
|
-
const coverageFailed2 = coverageScore < minCoverage;
|
|
1468
|
-
const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
|
|
1469
|
-
const hasTypecheckErrors2 = typecheckErrors.length > 0;
|
|
1470
|
-
if (coverageFailed2 || driftFailed2 || hasTypecheckErrors2) {
|
|
1707
|
+
const passed2 = handleNonTextOutput({
|
|
1708
|
+
format,
|
|
1709
|
+
spec,
|
|
1710
|
+
rawSpec: specResult.spec,
|
|
1711
|
+
coverageScore,
|
|
1712
|
+
minCoverage,
|
|
1713
|
+
maxDrift,
|
|
1714
|
+
driftExports,
|
|
1715
|
+
typecheckErrors,
|
|
1716
|
+
limit: parseInt(options.limit, 10) || 20,
|
|
1717
|
+
stdout: options.stdout,
|
|
1718
|
+
outputPath: options.output,
|
|
1719
|
+
cwd: options.cwd
|
|
1720
|
+
}, { log });
|
|
1721
|
+
if (!passed2) {
|
|
1471
1722
|
process.exit(1);
|
|
1472
1723
|
}
|
|
1473
1724
|
return;
|
|
1474
1725
|
}
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
for (const diag of specInfos) {
|
|
1490
|
-
log(chalk4.cyan(`ℹ ${diag.message}`));
|
|
1491
|
-
if (diag.suggestion) {
|
|
1492
|
-
log(chalk4.gray(` ${diag.suggestion}`));
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
const pkgName = spec.meta?.name ?? "unknown";
|
|
1497
|
-
const pkgVersion = spec.meta?.version ?? "";
|
|
1498
|
-
const totalExports = spec.exports?.length ?? 0;
|
|
1499
|
-
log("");
|
|
1500
|
-
log(chalk4.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
|
|
1501
|
-
log("");
|
|
1502
|
-
log(` Exports: ${totalExports}`);
|
|
1503
|
-
if (coverageFailed) {
|
|
1504
|
-
log(chalk4.red(` Coverage: ✗ ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
|
|
1505
|
-
} else {
|
|
1506
|
-
log(chalk4.green(` Coverage: ✓ ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
|
|
1507
|
-
}
|
|
1508
|
-
if (maxDrift !== undefined) {
|
|
1509
|
-
if (driftFailed) {
|
|
1510
|
-
log(chalk4.red(` Drift: ✗ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
|
|
1511
|
-
} else {
|
|
1512
|
-
log(chalk4.green(` Drift: ✓ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
|
|
1513
|
-
}
|
|
1514
|
-
} else {
|
|
1515
|
-
log(` Drift: ${driftScore}%`);
|
|
1516
|
-
}
|
|
1517
|
-
if (exampleResult) {
|
|
1518
|
-
const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
|
|
1519
|
-
if (typecheckCount > 0) {
|
|
1520
|
-
log(chalk4.yellow(` Examples: ${typecheckCount} type error(s)`));
|
|
1521
|
-
for (const err of typecheckErrors.slice(0, 5)) {
|
|
1522
|
-
const loc = `example[${err.error.exampleIndex}]:${err.error.line}:${err.error.column}`;
|
|
1523
|
-
log(chalk4.dim(` ${err.exportName} ${loc}`));
|
|
1524
|
-
log(chalk4.red(` ${err.error.message}`));
|
|
1525
|
-
}
|
|
1526
|
-
if (typecheckErrors.length > 5) {
|
|
1527
|
-
log(chalk4.dim(` ... and ${typecheckErrors.length - 5} more`));
|
|
1528
|
-
}
|
|
1529
|
-
} else {
|
|
1530
|
-
log(chalk4.green(` Examples: ✓ validated`));
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
const hasStaleRefs = staleRefs.length > 0;
|
|
1534
|
-
if (hasStaleRefs) {
|
|
1535
|
-
log(chalk4.yellow(` Docs: ${staleRefs.length} stale ref(s)`));
|
|
1536
|
-
for (const ref of staleRefs.slice(0, 5)) {
|
|
1537
|
-
log(chalk4.dim(` ${ref.file}:${ref.line} - "${ref.exportName}"`));
|
|
1538
|
-
}
|
|
1539
|
-
if (staleRefs.length > 5) {
|
|
1540
|
-
log(chalk4.dim(` ... and ${staleRefs.length - 5} more`));
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
log("");
|
|
1544
|
-
const failed = coverageFailed || driftFailed || hasTypecheckErrors || hasStaleRefs;
|
|
1545
|
-
if (!failed) {
|
|
1546
|
-
const thresholdParts = [];
|
|
1547
|
-
thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
|
|
1548
|
-
if (maxDrift !== undefined) {
|
|
1549
|
-
thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
|
|
1550
|
-
}
|
|
1551
|
-
log(chalk4.green(`✓ Check passed (${thresholdParts.join(", ")})`));
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1554
|
-
if (hasTypecheckErrors) {
|
|
1555
|
-
log(chalk4.red(`✗ ${typecheckErrors.length} example type errors`));
|
|
1556
|
-
}
|
|
1557
|
-
if (hasStaleRefs) {
|
|
1558
|
-
log(chalk4.red(`✗ ${staleRefs.length} stale references in docs`));
|
|
1726
|
+
const passed = displayTextOutput({
|
|
1727
|
+
spec,
|
|
1728
|
+
coverageScore,
|
|
1729
|
+
minCoverage,
|
|
1730
|
+
maxDrift,
|
|
1731
|
+
driftExports,
|
|
1732
|
+
typecheckErrors,
|
|
1733
|
+
staleRefs,
|
|
1734
|
+
exampleResult,
|
|
1735
|
+
specWarnings,
|
|
1736
|
+
specInfos
|
|
1737
|
+
}, { log });
|
|
1738
|
+
if (!passed) {
|
|
1739
|
+
process.exit(1);
|
|
1559
1740
|
}
|
|
1560
|
-
log("");
|
|
1561
|
-
log(chalk4.dim("Use --format json or --format markdown for detailed reports"));
|
|
1562
|
-
process.exit(1);
|
|
1563
1741
|
} catch (commandError) {
|
|
1564
|
-
error(
|
|
1742
|
+
error(chalk6.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
1565
1743
|
process.exit(1);
|
|
1566
1744
|
}
|
|
1567
1745
|
});
|
|
1568
1746
|
}
|
|
1569
|
-
function collectDrift(exportsList) {
|
|
1570
|
-
const drifts = [];
|
|
1571
|
-
for (const entry of exportsList) {
|
|
1572
|
-
const drift = entry.docs?.drift;
|
|
1573
|
-
if (!drift || drift.length === 0) {
|
|
1574
|
-
continue;
|
|
1575
|
-
}
|
|
1576
|
-
for (const d of drift) {
|
|
1577
|
-
drifts.push({
|
|
1578
|
-
name: entry.name,
|
|
1579
|
-
type: d.type,
|
|
1580
|
-
issue: d.issue ?? "Documentation drift detected.",
|
|
1581
|
-
suggestion: d.suggestion,
|
|
1582
|
-
category: DRIFT_CATEGORIES2[d.type]
|
|
1583
|
-
});
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
return drifts;
|
|
1587
|
-
}
|
|
1588
|
-
function collect(value, previous) {
|
|
1589
|
-
return previous.concat([value]);
|
|
1590
|
-
}
|
|
1591
|
-
async function loadMarkdownFiles(patterns, cwd) {
|
|
1592
|
-
const files = [];
|
|
1593
|
-
for (const pattern of patterns) {
|
|
1594
|
-
const matches = await glob(pattern, { nodir: true, cwd });
|
|
1595
|
-
for (const filePath of matches) {
|
|
1596
|
-
try {
|
|
1597
|
-
const fullPath = path4.resolve(cwd, filePath);
|
|
1598
|
-
const content = fs2.readFileSync(fullPath, "utf-8");
|
|
1599
|
-
files.push({ path: filePath, content });
|
|
1600
|
-
} catch {}
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
return parseMarkdownFiles(files);
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
1747
|
// src/commands/diff.ts
|
|
1607
|
-
import * as
|
|
1608
|
-
import * as
|
|
1748
|
+
import * as fs4 from "node:fs";
|
|
1749
|
+
import * as path6 from "node:path";
|
|
1609
1750
|
import {
|
|
1610
1751
|
diffSpecWithDocs,
|
|
1611
1752
|
ensureSpecCoverage,
|
|
@@ -1616,10 +1757,10 @@ import {
|
|
|
1616
1757
|
parseMarkdownFiles as parseMarkdownFiles2
|
|
1617
1758
|
} from "@doccov/sdk";
|
|
1618
1759
|
import { calculateNextVersion, recommendSemverBump } from "@openpkg-ts/spec";
|
|
1619
|
-
import
|
|
1760
|
+
import chalk7 from "chalk";
|
|
1620
1761
|
import { glob as glob2 } from "glob";
|
|
1621
1762
|
var defaultDependencies2 = {
|
|
1622
|
-
readFileSync:
|
|
1763
|
+
readFileSync: fs4.readFileSync,
|
|
1623
1764
|
log: console.log,
|
|
1624
1765
|
error: console.error
|
|
1625
1766
|
};
|
|
@@ -1657,12 +1798,12 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1657
1798
|
const baseHash = hashString(JSON.stringify(baseSpec));
|
|
1658
1799
|
const headHash = hashString(JSON.stringify(headSpec));
|
|
1659
1800
|
const cacheEnabled = options.cache !== false;
|
|
1660
|
-
const cachedReportPath =
|
|
1801
|
+
const cachedReportPath = path6.resolve(options.cwd, getDiffReportPath(baseHash, headHash, "json"));
|
|
1661
1802
|
let diff;
|
|
1662
1803
|
let fromCache = false;
|
|
1663
|
-
if (cacheEnabled &&
|
|
1804
|
+
if (cacheEnabled && fs4.existsSync(cachedReportPath)) {
|
|
1664
1805
|
try {
|
|
1665
|
-
const cached = JSON.parse(
|
|
1806
|
+
const cached = JSON.parse(fs4.readFileSync(cachedReportPath, "utf-8"));
|
|
1666
1807
|
diff = cached;
|
|
1667
1808
|
fromCache = true;
|
|
1668
1809
|
} catch {
|
|
@@ -1689,9 +1830,9 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1689
1830
|
}, null, 2));
|
|
1690
1831
|
} else {
|
|
1691
1832
|
log("");
|
|
1692
|
-
log(
|
|
1833
|
+
log(chalk7.bold("Semver Recommendation"));
|
|
1693
1834
|
log(` Current version: ${currentVersion}`);
|
|
1694
|
-
log(` Recommended: ${
|
|
1835
|
+
log(` Recommended: ${chalk7.cyan(nextVersion)} (${chalk7.yellow(recommendation.bump.toUpperCase())})`);
|
|
1695
1836
|
log(` Reason: ${recommendation.reason}`);
|
|
1696
1837
|
}
|
|
1697
1838
|
return;
|
|
@@ -1699,8 +1840,8 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1699
1840
|
const format = options.format ?? "text";
|
|
1700
1841
|
const limit = parseInt(options.limit, 10) || 10;
|
|
1701
1842
|
const checks = getStrictChecks(options.strict);
|
|
1702
|
-
const baseName =
|
|
1703
|
-
const headName =
|
|
1843
|
+
const baseName = path6.basename(baseFile);
|
|
1844
|
+
const headName = path6.basename(headFile);
|
|
1704
1845
|
const reportData = {
|
|
1705
1846
|
baseName,
|
|
1706
1847
|
headName,
|
|
@@ -1720,8 +1861,8 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1720
1861
|
silent: true
|
|
1721
1862
|
});
|
|
1722
1863
|
}
|
|
1723
|
-
const cacheNote = fromCache ?
|
|
1724
|
-
log(
|
|
1864
|
+
const cacheNote = fromCache ? chalk7.cyan(" (cached)") : "";
|
|
1865
|
+
log(chalk7.dim(`Report: ${jsonPath}`) + cacheNote);
|
|
1725
1866
|
}
|
|
1726
1867
|
break;
|
|
1727
1868
|
case "json": {
|
|
@@ -1779,7 +1920,10 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1779
1920
|
sha: options.sha,
|
|
1780
1921
|
minCoverage,
|
|
1781
1922
|
limit,
|
|
1782
|
-
semverBump: {
|
|
1923
|
+
semverBump: {
|
|
1924
|
+
bump: semverRecommendation.bump,
|
|
1925
|
+
reason: semverRecommendation.reason
|
|
1926
|
+
}
|
|
1783
1927
|
});
|
|
1784
1928
|
log(content);
|
|
1785
1929
|
break;
|
|
@@ -1813,18 +1957,18 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1813
1957
|
checks
|
|
1814
1958
|
});
|
|
1815
1959
|
if (failures.length > 0) {
|
|
1816
|
-
log(
|
|
1960
|
+
log(chalk7.red(`
|
|
1817
1961
|
✗ Check failed`));
|
|
1818
1962
|
for (const f of failures) {
|
|
1819
|
-
log(
|
|
1963
|
+
log(chalk7.red(` - ${f}`));
|
|
1820
1964
|
}
|
|
1821
1965
|
process.exitCode = 1;
|
|
1822
1966
|
} else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
|
|
1823
|
-
log(
|
|
1967
|
+
log(chalk7.green(`
|
|
1824
1968
|
✓ All checks passed`));
|
|
1825
1969
|
}
|
|
1826
1970
|
} catch (commandError) {
|
|
1827
|
-
error(
|
|
1971
|
+
error(chalk7.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
1828
1972
|
process.exitCode = 1;
|
|
1829
1973
|
}
|
|
1830
1974
|
});
|
|
@@ -1838,7 +1982,7 @@ async function loadMarkdownFiles2(patterns) {
|
|
|
1838
1982
|
const matches = await glob2(pattern, { nodir: true });
|
|
1839
1983
|
for (const filePath of matches) {
|
|
1840
1984
|
try {
|
|
1841
|
-
const content =
|
|
1985
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1842
1986
|
files.push({ path: filePath, content });
|
|
1843
1987
|
} catch {}
|
|
1844
1988
|
}
|
|
@@ -1851,7 +1995,7 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
|
|
|
1851
1995
|
if (!docsPatterns || docsPatterns.length === 0) {
|
|
1852
1996
|
if (config?.docs?.include) {
|
|
1853
1997
|
docsPatterns = config.docs.include;
|
|
1854
|
-
log(
|
|
1998
|
+
log(chalk7.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
|
|
1855
1999
|
}
|
|
1856
2000
|
}
|
|
1857
2001
|
if (docsPatterns && docsPatterns.length > 0) {
|
|
@@ -1860,8 +2004,8 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
|
|
|
1860
2004
|
return diffSpecWithDocs(baseSpec, headSpec, { markdownFiles });
|
|
1861
2005
|
}
|
|
1862
2006
|
function loadSpec(filePath, readFileSync3) {
|
|
1863
|
-
const resolvedPath =
|
|
1864
|
-
if (!
|
|
2007
|
+
const resolvedPath = path6.resolve(filePath);
|
|
2008
|
+
if (!fs4.existsSync(resolvedPath)) {
|
|
1865
2009
|
throw new Error(`File not found: ${filePath}`);
|
|
1866
2010
|
}
|
|
1867
2011
|
try {
|
|
@@ -1874,37 +2018,37 @@ function loadSpec(filePath, readFileSync3) {
|
|
|
1874
2018
|
}
|
|
1875
2019
|
function printSummary(diff, baseName, headName, fromCache, log) {
|
|
1876
2020
|
log("");
|
|
1877
|
-
const cacheIndicator = fromCache ?
|
|
1878
|
-
log(
|
|
2021
|
+
const cacheIndicator = fromCache ? chalk7.cyan(" (cached)") : "";
|
|
2022
|
+
log(chalk7.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
|
|
1879
2023
|
log("─".repeat(40));
|
|
1880
2024
|
log("");
|
|
1881
|
-
const coverageColor = diff.coverageDelta > 0 ?
|
|
2025
|
+
const coverageColor = diff.coverageDelta > 0 ? chalk7.green : diff.coverageDelta < 0 ? chalk7.red : chalk7.gray;
|
|
1882
2026
|
const coverageSign = diff.coverageDelta > 0 ? "+" : "";
|
|
1883
2027
|
log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
|
|
1884
2028
|
const breakingCount = diff.breaking.length;
|
|
1885
2029
|
const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
|
|
1886
2030
|
if (breakingCount > 0) {
|
|
1887
|
-
const severityNote = highSeverity > 0 ?
|
|
1888
|
-
log(` Breaking: ${
|
|
2031
|
+
const severityNote = highSeverity > 0 ? chalk7.red(` (${highSeverity} high severity)`) : "";
|
|
2032
|
+
log(` Breaking: ${chalk7.red(breakingCount)} changes${severityNote}`);
|
|
1889
2033
|
} else {
|
|
1890
|
-
log(` Breaking: ${
|
|
2034
|
+
log(` Breaking: ${chalk7.green("0")} changes`);
|
|
1891
2035
|
}
|
|
1892
2036
|
const newCount = diff.nonBreaking.length;
|
|
1893
2037
|
const undocCount = diff.newUndocumented.length;
|
|
1894
2038
|
if (newCount > 0) {
|
|
1895
|
-
const undocNote = undocCount > 0 ?
|
|
1896
|
-
log(` New: ${
|
|
2039
|
+
const undocNote = undocCount > 0 ? chalk7.yellow(` (${undocCount} undocumented)`) : "";
|
|
2040
|
+
log(` New: ${chalk7.green(newCount)} exports${undocNote}`);
|
|
1897
2041
|
}
|
|
1898
2042
|
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
1899
2043
|
const parts = [];
|
|
1900
2044
|
if (diff.driftIntroduced > 0)
|
|
1901
|
-
parts.push(
|
|
2045
|
+
parts.push(chalk7.red(`+${diff.driftIntroduced}`));
|
|
1902
2046
|
if (diff.driftResolved > 0)
|
|
1903
|
-
parts.push(
|
|
2047
|
+
parts.push(chalk7.green(`-${diff.driftResolved}`));
|
|
1904
2048
|
log(` Drift: ${parts.join(", ")}`);
|
|
1905
2049
|
}
|
|
1906
2050
|
const recommendation = recommendSemverBump(diff);
|
|
1907
|
-
const bumpColor = recommendation.bump === "major" ?
|
|
2051
|
+
const bumpColor = recommendation.bump === "major" ? chalk7.red : recommendation.bump === "minor" ? chalk7.yellow : chalk7.green;
|
|
1908
2052
|
log(` Semver: ${bumpColor(recommendation.bump.toUpperCase())} (${recommendation.reason})`);
|
|
1909
2053
|
log("");
|
|
1910
2054
|
}
|
|
@@ -1986,7 +2130,7 @@ function printGitHubAnnotations(diff, log) {
|
|
|
1986
2130
|
|
|
1987
2131
|
// src/commands/info.ts
|
|
1988
2132
|
import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
|
|
1989
|
-
import
|
|
2133
|
+
import chalk8 from "chalk";
|
|
1990
2134
|
function registerInfoCommand(program) {
|
|
1991
2135
|
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) => {
|
|
1992
2136
|
try {
|
|
@@ -2008,28 +2152,28 @@ function registerInfoCommand(program) {
|
|
|
2008
2152
|
const spec = enrichSpec2(specResult.spec);
|
|
2009
2153
|
const stats = computeStats(spec);
|
|
2010
2154
|
console.log("");
|
|
2011
|
-
console.log(
|
|
2155
|
+
console.log(chalk8.bold(`${stats.packageName}@${stats.version}`));
|
|
2012
2156
|
console.log("");
|
|
2013
|
-
console.log(` Exports: ${
|
|
2014
|
-
console.log(` Coverage: ${
|
|
2015
|
-
console.log(` Drift: ${
|
|
2157
|
+
console.log(` Exports: ${chalk8.bold(stats.totalExports.toString())}`);
|
|
2158
|
+
console.log(` Coverage: ${chalk8.bold(`${stats.coverageScore}%`)}`);
|
|
2159
|
+
console.log(` Drift: ${chalk8.bold(`${stats.driftScore}%`)}`);
|
|
2016
2160
|
console.log("");
|
|
2017
2161
|
} catch (err) {
|
|
2018
|
-
console.error(
|
|
2162
|
+
console.error(chalk8.red("Error:"), err instanceof Error ? err.message : err);
|
|
2019
2163
|
process.exit(1);
|
|
2020
2164
|
}
|
|
2021
2165
|
});
|
|
2022
2166
|
}
|
|
2023
2167
|
|
|
2024
2168
|
// src/commands/init.ts
|
|
2025
|
-
import * as
|
|
2026
|
-
import * as
|
|
2027
|
-
import
|
|
2169
|
+
import * as fs5 from "node:fs";
|
|
2170
|
+
import * as path7 from "node:path";
|
|
2171
|
+
import chalk9 from "chalk";
|
|
2028
2172
|
var defaultDependencies3 = {
|
|
2029
|
-
fileExists:
|
|
2030
|
-
writeFileSync:
|
|
2031
|
-
readFileSync:
|
|
2032
|
-
mkdirSync:
|
|
2173
|
+
fileExists: fs5.existsSync,
|
|
2174
|
+
writeFileSync: fs5.writeFileSync,
|
|
2175
|
+
readFileSync: fs5.readFileSync,
|
|
2176
|
+
mkdirSync: fs5.mkdirSync,
|
|
2033
2177
|
log: console.log,
|
|
2034
2178
|
error: console.error
|
|
2035
2179
|
};
|
|
@@ -2039,56 +2183,56 @@ function registerInitCommand(program, dependencies = {}) {
|
|
|
2039
2183
|
...dependencies
|
|
2040
2184
|
};
|
|
2041
2185
|
program.command("init").description("Initialize DocCov: config, GitHub Action, and badge").option("--cwd <dir>", "Working directory", process.cwd()).option("--skip-action", "Skip GitHub Action workflow creation").action((options) => {
|
|
2042
|
-
const cwd =
|
|
2186
|
+
const cwd = path7.resolve(options.cwd);
|
|
2043
2187
|
const existing = findExistingConfig(cwd, fileExists2);
|
|
2044
2188
|
if (existing) {
|
|
2045
|
-
error(
|
|
2189
|
+
error(chalk9.red(`A DocCov config already exists at ${path7.relative(cwd, existing) || "./doccov.config.*"}.`));
|
|
2046
2190
|
process.exitCode = 1;
|
|
2047
2191
|
return;
|
|
2048
2192
|
}
|
|
2049
2193
|
const packageType = detectPackageType(cwd, fileExists2, readFileSync4);
|
|
2050
2194
|
const targetFormat = packageType === "module" ? "ts" : "mts";
|
|
2051
2195
|
const fileName = `doccov.config.${targetFormat}`;
|
|
2052
|
-
const outputPath =
|
|
2196
|
+
const outputPath = path7.join(cwd, fileName);
|
|
2053
2197
|
if (fileExists2(outputPath)) {
|
|
2054
|
-
error(
|
|
2198
|
+
error(chalk9.red(`Cannot create ${fileName}; file already exists.`));
|
|
2055
2199
|
process.exitCode = 1;
|
|
2056
2200
|
return;
|
|
2057
2201
|
}
|
|
2058
2202
|
const template = buildConfigTemplate();
|
|
2059
2203
|
writeFileSync3(outputPath, template, { encoding: "utf8" });
|
|
2060
|
-
log(
|
|
2204
|
+
log(chalk9.green(`✓ Created ${fileName}`));
|
|
2061
2205
|
if (!options.skipAction) {
|
|
2062
|
-
const workflowDir =
|
|
2063
|
-
const workflowPath =
|
|
2206
|
+
const workflowDir = path7.join(cwd, ".github", "workflows");
|
|
2207
|
+
const workflowPath = path7.join(workflowDir, "doccov.yml");
|
|
2064
2208
|
if (!fileExists2(workflowPath)) {
|
|
2065
2209
|
mkdirSync3(workflowDir, { recursive: true });
|
|
2066
2210
|
writeFileSync3(workflowPath, buildWorkflowTemplate(), { encoding: "utf8" });
|
|
2067
|
-
log(
|
|
2211
|
+
log(chalk9.green(`✓ Created .github/workflows/doccov.yml`));
|
|
2068
2212
|
} else {
|
|
2069
|
-
log(
|
|
2213
|
+
log(chalk9.yellow(` Skipped .github/workflows/doccov.yml (already exists)`));
|
|
2070
2214
|
}
|
|
2071
2215
|
}
|
|
2072
2216
|
const repoInfo = detectRepoInfo(cwd, fileExists2, readFileSync4);
|
|
2073
2217
|
log("");
|
|
2074
|
-
log(
|
|
2218
|
+
log(chalk9.bold("Add this badge to your README:"));
|
|
2075
2219
|
log("");
|
|
2076
2220
|
if (repoInfo) {
|
|
2077
|
-
log(
|
|
2221
|
+
log(chalk9.cyan(`[](https://doccov.dev/${repoInfo.owner}/${repoInfo.repo})`));
|
|
2078
2222
|
} else {
|
|
2079
|
-
log(
|
|
2080
|
-
log(
|
|
2223
|
+
log(chalk9.cyan(`[](https://doccov.dev/OWNER/REPO)`));
|
|
2224
|
+
log(chalk9.dim(" Replace OWNER/REPO with your GitHub repo"));
|
|
2081
2225
|
}
|
|
2082
2226
|
log("");
|
|
2083
|
-
log(
|
|
2227
|
+
log(chalk9.dim("Run `doccov check` to verify your documentation coverage"));
|
|
2084
2228
|
});
|
|
2085
2229
|
}
|
|
2086
2230
|
var findExistingConfig = (cwd, fileExists2) => {
|
|
2087
|
-
let current =
|
|
2088
|
-
const { root } =
|
|
2231
|
+
let current = path7.resolve(cwd);
|
|
2232
|
+
const { root } = path7.parse(current);
|
|
2089
2233
|
while (true) {
|
|
2090
2234
|
for (const candidate of DOCCOV_CONFIG_FILENAMES) {
|
|
2091
|
-
const candidatePath =
|
|
2235
|
+
const candidatePath = path7.join(current, candidate);
|
|
2092
2236
|
if (fileExists2(candidatePath)) {
|
|
2093
2237
|
return candidatePath;
|
|
2094
2238
|
}
|
|
@@ -2096,7 +2240,7 @@ var findExistingConfig = (cwd, fileExists2) => {
|
|
|
2096
2240
|
if (current === root) {
|
|
2097
2241
|
break;
|
|
2098
2242
|
}
|
|
2099
|
-
current =
|
|
2243
|
+
current = path7.dirname(current);
|
|
2100
2244
|
}
|
|
2101
2245
|
return null;
|
|
2102
2246
|
};
|
|
@@ -2118,17 +2262,17 @@ var detectPackageType = (cwd, fileExists2, readFileSync4) => {
|
|
|
2118
2262
|
return;
|
|
2119
2263
|
};
|
|
2120
2264
|
var findNearestPackageJson = (cwd, fileExists2) => {
|
|
2121
|
-
let current =
|
|
2122
|
-
const { root } =
|
|
2265
|
+
let current = path7.resolve(cwd);
|
|
2266
|
+
const { root } = path7.parse(current);
|
|
2123
2267
|
while (true) {
|
|
2124
|
-
const candidate =
|
|
2268
|
+
const candidate = path7.join(current, "package.json");
|
|
2125
2269
|
if (fileExists2(candidate)) {
|
|
2126
2270
|
return candidate;
|
|
2127
2271
|
}
|
|
2128
2272
|
if (current === root) {
|
|
2129
2273
|
break;
|
|
2130
2274
|
}
|
|
2131
|
-
current =
|
|
2275
|
+
current = path7.dirname(current);
|
|
2132
2276
|
}
|
|
2133
2277
|
return null;
|
|
2134
2278
|
};
|
|
@@ -2190,7 +2334,7 @@ var detectRepoInfo = (cwd, fileExists2, readFileSync4) => {
|
|
|
2190
2334
|
}
|
|
2191
2335
|
} catch {}
|
|
2192
2336
|
}
|
|
2193
|
-
const gitConfigPath =
|
|
2337
|
+
const gitConfigPath = path7.join(cwd, ".git", "config");
|
|
2194
2338
|
if (fileExists2(gitConfigPath)) {
|
|
2195
2339
|
try {
|
|
2196
2340
|
const config = readFileSync4(gitConfigPath, "utf8");
|
|
@@ -2204,18 +2348,24 @@ var detectRepoInfo = (cwd, fileExists2, readFileSync4) => {
|
|
|
2204
2348
|
};
|
|
2205
2349
|
|
|
2206
2350
|
// src/commands/spec.ts
|
|
2207
|
-
import * as
|
|
2208
|
-
import * as
|
|
2209
|
-
import {
|
|
2351
|
+
import * as fs6 from "node:fs";
|
|
2352
|
+
import * as path8 from "node:path";
|
|
2353
|
+
import {
|
|
2354
|
+
DocCov as DocCov3,
|
|
2355
|
+
detectPackageManager,
|
|
2356
|
+
NodeFileSystem as NodeFileSystem3,
|
|
2357
|
+
renderApiSurface,
|
|
2358
|
+
resolveTarget as resolveTarget3
|
|
2359
|
+
} from "@doccov/sdk";
|
|
2210
2360
|
import { normalize, validateSpec } from "@openpkg-ts/spec";
|
|
2211
|
-
import
|
|
2361
|
+
import chalk10 from "chalk";
|
|
2212
2362
|
// package.json
|
|
2213
|
-
var version = "0.
|
|
2363
|
+
var version = "0.22.0";
|
|
2214
2364
|
|
|
2215
2365
|
// src/commands/spec.ts
|
|
2216
2366
|
var defaultDependencies4 = {
|
|
2217
2367
|
createDocCov: (options) => new DocCov3(options),
|
|
2218
|
-
writeFileSync:
|
|
2368
|
+
writeFileSync: fs6.writeFileSync,
|
|
2219
2369
|
log: console.log,
|
|
2220
2370
|
error: console.error
|
|
2221
2371
|
};
|
|
@@ -2224,8 +2374,8 @@ function getArrayLength(value) {
|
|
|
2224
2374
|
}
|
|
2225
2375
|
function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
|
|
2226
2376
|
const location = diagnostic.location;
|
|
2227
|
-
const relativePath = location?.file ?
|
|
2228
|
-
const locationText = location && relativePath ?
|
|
2377
|
+
const relativePath = location?.file ? path8.relative(baseDir, location.file) || location.file : undefined;
|
|
2378
|
+
const locationText = location && relativePath ? chalk10.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
|
|
2229
2379
|
const locationPrefix = locationText ? `${locationText} ` : "";
|
|
2230
2380
|
return `${prefix} ${locationPrefix}${diagnostic.message}`;
|
|
2231
2381
|
}
|
|
@@ -2256,7 +2406,7 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2256
2406
|
try {
|
|
2257
2407
|
config = await loadDocCovConfig(targetDir);
|
|
2258
2408
|
} catch (configError) {
|
|
2259
|
-
error(
|
|
2409
|
+
error(chalk10.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
|
|
2260
2410
|
process.exit(1);
|
|
2261
2411
|
}
|
|
2262
2412
|
steps.next();
|
|
@@ -2275,7 +2425,7 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2275
2425
|
schemaExtraction: options.runtime ? "hybrid" : "static"
|
|
2276
2426
|
});
|
|
2277
2427
|
const generationInput = {
|
|
2278
|
-
entryPoint:
|
|
2428
|
+
entryPoint: path8.relative(targetDir, entryFile),
|
|
2279
2429
|
entryPointSource: entryPointInfo.source,
|
|
2280
2430
|
isDeclarationOnly: entryPointInfo.isDeclarationOnly ?? false,
|
|
2281
2431
|
generatorName: "@doccov/cli",
|
|
@@ -2300,15 +2450,15 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2300
2450
|
const normalized = normalize(result.spec);
|
|
2301
2451
|
const validation = validateSpec(normalized);
|
|
2302
2452
|
if (!validation.ok) {
|
|
2303
|
-
error(
|
|
2453
|
+
error(chalk10.red("Spec failed schema validation"));
|
|
2304
2454
|
for (const err of validation.errors) {
|
|
2305
|
-
error(
|
|
2455
|
+
error(chalk10.red(`schema: ${err.instancePath || "/"} ${err.message}`));
|
|
2306
2456
|
}
|
|
2307
2457
|
process.exit(1);
|
|
2308
2458
|
}
|
|
2309
2459
|
steps.next();
|
|
2310
2460
|
const format = options.format ?? "json";
|
|
2311
|
-
const outputPath =
|
|
2461
|
+
const outputPath = path8.resolve(options.cwd, options.output);
|
|
2312
2462
|
if (format === "api-surface") {
|
|
2313
2463
|
const apiSurface = renderApiSurface(normalized);
|
|
2314
2464
|
writeFileSync4(outputPath, apiSurface);
|
|
@@ -2317,78 +2467,78 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2317
2467
|
writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
|
|
2318
2468
|
steps.complete(`Generated ${options.output}`);
|
|
2319
2469
|
}
|
|
2320
|
-
log(
|
|
2321
|
-
log(
|
|
2470
|
+
log(chalk10.gray(` ${getArrayLength(normalized.exports)} exports`));
|
|
2471
|
+
log(chalk10.gray(` ${getArrayLength(normalized.types)} types`));
|
|
2322
2472
|
const schemaExtraction = normalized.generation?.analysis?.schemaExtraction;
|
|
2323
2473
|
if (options.runtime && (!schemaExtraction?.runtimeCount || schemaExtraction.runtimeCount === 0)) {
|
|
2324
2474
|
const pm = await detectPackageManager(fileSystem);
|
|
2325
2475
|
const buildCmd = pm.name === "npm" ? "npm run build" : `${pm.name} run build`;
|
|
2326
2476
|
log("");
|
|
2327
|
-
log(
|
|
2328
|
-
log(
|
|
2477
|
+
log(chalk10.yellow("⚠ Runtime extraction requested but no schemas extracted."));
|
|
2478
|
+
log(chalk10.yellow(` Ensure project is built (${buildCmd}) and dist/ exists.`));
|
|
2329
2479
|
}
|
|
2330
2480
|
if (options.verbose && normalized.generation) {
|
|
2331
2481
|
const gen = normalized.generation;
|
|
2332
2482
|
log("");
|
|
2333
|
-
log(
|
|
2334
|
-
log(
|
|
2335
|
-
log(
|
|
2336
|
-
log(
|
|
2337
|
-
log(
|
|
2338
|
-
log(
|
|
2339
|
-
log(
|
|
2483
|
+
log(chalk10.bold("Generation Info"));
|
|
2484
|
+
log(chalk10.gray(` Timestamp: ${gen.timestamp}`));
|
|
2485
|
+
log(chalk10.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
|
|
2486
|
+
log(chalk10.gray(` Entry point: ${gen.analysis.entryPoint}`));
|
|
2487
|
+
log(chalk10.gray(` Detected via: ${gen.analysis.entryPointSource}`));
|
|
2488
|
+
log(chalk10.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
|
|
2489
|
+
log(chalk10.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
|
|
2340
2490
|
if (gen.analysis.maxTypeDepth) {
|
|
2341
|
-
log(
|
|
2491
|
+
log(chalk10.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
|
|
2342
2492
|
}
|
|
2343
2493
|
if (gen.analysis.schemaExtraction) {
|
|
2344
2494
|
const se = gen.analysis.schemaExtraction;
|
|
2345
|
-
log(
|
|
2495
|
+
log(chalk10.gray(` Schema extraction: ${se.method}`));
|
|
2346
2496
|
if (se.runtimeCount) {
|
|
2347
|
-
log(
|
|
2497
|
+
log(chalk10.gray(` Runtime schemas: ${se.runtimeCount} (${se.vendors?.join(", ")})`));
|
|
2348
2498
|
}
|
|
2349
2499
|
}
|
|
2350
2500
|
log("");
|
|
2351
|
-
log(
|
|
2352
|
-
log(
|
|
2501
|
+
log(chalk10.bold("Environment"));
|
|
2502
|
+
log(chalk10.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
|
|
2353
2503
|
if (gen.environment.packageManager) {
|
|
2354
|
-
log(
|
|
2504
|
+
log(chalk10.gray(` Package manager: ${gen.environment.packageManager}`));
|
|
2355
2505
|
}
|
|
2356
2506
|
if (gen.environment.isMonorepo) {
|
|
2357
|
-
log(
|
|
2507
|
+
log(chalk10.gray(` Monorepo: yes`));
|
|
2358
2508
|
}
|
|
2359
2509
|
if (gen.environment.targetPackage) {
|
|
2360
|
-
log(
|
|
2510
|
+
log(chalk10.gray(` Target package: ${gen.environment.targetPackage}`));
|
|
2361
2511
|
}
|
|
2362
2512
|
if (gen.issues.length > 0) {
|
|
2363
2513
|
log("");
|
|
2364
|
-
log(
|
|
2514
|
+
log(chalk10.bold("Issues"));
|
|
2365
2515
|
for (const issue of gen.issues) {
|
|
2366
|
-
const prefix = issue.severity === "error" ?
|
|
2516
|
+
const prefix = issue.severity === "error" ? chalk10.red(">") : issue.severity === "warning" ? chalk10.yellow(">") : chalk10.cyan(">");
|
|
2367
2517
|
log(`${prefix} [${issue.code}] ${issue.message}`);
|
|
2368
2518
|
if (issue.suggestion) {
|
|
2369
|
-
log(
|
|
2519
|
+
log(chalk10.gray(` ${issue.suggestion}`));
|
|
2370
2520
|
}
|
|
2371
2521
|
}
|
|
2372
2522
|
}
|
|
2373
2523
|
}
|
|
2374
2524
|
if (options.showDiagnostics && result.diagnostics.length > 0) {
|
|
2375
2525
|
log("");
|
|
2376
|
-
log(
|
|
2526
|
+
log(chalk10.bold("Diagnostics"));
|
|
2377
2527
|
for (const diagnostic of result.diagnostics) {
|
|
2378
|
-
const prefix = diagnostic.severity === "error" ?
|
|
2528
|
+
const prefix = diagnostic.severity === "error" ? chalk10.red(">") : diagnostic.severity === "warning" ? chalk10.yellow(">") : chalk10.cyan(">");
|
|
2379
2529
|
log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
|
|
2380
2530
|
}
|
|
2381
2531
|
}
|
|
2382
2532
|
} catch (commandError) {
|
|
2383
|
-
error(
|
|
2533
|
+
error(chalk10.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
2384
2534
|
process.exit(1);
|
|
2385
2535
|
}
|
|
2386
2536
|
});
|
|
2387
2537
|
}
|
|
2388
2538
|
|
|
2389
2539
|
// src/commands/trends.ts
|
|
2390
|
-
import * as
|
|
2391
|
-
import * as
|
|
2540
|
+
import * as fs7 from "node:fs";
|
|
2541
|
+
import * as path9 from "node:path";
|
|
2392
2542
|
import {
|
|
2393
2543
|
formatDelta as formatDelta2,
|
|
2394
2544
|
getExtendedTrend,
|
|
@@ -2396,11 +2546,11 @@ import {
|
|
|
2396
2546
|
loadSnapshots,
|
|
2397
2547
|
pruneByTier,
|
|
2398
2548
|
pruneHistory,
|
|
2399
|
-
renderSparkline,
|
|
2400
2549
|
RETENTION_DAYS,
|
|
2550
|
+
renderSparkline,
|
|
2401
2551
|
saveSnapshot
|
|
2402
2552
|
} from "@doccov/sdk";
|
|
2403
|
-
import
|
|
2553
|
+
import chalk11 from "chalk";
|
|
2404
2554
|
function formatDate(timestamp) {
|
|
2405
2555
|
const date = new Date(timestamp);
|
|
2406
2556
|
return date.toLocaleDateString("en-US", {
|
|
@@ -2413,19 +2563,19 @@ function formatDate(timestamp) {
|
|
|
2413
2563
|
}
|
|
2414
2564
|
function getColorForScore(score) {
|
|
2415
2565
|
if (score >= 90)
|
|
2416
|
-
return
|
|
2566
|
+
return chalk11.green;
|
|
2417
2567
|
if (score >= 70)
|
|
2418
|
-
return
|
|
2568
|
+
return chalk11.yellow;
|
|
2419
2569
|
if (score >= 50)
|
|
2420
|
-
return
|
|
2421
|
-
return
|
|
2570
|
+
return chalk11.hex("#FFA500");
|
|
2571
|
+
return chalk11.red;
|
|
2422
2572
|
}
|
|
2423
2573
|
function formatSnapshot(snapshot) {
|
|
2424
2574
|
const color = getColorForScore(snapshot.coverageScore);
|
|
2425
2575
|
const date = formatDate(snapshot.timestamp);
|
|
2426
2576
|
const version2 = snapshot.version ? ` v${snapshot.version}` : "";
|
|
2427
|
-
const commit = snapshot.commit ?
|
|
2428
|
-
return `${
|
|
2577
|
+
const commit = snapshot.commit ? chalk11.gray(` (${snapshot.commit.slice(0, 7)})`) : "";
|
|
2578
|
+
return `${chalk11.gray(date)} ${color(`${snapshot.coverageScore}%`)} ${snapshot.documentedExports}/${snapshot.totalExports} exports${version2}${commit}`;
|
|
2429
2579
|
}
|
|
2430
2580
|
function formatWeekDate(timestamp) {
|
|
2431
2581
|
const date = new Date(timestamp);
|
|
@@ -2433,55 +2583,55 @@ function formatWeekDate(timestamp) {
|
|
|
2433
2583
|
}
|
|
2434
2584
|
function formatVelocity(velocity) {
|
|
2435
2585
|
if (velocity > 0)
|
|
2436
|
-
return
|
|
2586
|
+
return chalk11.green(`+${velocity}%/day`);
|
|
2437
2587
|
if (velocity < 0)
|
|
2438
|
-
return
|
|
2439
|
-
return
|
|
2588
|
+
return chalk11.red(`${velocity}%/day`);
|
|
2589
|
+
return chalk11.gray("0%/day");
|
|
2440
2590
|
}
|
|
2441
2591
|
function registerTrendsCommand(program) {
|
|
2442
2592
|
program.command("trends").description("Show coverage trends over time").option("--cwd <dir>", "Working directory", process.cwd()).option("-n, --limit <count>", "Number of snapshots to show", "10").option("--prune <count>", "Prune history to keep only N snapshots").option("--record", "Record current coverage to history").option("--json", "Output as JSON").option("--extended", "Show extended trend analysis (velocity, projections)").option("--tier <tier>", "Retention tier: free (7d), team (30d), pro (90d)", "pro").option("--weekly", "Show weekly summary breakdown").action(async (options) => {
|
|
2443
|
-
const cwd =
|
|
2593
|
+
const cwd = path9.resolve(options.cwd);
|
|
2444
2594
|
const tier = options.tier ?? "pro";
|
|
2445
2595
|
if (options.prune) {
|
|
2446
2596
|
const keepCount = parseInt(options.prune, 10);
|
|
2447
|
-
if (!isNaN(keepCount)) {
|
|
2597
|
+
if (!Number.isNaN(keepCount)) {
|
|
2448
2598
|
const deleted = pruneHistory(cwd, keepCount);
|
|
2449
|
-
console.log(
|
|
2599
|
+
console.log(chalk11.green(`Pruned ${deleted} old snapshots, kept ${keepCount} most recent`));
|
|
2450
2600
|
} else {
|
|
2451
2601
|
const deleted = pruneByTier(cwd, tier);
|
|
2452
|
-
console.log(
|
|
2602
|
+
console.log(chalk11.green(`Pruned ${deleted} snapshots older than ${RETENTION_DAYS[tier]} days`));
|
|
2453
2603
|
}
|
|
2454
2604
|
return;
|
|
2455
2605
|
}
|
|
2456
2606
|
if (options.record) {
|
|
2457
|
-
const specPath =
|
|
2458
|
-
if (!
|
|
2459
|
-
console.error(
|
|
2607
|
+
const specPath = path9.resolve(cwd, "openpkg.json");
|
|
2608
|
+
if (!fs7.existsSync(specPath)) {
|
|
2609
|
+
console.error(chalk11.red("No openpkg.json found. Run `doccov spec` first to generate a spec."));
|
|
2460
2610
|
process.exit(1);
|
|
2461
2611
|
}
|
|
2462
2612
|
try {
|
|
2463
|
-
const specContent =
|
|
2613
|
+
const specContent = fs7.readFileSync(specPath, "utf-8");
|
|
2464
2614
|
const spec = JSON.parse(specContent);
|
|
2465
2615
|
const trend = getTrend(spec, cwd);
|
|
2466
2616
|
saveSnapshot(trend.current, cwd);
|
|
2467
|
-
console.log(
|
|
2617
|
+
console.log(chalk11.green("Recorded coverage snapshot:"));
|
|
2468
2618
|
console.log(formatSnapshot(trend.current));
|
|
2469
2619
|
if (trend.delta !== undefined) {
|
|
2470
2620
|
const deltaStr = formatDelta2(trend.delta);
|
|
2471
|
-
const deltaColor = trend.delta > 0 ?
|
|
2472
|
-
console.log(
|
|
2621
|
+
const deltaColor = trend.delta > 0 ? chalk11.green : trend.delta < 0 ? chalk11.red : chalk11.gray;
|
|
2622
|
+
console.log(chalk11.gray("Change from previous:"), deltaColor(deltaStr));
|
|
2473
2623
|
}
|
|
2474
2624
|
return;
|
|
2475
2625
|
} catch (error) {
|
|
2476
|
-
console.error(
|
|
2626
|
+
console.error(chalk11.red("Failed to read openpkg.json:"), error instanceof Error ? error.message : error);
|
|
2477
2627
|
process.exit(1);
|
|
2478
2628
|
}
|
|
2479
2629
|
}
|
|
2480
2630
|
const snapshots = loadSnapshots(cwd);
|
|
2481
2631
|
const limit = parseInt(options.limit ?? "10", 10);
|
|
2482
2632
|
if (snapshots.length === 0) {
|
|
2483
|
-
console.log(
|
|
2484
|
-
console.log(
|
|
2633
|
+
console.log(chalk11.yellow("No coverage history found."));
|
|
2634
|
+
console.log(chalk11.gray("Run `doccov trends --record` to save the current coverage."));
|
|
2485
2635
|
return;
|
|
2486
2636
|
}
|
|
2487
2637
|
if (options.json) {
|
|
@@ -2496,28 +2646,28 @@ function registerTrendsCommand(program) {
|
|
|
2496
2646
|
}
|
|
2497
2647
|
const sparklineData = snapshots.slice(0, 10).map((s) => s.coverageScore).reverse();
|
|
2498
2648
|
const sparkline = renderSparkline(sparklineData);
|
|
2499
|
-
console.log(
|
|
2500
|
-
console.log(
|
|
2501
|
-
console.log(
|
|
2649
|
+
console.log(chalk11.bold("Coverage Trends"));
|
|
2650
|
+
console.log(chalk11.gray(`Package: ${snapshots[0].package}`));
|
|
2651
|
+
console.log(chalk11.gray(`Sparkline: ${sparkline}`));
|
|
2502
2652
|
console.log("");
|
|
2503
2653
|
if (snapshots.length >= 2) {
|
|
2504
2654
|
const oldest = snapshots[snapshots.length - 1];
|
|
2505
2655
|
const newest = snapshots[0];
|
|
2506
2656
|
const overallDelta = newest.coverageScore - oldest.coverageScore;
|
|
2507
2657
|
const deltaStr = formatDelta2(overallDelta);
|
|
2508
|
-
const deltaColor = overallDelta > 0 ?
|
|
2509
|
-
console.log(
|
|
2658
|
+
const deltaColor = overallDelta > 0 ? chalk11.green : overallDelta < 0 ? chalk11.red : chalk11.gray;
|
|
2659
|
+
console.log(chalk11.gray("Overall trend:"), deltaColor(deltaStr), chalk11.gray(`(${snapshots.length} snapshots)`));
|
|
2510
2660
|
console.log("");
|
|
2511
2661
|
}
|
|
2512
2662
|
if (options.extended) {
|
|
2513
|
-
const specPath =
|
|
2514
|
-
if (
|
|
2663
|
+
const specPath = path9.resolve(cwd, "openpkg.json");
|
|
2664
|
+
if (fs7.existsSync(specPath)) {
|
|
2515
2665
|
try {
|
|
2516
|
-
const specContent =
|
|
2666
|
+
const specContent = fs7.readFileSync(specPath, "utf-8");
|
|
2517
2667
|
const spec = JSON.parse(specContent);
|
|
2518
2668
|
const extended = getExtendedTrend(spec, cwd, { tier });
|
|
2519
|
-
console.log(
|
|
2520
|
-
console.log(
|
|
2669
|
+
console.log(chalk11.bold("Extended Analysis"));
|
|
2670
|
+
console.log(chalk11.gray(`Tier: ${tier} (${RETENTION_DAYS[tier]}-day retention)`));
|
|
2521
2671
|
console.log("");
|
|
2522
2672
|
console.log(" Velocity:");
|
|
2523
2673
|
console.log(` 7-day: ${formatVelocity(extended.velocity7d)}`);
|
|
@@ -2526,55 +2676,55 @@ function registerTrendsCommand(program) {
|
|
|
2526
2676
|
console.log(` 90-day: ${formatVelocity(extended.velocity90d)}`);
|
|
2527
2677
|
}
|
|
2528
2678
|
console.log("");
|
|
2529
|
-
const projColor = extended.projected30d >= extended.trend.current.coverageScore ?
|
|
2679
|
+
const projColor = extended.projected30d >= extended.trend.current.coverageScore ? chalk11.green : chalk11.red;
|
|
2530
2680
|
console.log(` Projected (30d): ${projColor(`${extended.projected30d}%`)}`);
|
|
2531
|
-
console.log(` All-time high: ${
|
|
2532
|
-
console.log(` All-time low: ${
|
|
2681
|
+
console.log(` All-time high: ${chalk11.green(`${extended.allTimeHigh}%`)}`);
|
|
2682
|
+
console.log(` All-time low: ${chalk11.red(`${extended.allTimeLow}%`)}`);
|
|
2533
2683
|
if (extended.dataRange) {
|
|
2534
2684
|
const startDate = formatWeekDate(extended.dataRange.start);
|
|
2535
2685
|
const endDate = formatWeekDate(extended.dataRange.end);
|
|
2536
|
-
console.log(
|
|
2686
|
+
console.log(chalk11.gray(` Data range: ${startDate} - ${endDate}`));
|
|
2537
2687
|
}
|
|
2538
2688
|
console.log("");
|
|
2539
2689
|
if (options.weekly && extended.weeklySummaries.length > 0) {
|
|
2540
|
-
console.log(
|
|
2690
|
+
console.log(chalk11.bold("Weekly Summary"));
|
|
2541
2691
|
const weekLimit = Math.min(extended.weeklySummaries.length, 8);
|
|
2542
2692
|
for (let i = 0;i < weekLimit; i++) {
|
|
2543
2693
|
const week = extended.weeklySummaries[i];
|
|
2544
2694
|
const weekStart = formatWeekDate(week.weekStart);
|
|
2545
2695
|
const weekEnd = formatWeekDate(week.weekEnd);
|
|
2546
|
-
const deltaColor = week.delta > 0 ?
|
|
2696
|
+
const deltaColor = week.delta > 0 ? chalk11.green : week.delta < 0 ? chalk11.red : chalk11.gray;
|
|
2547
2697
|
const deltaStr = week.delta > 0 ? `+${week.delta}%` : `${week.delta}%`;
|
|
2548
2698
|
console.log(` ${weekStart} - ${weekEnd}: ${week.avgCoverage}% avg ${deltaColor(deltaStr)} (${week.snapshotCount} snapshots)`);
|
|
2549
2699
|
}
|
|
2550
2700
|
if (extended.weeklySummaries.length > weekLimit) {
|
|
2551
|
-
console.log(
|
|
2701
|
+
console.log(chalk11.gray(` ... and ${extended.weeklySummaries.length - weekLimit} more weeks`));
|
|
2552
2702
|
}
|
|
2553
2703
|
console.log("");
|
|
2554
2704
|
}
|
|
2555
2705
|
} catch {
|
|
2556
|
-
console.log(
|
|
2706
|
+
console.log(chalk11.yellow("Could not load openpkg.json for extended analysis"));
|
|
2557
2707
|
console.log("");
|
|
2558
2708
|
}
|
|
2559
2709
|
}
|
|
2560
2710
|
}
|
|
2561
|
-
console.log(
|
|
2711
|
+
console.log(chalk11.bold("History"));
|
|
2562
2712
|
const displaySnapshots = snapshots.slice(0, limit);
|
|
2563
2713
|
for (let i = 0;i < displaySnapshots.length; i++) {
|
|
2564
2714
|
const snapshot = displaySnapshots[i];
|
|
2565
|
-
const prefix = i === 0 ?
|
|
2715
|
+
const prefix = i === 0 ? chalk11.cyan("→") : " ";
|
|
2566
2716
|
console.log(`${prefix} ${formatSnapshot(snapshot)}`);
|
|
2567
2717
|
}
|
|
2568
2718
|
if (snapshots.length > limit) {
|
|
2569
|
-
console.log(
|
|
2719
|
+
console.log(chalk11.gray(` ... and ${snapshots.length - limit} more`));
|
|
2570
2720
|
}
|
|
2571
2721
|
});
|
|
2572
2722
|
}
|
|
2573
2723
|
|
|
2574
2724
|
// src/cli.ts
|
|
2575
2725
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2576
|
-
var __dirname2 =
|
|
2577
|
-
var packageJson = JSON.parse(readFileSync5(
|
|
2726
|
+
var __dirname2 = path10.dirname(__filename2);
|
|
2727
|
+
var packageJson = JSON.parse(readFileSync5(path10.join(__dirname2, "../package.json"), "utf-8"));
|
|
2578
2728
|
var program = new Command;
|
|
2579
2729
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
2580
2730
|
registerCheckCommand(program);
|