@mui/internal-code-infra 0.0.4-canary.3 → 0.0.4-canary.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +19 -8
  2. package/build/babel-config.d.mts +11 -3
  3. package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
  4. package/build/brokenLinksChecker/index.d.mts +35 -2
  5. package/build/changelog/types.d.ts +1 -1
  6. package/build/cli/cmdArgosPush.d.mts +2 -2
  7. package/build/cli/cmdBuild.d.mts +2 -2
  8. package/build/cli/cmdCopyFiles.d.mts +2 -2
  9. package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
  10. package/build/cli/cmdGenerateChangelog.d.mts +2 -2
  11. package/build/cli/cmdGithubAuth.d.mts +2 -2
  12. package/build/cli/cmdListWorkspaces.d.mts +4 -2
  13. package/build/cli/cmdNetlifyIgnore.d.mts +2 -2
  14. package/build/cli/cmdPublish.d.mts +4 -2
  15. package/build/cli/cmdPublishCanary.d.mts +3 -2
  16. package/build/cli/cmdPublishNewPackage.d.mts +4 -2
  17. package/build/cli/cmdSetVersionOverrides.d.mts +2 -2
  18. package/build/cli/cmdVale.d.mts +46 -0
  19. package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
  20. package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
  21. package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
  22. package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +2 -2
  23. package/build/markdownlint/duplicate-h1.d.mts +1 -1
  24. package/build/markdownlint/git-diff.d.mts +1 -1
  25. package/build/markdownlint/index.d.mts +1 -1
  26. package/build/markdownlint/straight-quotes.d.mts +1 -1
  27. package/build/markdownlint/table-alignment.d.mts +1 -1
  28. package/build/markdownlint/terminal-language.d.mts +1 -1
  29. package/build/utils/build.d.mts +3 -3
  30. package/build/utils/github.d.mts +1 -1
  31. package/build/utils/pnpm.d.mts +68 -2
  32. package/build/utils/testUtils.d.mts +7 -0
  33. package/package.json +38 -31
  34. package/src/babel-config.mjs +9 -3
  35. package/src/brokenLinksChecker/__fixtures__/static-site/index.html +1 -0
  36. package/src/brokenLinksChecker/__fixtures__/static-site/invalid-html.html +15 -0
  37. package/src/brokenLinksChecker/crawlWorker.mjs +173 -0
  38. package/src/brokenLinksChecker/index.mjs +177 -164
  39. package/src/brokenLinksChecker/index.test.ts +55 -13
  40. package/src/build-env.d.ts +13 -0
  41. package/src/changelog/fetchChangelogs.mjs +6 -2
  42. package/src/changelog/types.ts +1 -1
  43. package/src/cli/cmdListWorkspaces.mjs +9 -2
  44. package/src/cli/cmdNetlifyIgnore.mjs +4 -88
  45. package/src/cli/cmdPublish.mjs +51 -14
  46. package/src/cli/cmdPublishCanary.mjs +139 -107
  47. package/src/cli/cmdPublishNewPackage.mjs +27 -6
  48. package/src/cli/cmdVale.mjs +513 -0
  49. package/src/cli/cmdVale.test.mjs +644 -0
  50. package/src/cli/index.mjs +2 -0
  51. package/src/eslint/baseConfig.mjs +2 -1
  52. package/src/eslint/docsConfig.mjs +2 -1
  53. package/src/eslint/jsonConfig.mjs +2 -1
  54. package/src/eslint/mui/config.mjs +11 -1
  55. package/src/eslint/testConfig.mjs +2 -1
  56. package/src/estree-typescript.d.ts +1 -1
  57. package/src/untyped-plugins.d.ts +11 -11
  58. package/src/utils/build.test.mjs +546 -575
  59. package/src/utils/pnpm.mjs +192 -3
  60. package/src/utils/pnpm.test.mjs +580 -0
  61. package/src/utils/testUtils.mjs +18 -0
  62. package/src/utils/typescript.test.mjs +249 -272
  63. package/vale/.vale.ini +1 -0
  64. package/vale/styles/MUI/CorrectReferenceAllCases.yml +43 -0
  65. package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
  66. package/vale/styles/MUI/GoogleLatin.yml +11 -0
  67. package/vale/styles/MUI/MuiBrandName.yml +22 -0
  68. package/vale/styles/MUI/NoBritish.yml +112 -0
  69. package/vale/styles/MUI/NoCompanyName.yml +17 -0
