@writechoice/mint-cli 0.0.7 → 0.0.9
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/PUBLISH.md +54 -332
- package/README.md +138 -287
- package/bin/cli.js +58 -8
- package/package.json +5 -4
- package/src/commands/fix/links.js +212 -0
- package/src/commands/validate/links.js +13 -299
- package/src/commands/validate/mdx.js +213 -0
- package/src/utils/config.js +115 -0
- package/src/utils/reports.js +182 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Fix Tool
|
|
3
|
+
*
|
|
4
|
+
* Fixes broken anchor links in MDX files based on validation reports.
|
|
5
|
+
* Reads a report from check links command and applies corrections.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fixes links from a JSON report file
|
|
14
|
+
* @param {string} reportPath - Path to the report file
|
|
15
|
+
* @param {string} repoRoot - Repository root directory
|
|
16
|
+
* @param {boolean} verbose - Show detailed output
|
|
17
|
+
* @returns {Object} Map of filePath -> number of fixes applied
|
|
18
|
+
*/
|
|
19
|
+
function fixLinksFromReport(reportPath, repoRoot, verbose = false) {
|
|
20
|
+
if (!existsSync(reportPath)) {
|
|
21
|
+
console.error(`Error: Report file not found: ${reportPath}`);
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let reportData;
|
|
26
|
+
try {
|
|
27
|
+
reportData = JSON.parse(readFileSync(reportPath, "utf-8"));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(`Error reading report file: ${error.message}`);
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const resultsByFile = reportData.results_by_file || {};
|
|
34
|
+
|
|
35
|
+
if (Object.keys(resultsByFile).length === 0) {
|
|
36
|
+
if (verbose) {
|
|
37
|
+
console.log("No failures found in report.");
|
|
38
|
+
}
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fixesApplied = {};
|
|
43
|
+
|
|
44
|
+
for (const [filePath, failures] of Object.entries(resultsByFile)) {
|
|
45
|
+
const fullPath = join(repoRoot, filePath);
|
|
46
|
+
|
|
47
|
+
if (!existsSync(fullPath)) {
|
|
48
|
+
if (verbose) {
|
|
49
|
+
console.log(`Warning: File not found: ${filePath}`);
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fixableFailures = failures.filter(
|
|
55
|
+
(f) => f.status === "failure" && f.actualHeadingAnchor && f.anchor
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (fixableFailures.length === 0) continue;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
62
|
+
let lines = content.split("\n");
|
|
63
|
+
let modified = false;
|
|
64
|
+
let fixesCount = 0;
|
|
65
|
+
|
|
66
|
+
// Sort by line number descending to avoid line number shifting
|
|
67
|
+
fixableFailures.sort((a, b) => b.source.lineNumber - a.source.lineNumber);
|
|
68
|
+
|
|
69
|
+
for (const failure of fixableFailures) {
|
|
70
|
+
const lineNum = failure.source.lineNumber - 1;
|
|
71
|
+
|
|
72
|
+
if (lineNum >= lines.length) {
|
|
73
|
+
if (verbose) {
|
|
74
|
+
console.log(`Warning: Line ${failure.source.lineNumber} not found in ${filePath}`);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let line = lines[lineNum];
|
|
80
|
+
const oldHref = failure.source.rawHref;
|
|
81
|
+
const newAnchor = failure.actualHeadingAnchor;
|
|
82
|
+
const linkType = failure.source.linkType;
|
|
83
|
+
|
|
84
|
+
const pathPart = oldHref.includes("#") ? oldHref.split("#")[0] : oldHref;
|
|
85
|
+
const newHref = pathPart ? `${pathPart}#${newAnchor}` : `#${newAnchor}`;
|
|
86
|
+
|
|
87
|
+
if (oldHref === newHref) {
|
|
88
|
+
if (verbose) {
|
|
89
|
+
console.log(`Skipping ${filePath}:${failure.source.lineNumber} (no change needed)`);
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let replaced = false;
|
|
95
|
+
|
|
96
|
+
if (linkType === "markdown") {
|
|
97
|
+
const oldPattern = `(${oldHref})`;
|
|
98
|
+
const newPattern = `(${newHref})`;
|
|
99
|
+
if (line.includes(oldPattern)) {
|
|
100
|
+
line = line.replace(oldPattern, newPattern);
|
|
101
|
+
replaced = true;
|
|
102
|
+
}
|
|
103
|
+
} else if (linkType === "html" || linkType === "jsx") {
|
|
104
|
+
for (const quote of ['"', "'"]) {
|
|
105
|
+
const oldPattern = `href=${quote}${oldHref}${quote}`;
|
|
106
|
+
const newPattern = `href=${quote}${newHref}${quote}`;
|
|
107
|
+
if (line.includes(oldPattern)) {
|
|
108
|
+
line = line.replace(oldPattern, newPattern);
|
|
109
|
+
replaced = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (replaced) {
|
|
116
|
+
lines[lineNum] = line;
|
|
117
|
+
modified = true;
|
|
118
|
+
fixesCount++;
|
|
119
|
+
|
|
120
|
+
if (verbose) {
|
|
121
|
+
console.log(`Fixed ${filePath}:${failure.source.lineNumber}`);
|
|
122
|
+
console.log(` Old: ${oldHref}`);
|
|
123
|
+
console.log(` New: ${newHref}`);
|
|
124
|
+
}
|
|
125
|
+
} else if (verbose) {
|
|
126
|
+
console.log(
|
|
127
|
+
`Warning: Could not find href '${oldHref}' on line ${failure.source.lineNumber} in ${filePath}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (modified) {
|
|
133
|
+
const newContent = lines.join("\n");
|
|
134
|
+
writeFileSync(fullPath, newContent, "utf-8");
|
|
135
|
+
fixesApplied[filePath] = fixesCount;
|
|
136
|
+
|
|
137
|
+
if (verbose) {
|
|
138
|
+
console.log(`Saved ${fixesCount} fix(es) to ${filePath}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (verbose) {
|
|
143
|
+
console.log(`Error fixing ${filePath}: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return fixesApplied;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Main CLI function for fixing links
|
|
153
|
+
* @param {Object} options - CLI options
|
|
154
|
+
*/
|
|
155
|
+
export async function fixLinks(options) {
|
|
156
|
+
const repoRoot = process.cwd();
|
|
157
|
+
|
|
158
|
+
// Determine report path
|
|
159
|
+
const reportPath = options.report || "links_report.json";
|
|
160
|
+
|
|
161
|
+
// Validate that the report file exists and is JSON
|
|
162
|
+
if (!existsSync(reportPath)) {
|
|
163
|
+
console.error(chalk.red(`\n✗ Error: Report file not found: ${reportPath}`));
|
|
164
|
+
|
|
165
|
+
// Check if user might have provided a .md file instead
|
|
166
|
+
if (reportPath.endsWith('.md')) {
|
|
167
|
+
const jsonPath = reportPath.replace(/\.md$/, '.json');
|
|
168
|
+
console.error(chalk.yellow(`\n⚠️ The fix command requires a JSON report file.`));
|
|
169
|
+
console.error(chalk.yellow(`Try using: ${chalk.cyan(jsonPath)}`));
|
|
170
|
+
} else {
|
|
171
|
+
console.error(chalk.yellow(`\n⚠️ Make sure to run the validation command first:`));
|
|
172
|
+
console.error(chalk.gray(` writechoice check links <baseUrl>`));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check if it's a JSON file
|
|
179
|
+
if (!reportPath.endsWith('.json')) {
|
|
180
|
+
console.error(chalk.red(`\n✗ Error: The fix command requires a JSON report file.`));
|
|
181
|
+
console.error(chalk.yellow(`\nProvided file: ${reportPath}`));
|
|
182
|
+
|
|
183
|
+
if (reportPath.endsWith('.md')) {
|
|
184
|
+
const jsonPath = reportPath.replace(/\.md$/, '.json');
|
|
185
|
+
console.error(chalk.yellow(`\nThe markdown (.md) report is for human readability only.`));
|
|
186
|
+
console.error(chalk.yellow(`Please use the JSON report instead: ${chalk.cyan(jsonPath)}`));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!options.quiet) {
|
|
193
|
+
console.log(chalk.bold("\n🔧 Link Fixer\n"));
|
|
194
|
+
console.log(`Reading report: ${chalk.cyan(reportPath)}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const fixesApplied = fixLinksFromReport(reportPath, repoRoot, options.verbose && !options.quiet);
|
|
198
|
+
|
|
199
|
+
if (!options.quiet) {
|
|
200
|
+
if (Object.keys(fixesApplied).length > 0) {
|
|
201
|
+
const totalFixes = Object.values(fixesApplied).reduce((a, b) => a + b, 0);
|
|
202
|
+
console.log(chalk.green(`\n✓ Fixed ${totalFixes} link(s) in ${Object.keys(fixesApplied).length} file(s):`));
|
|
203
|
+
for (const [filePath, count] of Object.entries(fixesApplied)) {
|
|
204
|
+
console.log(` ${chalk.cyan(filePath)}: ${count} fix(es)`);
|
|
205
|
+
}
|
|
206
|
+
console.log(chalk.yellow("\n⚠️ Run validation again to verify the fixes:"));
|
|
207
|
+
console.log(chalk.gray(" writechoice check links <baseUrl>"));
|
|
208
|
+
} else {
|
|
209
|
+
console.log(chalk.yellow("\n⚠️ No fixable issues found in report."));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* JavaScript-rendered Mintlify pages.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { readFileSync,
|
|
9
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
10
10
|
import { join, relative, resolve, dirname } from "path";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
import { chromium } from "playwright";
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
removeCodeBlocksAndFrontmatter,
|
|
22
22
|
resolvePath as resolvePathUtil,
|
|
23
23
|
} from "../../utils/helpers.js";
|
|
24
|
+
import { writeBothFormats, generateLinksMarkdown } from "../../utils/reports.js";
|
|
24
25
|
|
|
25
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
27
|
const __dirname = dirname(__filename);
|
|
@@ -860,253 +861,9 @@ async function validateLinksAsync(links, baseUrl, validationBaseUrl, repoRoot, c
|
|
|
860
861
|
return results;
|
|
861
862
|
}
|
|
862
863
|
|
|
863
|
-
// Fix Links in MDX Files
|
|
864
|
-
|
|
865
|
-
function fixLinksFromReport(reportPath, repoRoot, verbose = false) {
|
|
866
|
-
if (!existsSync(reportPath)) {
|
|
867
|
-
console.error(`Error: Report file not found: ${reportPath}`);
|
|
868
|
-
return {};
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
let reportData;
|
|
872
|
-
try {
|
|
873
|
-
reportData = JSON.parse(readFileSync(reportPath, "utf-8"));
|
|
874
|
-
} catch (error) {
|
|
875
|
-
console.error(`Error reading report file: ${error.message}`);
|
|
876
|
-
return {};
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
const resultsByFile = reportData.results_by_file || {};
|
|
880
|
-
|
|
881
|
-
if (Object.keys(resultsByFile).length === 0) {
|
|
882
|
-
if (verbose) {
|
|
883
|
-
console.log("No failures found in report.");
|
|
884
|
-
}
|
|
885
|
-
return {};
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
const fixesApplied = {};
|
|
889
|
-
|
|
890
|
-
for (const [filePath, failures] of Object.entries(resultsByFile)) {
|
|
891
|
-
const fullPath = join(repoRoot, filePath);
|
|
892
|
-
|
|
893
|
-
if (!existsSync(fullPath)) {
|
|
894
|
-
if (verbose) {
|
|
895
|
-
console.log(`Warning: File not found: ${filePath}`);
|
|
896
|
-
}
|
|
897
|
-
continue;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const fixableFailures = failures.filter((f) => f.status === "failure" && f.actual_heading_kebab && f.anchor);
|
|
901
|
-
|
|
902
|
-
if (fixableFailures.length === 0) continue;
|
|
903
|
-
|
|
904
|
-
try {
|
|
905
|
-
const content = readFileSync(fullPath, "utf-8");
|
|
906
|
-
let lines = content.split("\n");
|
|
907
|
-
let modified = false;
|
|
908
|
-
let fixesCount = 0;
|
|
909
|
-
|
|
910
|
-
fixableFailures.sort((a, b) => b.source.line_number - a.source.line_number);
|
|
911
|
-
|
|
912
|
-
for (const failure of fixableFailures) {
|
|
913
|
-
const lineNum = failure.source.line_number - 1;
|
|
914
|
-
|
|
915
|
-
if (lineNum >= lines.length) {
|
|
916
|
-
if (verbose) {
|
|
917
|
-
console.log(`Warning: Line ${failure.source.line_number} not found in ${filePath}`);
|
|
918
|
-
}
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
let line = lines[lineNum];
|
|
923
|
-
const oldHref = failure.source.raw_href;
|
|
924
|
-
const newAnchor = failure.actual_heading_kebab;
|
|
925
|
-
const linkType = failure.source.link_type;
|
|
926
|
-
|
|
927
|
-
const pathPart = oldHref.includes("#") ? oldHref.split("#")[0] : oldHref;
|
|
928
|
-
const newHref = pathPart ? `${pathPart}#${newAnchor}` : `#${newAnchor}`;
|
|
929
|
-
|
|
930
|
-
if (oldHref === newHref) {
|
|
931
|
-
if (verbose) {
|
|
932
|
-
console.log(`Skipping ${filePath}:${failure.source.line_number} (no change needed)`);
|
|
933
|
-
}
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
let replaced = false;
|
|
938
|
-
|
|
939
|
-
if (linkType === "markdown") {
|
|
940
|
-
const oldPattern = `(${oldHref})`;
|
|
941
|
-
const newPattern = `(${newHref})`;
|
|
942
|
-
if (line.includes(oldPattern)) {
|
|
943
|
-
line = line.replace(oldPattern, newPattern);
|
|
944
|
-
replaced = true;
|
|
945
|
-
}
|
|
946
|
-
} else if (linkType === "html" || linkType === "jsx") {
|
|
947
|
-
for (const quote of ['"', "'"]) {
|
|
948
|
-
const oldPattern = `href=${quote}${oldHref}${quote}`;
|
|
949
|
-
const newPattern = `href=${quote}${newHref}${quote}`;
|
|
950
|
-
if (line.includes(oldPattern)) {
|
|
951
|
-
line = line.replace(oldPattern, newPattern);
|
|
952
|
-
replaced = true;
|
|
953
|
-
break;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
if (replaced) {
|
|
959
|
-
lines[lineNum] = line;
|
|
960
|
-
modified = true;
|
|
961
|
-
fixesCount++;
|
|
962
|
-
|
|
963
|
-
if (verbose) {
|
|
964
|
-
console.log(`Fixed ${filePath}:${failure.source.line_number}`);
|
|
965
|
-
console.log(` Old: ${oldHref}`);
|
|
966
|
-
console.log(` New: ${newHref}`);
|
|
967
|
-
}
|
|
968
|
-
} else if (verbose) {
|
|
969
|
-
console.log(`Warning: Could not find href '${oldHref}' on line ${failure.source.line_number} in ${filePath}`);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
if (modified) {
|
|
974
|
-
const newContent = lines.join("\n");
|
|
975
|
-
writeFileSync(fullPath, newContent, "utf-8");
|
|
976
|
-
fixesApplied[filePath] = fixesCount;
|
|
977
|
-
|
|
978
|
-
if (verbose) {
|
|
979
|
-
console.log(`Saved ${fixesCount} fix(es) to ${filePath}`);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
} catch (error) {
|
|
983
|
-
if (verbose) {
|
|
984
|
-
console.log(`Error fixing ${filePath}: ${error.message}`);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
return fixesApplied;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
function fixLinks(results, repoRoot, verbose = false) {
|
|
993
|
-
const failuresByFile = {};
|
|
994
|
-
|
|
995
|
-
for (const result of results) {
|
|
996
|
-
if (result.status !== "failure" || !result.actualHeadingAnchor || !result.anchor) {
|
|
997
|
-
continue;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
const filePath = result.source.filePath;
|
|
1001
|
-
if (!failuresByFile[filePath]) {
|
|
1002
|
-
failuresByFile[filePath] = [];
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
failuresByFile[filePath].push(result);
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
const fixesApplied = {};
|
|
1009
|
-
|
|
1010
|
-
for (const [filePath, failures] of Object.entries(failuresByFile)) {
|
|
1011
|
-
const fullPath = join(repoRoot, filePath);
|
|
1012
|
-
|
|
1013
|
-
if (!existsSync(fullPath)) {
|
|
1014
|
-
if (verbose) {
|
|
1015
|
-
console.log(`Warning: File not found: ${filePath}`);
|
|
1016
|
-
}
|
|
1017
|
-
continue;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
try {
|
|
1021
|
-
const content = readFileSync(fullPath, "utf-8");
|
|
1022
|
-
let lines = content.split("\n");
|
|
1023
|
-
let modified = false;
|
|
1024
|
-
let fixesCount = 0;
|
|
1025
|
-
|
|
1026
|
-
failures.sort((a, b) => b.source.lineNumber - a.source.lineNumber);
|
|
1027
|
-
|
|
1028
|
-
for (const failure of failures) {
|
|
1029
|
-
const lineNum = failure.source.lineNumber - 1;
|
|
1030
|
-
|
|
1031
|
-
if (lineNum >= lines.length) {
|
|
1032
|
-
if (verbose) {
|
|
1033
|
-
console.log(`Warning: Line ${failure.source.lineNumber} not found in ${filePath}`);
|
|
1034
|
-
}
|
|
1035
|
-
continue;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
let line = lines[lineNum];
|
|
1039
|
-
const oldHref = failure.source.rawHref;
|
|
1040
|
-
const linkType = failure.source.linkType;
|
|
1041
|
-
|
|
1042
|
-
const pathPart = oldHref.includes("#") ? oldHref.split("#")[0] : oldHref;
|
|
1043
|
-
const newHref = pathPart ? `${pathPart}#${failure.actualHeadingAnchor}` : `#${failure.actualHeadingAnchor}`;
|
|
1044
|
-
|
|
1045
|
-
if (oldHref === newHref) {
|
|
1046
|
-
if (verbose) {
|
|
1047
|
-
console.log(`Skipping ${filePath}:${failure.source.lineNumber} (no change needed)`);
|
|
1048
|
-
}
|
|
1049
|
-
continue;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
let replaced = false;
|
|
1053
|
-
|
|
1054
|
-
if (linkType === "markdown") {
|
|
1055
|
-
const oldPattern = `(${oldHref})`;
|
|
1056
|
-
const newPattern = `(${newHref})`;
|
|
1057
|
-
if (line.includes(oldPattern)) {
|
|
1058
|
-
line = line.replace(oldPattern, newPattern);
|
|
1059
|
-
replaced = true;
|
|
1060
|
-
}
|
|
1061
|
-
} else if (linkType === "html" || linkType === "jsx") {
|
|
1062
|
-
for (const quote of ['"', "'"]) {
|
|
1063
|
-
const oldPattern = `href=${quote}${oldHref}${quote}`;
|
|
1064
|
-
const newPattern = `href=${quote}${newHref}${quote}`;
|
|
1065
|
-
if (line.includes(oldPattern)) {
|
|
1066
|
-
line = line.replace(oldPattern, newPattern);
|
|
1067
|
-
replaced = true;
|
|
1068
|
-
break;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
if (replaced) {
|
|
1074
|
-
lines[lineNum] = line;
|
|
1075
|
-
modified = true;
|
|
1076
|
-
fixesCount++;
|
|
1077
|
-
|
|
1078
|
-
if (verbose) {
|
|
1079
|
-
console.log(`Fixed ${filePath}:${failure.source.lineNumber}`);
|
|
1080
|
-
console.log(` Old: ${oldHref}`);
|
|
1081
|
-
console.log(` New: ${newHref}`);
|
|
1082
|
-
}
|
|
1083
|
-
} else if (verbose) {
|
|
1084
|
-
console.log(`Warning: Could not find href '${oldHref}' on line ${failure.source.lineNumber} in ${filePath}`);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
if (modified) {
|
|
1089
|
-
const newContent = lines.join("\n");
|
|
1090
|
-
writeFileSync(fullPath, newContent, "utf-8");
|
|
1091
|
-
fixesApplied[filePath] = fixesCount;
|
|
1092
|
-
|
|
1093
|
-
if (verbose) {
|
|
1094
|
-
console.log(`Saved ${fixesCount} fix(es) to ${filePath}`);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
} catch (error) {
|
|
1098
|
-
if (verbose) {
|
|
1099
|
-
console.log(`Error fixing ${filePath}: ${error.message}`);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
return fixesApplied;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
864
|
// Report Generation
|
|
1108
865
|
|
|
1109
|
-
function generateReport(results, config,
|
|
866
|
+
function generateReport(results, config, outputBaseName, repoRoot) {
|
|
1110
867
|
const total = results.length;
|
|
1111
868
|
const success = results.filter((r) => r.status === "success").length;
|
|
1112
869
|
const failure = results.filter((r) => r.status === "failure").length;
|
|
@@ -1147,9 +904,13 @@ function generateReport(results, config, outputPath) {
|
|
|
1147
904
|
results_by_file: resultsByFile,
|
|
1148
905
|
};
|
|
1149
906
|
|
|
1150
|
-
|
|
907
|
+
// Always generate markdown content for the MD report
|
|
908
|
+
report.markdownContent = generateLinksMarkdown(report);
|
|
909
|
+
|
|
910
|
+
// Write both JSON and MD reports
|
|
911
|
+
const { jsonPath, mdPath } = writeBothFormats(report, outputBaseName, repoRoot);
|
|
1151
912
|
|
|
1152
|
-
return report;
|
|
913
|
+
return { report, jsonPath, mdPath };
|
|
1153
914
|
}
|
|
1154
915
|
|
|
1155
916
|
// Main CLI Function
|
|
@@ -1157,34 +918,6 @@ function generateReport(results, config, outputPath) {
|
|
|
1157
918
|
export async function validateLinks(baseUrl, options) {
|
|
1158
919
|
const repoRoot = process.cwd();
|
|
1159
920
|
|
|
1160
|
-
// Handle --fix-from-report mode
|
|
1161
|
-
if (options.fixFromReport !== undefined) {
|
|
1162
|
-
// If flag is passed with a path, use that path; otherwise use default
|
|
1163
|
-
const reportPath =
|
|
1164
|
-
typeof options.fixFromReport === "string" && options.fixFromReport ? options.fixFromReport : "links_report.json";
|
|
1165
|
-
|
|
1166
|
-
if (!options.quiet) {
|
|
1167
|
-
console.log(`Applying fixes from report: ${reportPath}`);
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
const fixesApplied = fixLinksFromReport(reportPath, repoRoot, options.verbose && !options.quiet);
|
|
1171
|
-
|
|
1172
|
-
if (!options.quiet) {
|
|
1173
|
-
if (Object.keys(fixesApplied).length > 0) {
|
|
1174
|
-
const totalFixes = Object.values(fixesApplied).reduce((a, b) => a + b, 0);
|
|
1175
|
-
console.log(`\nFixed ${totalFixes} link(s) in ${Object.keys(fixesApplied).length} file(s):`);
|
|
1176
|
-
for (const [filePath, count] of Object.entries(fixesApplied)) {
|
|
1177
|
-
console.log(` ${filePath}: ${count} fix(es)`);
|
|
1178
|
-
}
|
|
1179
|
-
console.log("\nRun validation again to verify the fixes.");
|
|
1180
|
-
} else {
|
|
1181
|
-
console.log("\nNo fixable issues found in report.");
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
921
|
// Normalize base URL - add https:// if not present
|
|
1189
922
|
let normalizedBaseUrl = baseUrl;
|
|
1190
923
|
if (!normalizedBaseUrl.startsWith("http://") && !normalizedBaseUrl.startsWith("https://")) {
|
|
@@ -1277,27 +1010,6 @@ export async function validateLinks(baseUrl, options) {
|
|
|
1277
1010
|
|
|
1278
1011
|
const executionTime = (Date.now() - startTime) / 1000;
|
|
1279
1012
|
|
|
1280
|
-
if (options.fix) {
|
|
1281
|
-
if (!options.quiet) {
|
|
1282
|
-
console.log("\nApplying fixes...");
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
const fixesApplied = fixLinks(results, repoRoot, options.verbose && !options.quiet);
|
|
1286
|
-
|
|
1287
|
-
if (!options.quiet) {
|
|
1288
|
-
if (Object.keys(fixesApplied).length > 0) {
|
|
1289
|
-
const totalFixes = Object.values(fixesApplied).reduce((a, b) => a + b, 0);
|
|
1290
|
-
console.log(`\nFixed ${totalFixes} link(s) in ${Object.keys(fixesApplied).length} file(s):`);
|
|
1291
|
-
for (const [filePath, count] of Object.entries(fixesApplied)) {
|
|
1292
|
-
console.log(` ${filePath}: ${count} fix(es)`);
|
|
1293
|
-
}
|
|
1294
|
-
console.log("\nRun validation again to verify the fixes.");
|
|
1295
|
-
} else {
|
|
1296
|
-
console.log("\nNo fixable issues found.");
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
1013
|
const config = {
|
|
1302
1014
|
base_url: normalizedBaseUrl,
|
|
1303
1015
|
scanned_directories: options.dir || options.file ? [options.dir || options.file] : MDX_DIRS,
|
|
@@ -1306,7 +1018,7 @@ export async function validateLinks(baseUrl, options) {
|
|
|
1306
1018
|
execution_time_seconds: Math.round(executionTime * 100) / 100,
|
|
1307
1019
|
};
|
|
1308
1020
|
|
|
1309
|
-
const report = generateReport(results, config, options.output || "links_report
|
|
1021
|
+
const { report, jsonPath, mdPath } = generateReport(results, config, options.output || "links_report", repoRoot);
|
|
1310
1022
|
|
|
1311
1023
|
if (!options.quiet) {
|
|
1312
1024
|
console.log(`\n${"=".repeat(60)}`);
|
|
@@ -1317,7 +1029,9 @@ export async function validateLinks(baseUrl, options) {
|
|
|
1317
1029
|
console.log(`Failure: ${chalk.red(report.summary.failure + " ✗")}`);
|
|
1318
1030
|
console.log(`Error: ${chalk.yellow(report.summary.error + " ⚠")}`);
|
|
1319
1031
|
console.log(`Execution time: ${executionTime.toFixed(2)}s`);
|
|
1320
|
-
console.log(`\
|
|
1032
|
+
console.log(`\nReports saved to:`);
|
|
1033
|
+
console.log(` JSON: ${jsonPath}`);
|
|
1034
|
+
console.log(` MD: ${mdPath}`);
|
|
1321
1035
|
|
|
1322
1036
|
if (report.summary.failure > 0 || report.summary.error > 0) {
|
|
1323
1037
|
console.log(`\n${"=".repeat(60)}`);
|