@writechoice/mint-cli 0.0.8 → 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.
@@ -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, writeFileSync, existsSync, readdirSync, statSync } from "fs";
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, outputPath) {
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
- writeFileSync(outputPath, JSON.stringify(report, null, 2), "utf-8");
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.json");
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(`\nReport saved to: ${options.output || "links_report.json"}`);
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)}`);