@@ -0,0 +1,513 @@
1
+ /* eslint-disable no-console */
2
+ import * as crypto from 'node:crypto';
3
+ import * as fs from 'node:fs/promises';
4
+ import { createWriteStream } from 'node:fs';
5
+ import * as os from 'node:os';
6
+ import * as path from 'node:path';
7
+ import { pipeline } from 'node:stream/promises';
8
+ import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
9
+ import { $ } from 'execa';
10
+ import { mapConcurrently } from '../utils/build.mjs';
11
+
12
+ const LATEST_RELEASE_URL = 'https://api.github.com/repos/vale-cli/vale/releases/latest';
13
+
14
+ /**
15
+ * Fetches the latest vale release version tag from GitHub.
16
+ * @returns {Promise<string>}
17
+ */
18
+ async function fetchLatestVersion() {
19
+ const response = await fetchOrThrow(LATEST_RELEASE_URL);
20
+ const data = /** @type {{ tag_name: string }} */ (await response.json());
21
+ // tag_name is like "v3.14.1", strip the leading "v"
22
+ return data.tag_name.replace(/^v/, '');
23
+ }
24
+
25
+ /**
26
+ * @typedef {'Linux' | 'macOS' | 'Windows'} ValeOS
27
+ * @typedef {'64-bit' | 'arm64'} ValeArch
28
+ */
29
+
30
+ /**
31
+ * Detects the OS name used in vale release filenames.
32
+ * @returns {ValeOS}
33
+ */
34
+ function detectOS() {
35
+ const platform = os.platform();
36
+ if (platform === 'linux') {
37
+ return 'Linux';
38
+ }
39
+ if (platform === 'darwin') {
40
+ return 'macOS';
41
+ }
42
+ if (platform === 'win32') {
43
+ return 'Windows';
44
+ }
45
+ throw new Error(
46
+ `Unsupported platform: ${platform}. Vale only supports Linux, macOS, and Windows.`,
47
+ );
48
+ }
49
+
50
+ /**
51
+ * Detects the CPU architecture used in vale release filenames.
52
+ * Exits with an error for 32-bit systems.
53
+ * @returns {ValeArch}
54
+ */
55
+ function detectArch() {
56
+ const arch = os.arch();
57
+ if (arch === 'arm64') {
58
+ return 'arm64';
59
+ }
60
+ if (arch === 'x64') {
61
+ return '64-bit';
62
+ }
63
+ throw new Error(
64
+ `Unsupported architecture: ${arch}. Vale requires a 64-bit system (x64 or arm64).`,
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Returns the archive filename for the current platform.
70
+ * @param {ValeOS} valeOS
71
+ * @param {ValeArch} valeArch
72
+ * @returns {{ filename: string; isZip: boolean }}
73
+ */
74
+ /**
75
+ * Returns the base GitHub release download URL for a given vale version.
76
+ * @param {string} version
77
+ * @returns {string}
78
+ */
79
+ function getReleasesBase(version) {
80
+ return `https://github.com/vale-cli/vale/releases/download/v${version}`;
81
+ }
82
+
83
+ /**
84
+ * Returns the archive filename for the current platform.
85
+ * @param {string} version
86
+ * @param {ValeOS} valeOS
87
+ * @param {ValeArch} valeArch
88
+ * @returns {{ filename: string; isZip: boolean }}
89
+ */
90
+ function getArchiveInfo(version, valeOS, valeArch) {
91
+ const isZip = valeOS === 'Windows';
92
+ const ext = isZip ? '.zip' : '.tar.gz';
93
+ const filename = `vale_${version}_${valeOS}_${valeArch}${ext}`;
94
+ return { filename, isZip };
95
+ }
96
+
97
+ /**
98
+ * Fetches a URL and returns the Response, throwing on non-OK status.
99
+ * @param {string} url
100
+ * @returns {Promise<Response>}
101
+ */
102
+ async function fetchOrThrow(url) {
103
+ const response = await fetch(url);
104
+ if (!response.ok) {
105
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
106
+ }
107
+ return response;
108
+ }
109
+
110
+ /**
111
+ * Downloads the checksums file and parses it into a map of filename -> sha256 hex.
112
+ * @param {string} version
113
+ * @returns {Promise<Map<string, string>>}
114
+ */
115
+ async function fetchChecksums(version) {
116
+ const checksumsUrl = `${getReleasesBase(version)}/vale_${version}_checksums.txt`;
117
+ const response = await fetchOrThrow(checksumsUrl);
118
+ const text = await response.text();
119
+ /** @type {Map<string, string>} */
120
+ const map = new Map();
121
+ for (const line of text.trim().split('\n')) {
122
+ const [hash, name] = line.trim().split(/\s+/);
123
+ if (hash && name) {
124
+ map.set(name, hash);
125
+ }
126
+ }
127
+ return map;
128
+ }
129
+
130
+ /**
131
+ * Computes the SHA-256 hex digest of a file.
132
+ * @param {string} filePath
133
+ * @returns {Promise<string>}
134
+ */
135
+ async function sha256File(filePath) {
136
+ const hash = crypto.createHash('sha256');
137
+ const fileHandle = await fs.open(filePath, 'r');
138
+ const stream = fileHandle.createReadStream();
139
+ await pipeline(stream, hash);
140
+ return hash.digest('hex');
141
+ }
142
+
143
+ /**
144
+ * Downloads a URL to a local file path, showing progress.
145
+ * @param {string} url
146
+ * @param {string} destPath
147
+ * @param {string} version
148
+ * @returns {Promise<void>}
149
+ */
150
+ async function downloadFile(url, destPath, version) {
151
+ const response = await fetchOrThrow(url);
152
+ const totalBytes = Number(response.headers.get('content-length') ?? 0);
153
+ let downloadedBytes = 0;
154
+
155
+ if (!response.body) {
156
+ throw new Error(`No response body for ${url}`);
157
+ }
158
+
159
+ const writeStream = createWriteStream(destPath);
160
+
161
+ // Pipe the web ReadableStream into a Node.js WriteStream, tracking progress
162
+ const nodeReadable = /** @type {import('node:stream').Readable} */ (
163
+ /** @type {unknown} */ (response.body)
164
+ );
165
+
166
+ await pipeline(
167
+ nodeReadable,
168
+ async function* trackProgress(source) {
169
+ for await (const chunk of source) {
170
+ const bytes = /** @type {Buffer} */ (chunk);
171
+ downloadedBytes += bytes.length;
172
+ if (totalBytes > 0) {
173
+ const pct = ((downloadedBytes / totalBytes) * 100).toFixed(1);
174
+ process.stdout.write(`\rDownloading vale v${version}... ${pct}%`);
175
+ }
176
+ yield bytes;
177
+ }
178
+ },
179
+ writeStream,
180
+ );
181
+
182
+ if (totalBytes > 0) {
183
+ process.stdout.write('\n');
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Extracts a .tar.gz archive to a destination directory using the built-in tar command.
189
+ * @param {string} archivePath
190
+ * @param {string} destDir
191
+ * @returns {Promise<void>}
192
+ */
193
+ async function extractTarGz(archivePath, destDir) {
194
+ await $({ stdio: 'inherit' })`tar -xzf ${archivePath} -C ${destDir}`;
195
+ }
196
+
197
+ /**
198
+ * Extracts a .zip archive to a destination directory using the built-in unzip command.
199
+ * @param {string} archivePath
200
+ * @param {string} destDir
201
+ * @returns {Promise<void>}
202
+ */
203
+ async function extractZip(archivePath, destDir) {
204
+ await $({ stdio: 'inherit' })`unzip -o ${archivePath} -d ${destDir}`;
205
+ }
206
+
207
+ /**
208
+ * Returns the path to the vale binary, downloading it first if necessary.
209
+ * The binary is cached under `node_modules/.cache/mui-vale/<version>/`.
210
+ * @param {string} version
211
+ * @returns {Promise<string>}
212
+ */
213
+ async function ensureValeBinary(version) {
214
+ const valeOS = detectOS();
215
+ const valeArch = detectArch();
216
+ const { filename, isZip } = getArchiveInfo(version, valeOS, valeArch);
217
+
218
+ const binaryName = valeOS === 'Windows' ? 'vale.exe' : 'vale';
219
+ const workspaceDir = await findWorkspaceDir(process.cwd());
220
+ if (!workspaceDir) {
221
+ throw new Error('Could not find pnpm workspace root');
222
+ }
223
+ const cacheDir = path.join(workspaceDir, 'node_modules', '.cache', 'mui-vale', version);
224
+ const binaryPath = path.join(cacheDir, binaryName);
225
+
226
+ // Return cached binary if it already exists
227
+ const binaryExists = await fs
228
+ .stat(binaryPath)
229
+ .then((s) => s.isFile())
230
+ .catch(() => false);
231
+ if (binaryExists) {
232
+ return binaryPath;
233
+ }
234
+
235
+ console.log(`Vale v${version} not found in cache. Downloading...`);
236
+
237
+ // Fetch checksums first
238
+ const checksums = await fetchChecksums(version);
239
+ const expectedChecksum = checksums.get(filename);
240
+ if (!expectedChecksum) {
241
+ throw new Error(`No checksum found for ${filename} in checksums file.`);
242
+ }
243
+
244
+ await fs.mkdir(cacheDir, { recursive: true });
245
+
246
+ const archivePath = path.join(cacheDir, filename);
247
+
248
+ try {
249
+ const downloadUrl = `${getReleasesBase(version)}/${filename}`;
250
+ await downloadFile(downloadUrl, archivePath, version);
251
+
252
+ // Verify checksum
253
+ console.log('Verifying checksum...');
254
+ const actualChecksum = await sha256File(archivePath);
255
+ if (actualChecksum !== expectedChecksum) {
256
+ throw new Error(
257
+ `Checksum mismatch for ${filename}.\n Expected: ${expectedChecksum}\n Got: ${actualChecksum}`,
258
+ );
259
+ }
260
+ console.log('Checksum verified.');
261
+
262
+ // Extract archive
263
+ console.log(`Extracting ${filename}...`);
264
+ if (isZip) {
265
+ await extractZip(archivePath, cacheDir);
266
+ } else {
267
+ await extractTarGz(archivePath, cacheDir);
268
+ }
269
+
270
+ // Make the binary executable on Unix
271
+ if (valeOS !== 'Windows') {
272
+ await fs.chmod(binaryPath, 0o755);
273
+ }
274
+
275
+ console.log(`Vale v${version} ready at ${binaryPath}`);
276
+ } finally {
277
+ // Clean up the downloaded archive regardless of success or failure
278
+ await fs.rm(archivePath, { force: true });
279
+ }
280
+
281
+ return binaryPath;
282
+ }
283
+
284
+ /**
285
+ * Runs the vale binary, forwarding all extra args and inheriting stdio.
286
+ * @param {string} binaryPath
287
+ * @param {string[]} valeArgs
288
+ * @returns {Promise<void>}
289
+ */
290
+ async function runVale(binaryPath, valeArgs) {
291
+ const result = await $({ stdio: 'inherit', reject: false })`${binaryPath} ${valeArgs}`;
292
+ process.exitCode = result.exitCode;
293
+ }
294
+
295
+ /**
296
+ * Runs vale with JSON output and captures the results.
297
+ * @param {string} binaryPath
298
+ * @param {string[]} valeArgs
299
+ * @returns {Promise<Record<string, Array<{ Action: { Name: string; Params: string[] | null }; Span: [number, number]; Check: string; Message: string; Severity: string; Match: string; Line: number }>>>}
300
+ */
301
+ async function runValeJSON(binaryPath, valeArgs) {
302
+ const result = await $({ reject: false })`${binaryPath} --output JSON ${valeArgs}`;
303
+ try {
304
+ return JSON.parse(result.stdout);
305
+ } catch {
306
+ throw new Error(`Failed to parse vale JSON output: ${result.stdout}`);
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Extracts the replacement text from a vale alert.
312
+ * Returns null if no replacement can be determined.
313
+ * @param {{ Action: { Name: string; Params: string[] | null }; Message: string }} alert
314
+ * @returns {string | null}
315
+ */
316
+ export function getReplacementText(alert) {
317
+ // If vale provides an explicit action with replacement params, use that
318
+ if (alert.Action.Name === 'replace' && alert.Action.Params && alert.Action.Params.length > 0) {
319
+ return alert.Action.Params[0];
320
+ }
321
+
322
+ // Otherwise, try to extract from the message pattern.
323
+ // Vale messages follow patterns like:
324
+ // "Use 'X' instead of 'Y'"
325
+ // "Use the US spelling 'X' instead of the British 'Y'"
326
+ // "Use a non-breaking space ... ('X' instead of 'Y')"
327
+ const match = alert.Message.match(/'([^']+)'\s+instead\s+of\s+.*?'([^']+)'/);
328
+ if (match) {
329
+ return match[1];
330
+ }
331
+
332
+ return null;
333
+ }
334
+
335
+ /**
336
+ * Applies auto-fixes from vale alerts to the source files.
337
+ * Processes alerts in reverse order within each line to preserve column positions.
338
+ * @param {Record<string, Array<{ Action: { Name: string; Params: string[] | null }; Span: [number, number]; Check: string; Message: string; Severity: string; Match: string; Line: number }>>} results
339
+ * @param {'all' | 'error'} fixLevel
340
+ * @returns {Promise<{ fixed: number; skipped: number }>}
341
+ */
342
+ export async function applyFixes(results, fixLevel) {
343
+ const entries = Object.entries(results);
344
+
345
+ const perFileResults = await mapConcurrently(
346
+ entries,
347
+ async ([filePath, alerts]) => {
348
+ const content = await fs.readFile(filePath, 'utf-8');
349
+ const lines = content.split('\n');
350
+
351
+ // Filter alerts by severity and whether we can determine a replacement
352
+ const fixableAlerts = alerts.filter((alert) => {
353
+ if (fixLevel === 'error' && alert.Severity !== 'error') {
354
+ return false;
355
+ }
356
+ return getReplacementText(alert) !== null;
357
+ });
358
+
359
+ if (fixableAlerts.length === 0) {
360
+ return { fixed: 0, skipped: alerts.length };
361
+ }
362
+
363
+ // Deduplicate alerts at the same location (same line + span), keeping the first one
364
+ /** @type {Map<string, typeof fixableAlerts[0]>} */
365
+ const uniqueAlerts = new Map();
366
+ for (const alert of fixableAlerts) {
367
+ const key = `${alert.Line}:${alert.Span[0]}:${alert.Span[1]}`;
368
+ if (!uniqueAlerts.has(key)) {
369
+ uniqueAlerts.set(key, alert);
370
+ }
371
+ }
372
+
373
+ // Sort by line ascending, then by span start descending (so we apply right-to-left)
374
+ const sortedAlerts = [...uniqueAlerts.values()].sort((a, b) => {
375
+ if (a.Line !== b.Line) {
376
+ return a.Line - b.Line;
377
+ }
378
+ return b.Span[0] - a.Span[0];
379
+ });
380
+
381
+ // Group alerts by line number
382
+ /** @type {Map<number, typeof sortedAlerts>} */
383
+ const alertsByLine = new Map();
384
+ for (const alert of sortedAlerts) {
385
+ const lineAlerts = alertsByLine.get(alert.Line) ?? [];
386
+ lineAlerts.push(alert);
387
+ alertsByLine.set(alert.Line, lineAlerts);
388
+ }
389
+
390
+ // Apply fixes line by line, right-to-left within each line
391
+ let fileFixed = 0;
392
+ for (const [lineNum, lineAlerts] of alertsByLine) {
393
+ let line = lines[lineNum - 1]; // Line is 1-based
394
+ for (const alert of lineAlerts) {
395
+ const replacement = /** @type {string} */ (getReplacementText(alert));
396
+ // Span is 1-based [start, end] inclusive
397
+ const start = alert.Span[0] - 1;
398
+ const end = alert.Span[1];
399
+ line = line.slice(0, start) + replacement + line.slice(end);
400
+ fileFixed += 1;
401
+ }
402
+ lines[lineNum - 1] = line;
403
+ }
404
+
405
+ await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
406
+
407
+ return { fixed: fileFixed, skipped: alerts.length - fixableAlerts.length };
408
+ },
409
+ 10,
410
+ );
411
+
412
+ let fixed = 0;
413
+ let skipped = 0;
414
+ for (const result of perFileResults) {
415
+ if (result instanceof Error) {
416
+ throw result;
417
+ }
418
+ fixed += result.fixed;
419
+ skipped += result.skipped;
420
+ }
421
+
422
+ return { fixed, skipped };
423
+ }
424
+
425
+ /**
426
+ * @typedef {{ 'vale-version': string; 'auto-fix': 'all' | 'error' | undefined; }} Args
427
+ */
428
+
429
+ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
430
+ command: 'vale',
431
+ describe:
432
+ 'Download and run vale (a prose linter). All arguments are forwarded to the vale binary.',
433
+ builder: (yargs) => {
434
+ return (
435
+ yargs
436
+ // because `version` is taken over yargs itself
437
+ .option('vale-version', {
438
+ type: 'string',
439
+ demandOption: true,
440
+ description: 'The vale version to use (e.g. "3.14.1" or "latest").',
441
+ default: 'latest',
442
+ })
443
+ .option('auto-fix', {
444
+ type: 'string',
445
+ choices: ['all', 'error'],
446
+ description:
447
+ 'Automatically apply fixes suggested by vale. "all" fixes both errors and warnings, "error" fixes only errors.',
448
+ })
449
+ .example('$0 vale --vale-version 3.14.1 docs/', 'Lint all files in the docs/ directory')
450
+ .example(
451
+ '$0 vale --vale-version 3.14.1 --auto-fix=all docs/',
452
+ 'Lint and auto-fix all issues in docs/',
453
+ )
454
+ .strict(false)
455
+ );
456
+ },
457
+ handler: async (args) => {
458
+ const version = args.valeVersion === 'latest' ? await fetchLatestVersion() : args.valeVersion;
459
+ const binaryPath = await ensureValeBinary(version);
460
+
461
+ // Collect everything from process.argv that follows the "vale" token,
462
+ // excluding our own flags and their values.
463
+ const argvAfterVale = process.argv.slice(process.argv.indexOf('vale') + 1);
464
+ const valeArgs = [];
465
+ for (let i = 0; i < argvAfterVale.length; i += 1) {
466
+ const arg = argvAfterVale[i];
467
+ if (arg === '--vale-version' || arg.startsWith('--vale-version=')) {
468
+ // consumed by this wrapper; skip the next arg if it's the value
469
+ if (
470
+ arg === '--vale-version' &&
471
+ argvAfterVale[i + 1] &&
472
+ !argvAfterVale[i + 1].startsWith('--')
473
+ ) {
474
+ i += 1;
475
+ }
476
+ } else if (arg === '--auto-fix' || arg.startsWith('--auto-fix=')) {
477
+ // consumed by this wrapper; skip the next arg if it's the value
478
+ if (
479
+ arg === '--auto-fix' &&
480
+ argvAfterVale[i + 1] &&
481
+ !argvAfterVale[i + 1].startsWith('--')
482
+ ) {
483
+ i += 1;
484
+ }
485
+ } else {
486
+ valeArgs.push(arg);
487
+ }
488
+ }
489
+
490
+ if (args.autoFix) {
491
+ const fixLevel = /** @type {'all' | 'error'} */ (args.autoFix);
492
+ const results = await runValeJSON(binaryPath, valeArgs);
493
+ const totalAlerts = Object.values(results).reduce((sum, alerts) => sum + alerts.length, 0);
494
+
495
+ if (totalAlerts === 0) {
496
+ console.log('No issues found by vale.');
497
+ return;
498
+ }
499
+
500
+ const { fixed, skipped } = await applyFixes(results, fixLevel);
501
+ console.log(`Auto-fix complete: ${fixed} fixed, ${skipped} skipped.`);
502
+
503
+ if (fixed > 0) {
504
+ // Re-run vale to show remaining issues
505
+ console.log('\nRemaining issues after auto-fix:');
506
+ await runVale(binaryPath, valeArgs);
507
+ }
508
+ return;
509
+ }
510
+
511
+ await runVale(binaryPath, valeArgs);
512
+ },
513
+ });