@mintlify/cli 4.0.1410 → 4.0.1412
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/__test__/brokenLinks.test.ts +2 -3
- package/__test__/resolveFiles.test.ts +70 -0
- package/bin/cli.js +1 -39
- package/bin/{deslop/resolveFiles.js → resolveFiles.js} +0 -74
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/bin/vendor/converter.js +9 -3
- package/package.json +7 -7
- package/src/cli.tsx +1 -51
- package/src/{deslop/resolveFiles.ts → resolveFiles.ts} +0 -77
- package/__test__/deslop/client.test.ts +0 -167
- package/__test__/deslop/resolveFiles.test.ts +0 -183
- package/__test__/deslop/whitespace.test.ts +0 -94
- package/bin/deslop/client.js +0 -74
- package/bin/deslop/index.js +0 -242
- package/bin/deslop/types.js +0 -1
- package/bin/deslop/whitespace.js +0 -144
- package/src/deslop/client.ts +0 -77
- package/src/deslop/index.tsx +0 -286
- package/src/deslop/types.ts +0 -39
- package/src/deslop/whitespace.ts +0 -155
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { normalizeWhitespace, summarizeWhitespace } from '../../src/deslop/whitespace.js';
|
|
4
|
-
|
|
5
|
-
describe('normalizeWhitespace', () => {
|
|
6
|
-
it('strips trailing whitespace and collapses blank-line runs', () => {
|
|
7
|
-
const { fixed, counts } = normalizeWhitespace('Line one. \n\n\n\n\nLine two.\n');
|
|
8
|
-
expect(fixed).toBe('Line one.\n\nLine two.\n');
|
|
9
|
-
expect(counts.trailing).toBe(1);
|
|
10
|
-
expect(counts.blankRuns).toBe(1);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('replaces unicode spaces and removes zero-width characters in prose', () => {
|
|
14
|
-
const { fixed, counts } = normalizeWhitespace('Non breaking and zerowidth here.\n');
|
|
15
|
-
expect(fixed).toBe('Non breaking and zerowidth here.\n');
|
|
16
|
-
expect(counts.unicodeSpaces).toBe(1);
|
|
17
|
-
expect(counts.zeroWidth).toBe(1);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('collapses internal double spaces but preserves leading indentation', () => {
|
|
21
|
-
const { fixed } = normalizeWhitespace(' - item with gaps\n');
|
|
22
|
-
expect(fixed).toBe(' - item with gaps\n');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('leaves fenced code blocks byte-for-byte', () => {
|
|
26
|
-
const input = 'Prose here.\n\n```js\nconst x = 1; \n\n\n\ny;\n```\n\nMore prose.\n';
|
|
27
|
-
const { fixed } = normalizeWhitespace(input);
|
|
28
|
-
expect(fixed).toContain('const x = 1; \n\n\n\ny;');
|
|
29
|
-
expect(fixed).toContain('Prose here.');
|
|
30
|
-
expect(fixed).toContain('More prose.');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('preserves whitespace inside 4+-backtick fences (extended fences)', () => {
|
|
34
|
-
const input =
|
|
35
|
-
'Prose here.\n\n````md\n```js\nconst x = 1; \n```\n\n\n\ny;\n````\n\nMore prose.\n';
|
|
36
|
-
const { fixed } = normalizeWhitespace(input);
|
|
37
|
-
expect(fixed).toContain('const x = 1; \n```\n\n\n\ny;');
|
|
38
|
-
expect(fixed).toContain('More prose.');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('leaves YAML frontmatter untouched', () => {
|
|
42
|
-
const input = '---\ntitle: Spaced Title\n---\n\nBody text.\n';
|
|
43
|
-
const { fixed } = normalizeWhitespace(input);
|
|
44
|
-
expect(fixed).toContain('title: Spaced Title');
|
|
45
|
-
expect(fixed).toContain('Body text.');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('reports no change for already-clean content', () => {
|
|
49
|
-
const clean = 'A tidy line.\n\nAnother tidy line.\n';
|
|
50
|
-
const { changed } = normalizeWhitespace(clean);
|
|
51
|
-
expect(changed).toBe(false);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('preserves inline code spans (no collapse/trim inside backticks)', () => {
|
|
55
|
-
const { fixed } = normalizeWhitespace('Run `npm install x` then relax now.\n');
|
|
56
|
-
expect(fixed).toBe('Run `npm install x` then relax now.\n');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('strips trailing whitespace on CRLF lines and keeps the CR', () => {
|
|
60
|
-
const { fixed, counts } = normalizeWhitespace('First line. \r\nSecond.\r\n');
|
|
61
|
-
expect(fixed).toBe('First line.\r\nSecond.\r\n');
|
|
62
|
-
expect(counts.trailing).toBe(1);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('counts collapsed internal spaces so a space-only fix still registers', () => {
|
|
66
|
-
const { changed, counts } = normalizeWhitespace('one two three\n');
|
|
67
|
-
expect(changed).toBe(true);
|
|
68
|
-
expect(counts.spaces).toBe(1);
|
|
69
|
-
expect(summarizeWhitespace(counts)).toContain('collapsed spaces');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('summarizes counts or returns null when clean', () => {
|
|
73
|
-
expect(
|
|
74
|
-
summarizeWhitespace({
|
|
75
|
-
trailing: 2,
|
|
76
|
-
blankRuns: 0,
|
|
77
|
-
unicodeSpaces: 0,
|
|
78
|
-
zeroWidth: 1,
|
|
79
|
-
spaces: 0,
|
|
80
|
-
finalNewline: false,
|
|
81
|
-
})
|
|
82
|
-
).toBe('2 trailing, 1 zero-width chars');
|
|
83
|
-
expect(
|
|
84
|
-
summarizeWhitespace({
|
|
85
|
-
trailing: 0,
|
|
86
|
-
blankRuns: 0,
|
|
87
|
-
unicodeSpaces: 0,
|
|
88
|
-
zeroWidth: 0,
|
|
89
|
-
spaces: 0,
|
|
90
|
-
finalNewline: false,
|
|
91
|
-
})
|
|
92
|
-
).toBeNull();
|
|
93
|
-
});
|
|
94
|
-
});
|
package/bin/deslop/client.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
|
-
import { authenticatedFetch } from '../authenticatedFetch.js';
|
|
11
|
-
import { API_URL } from '../constants.js';
|
|
12
|
-
const DEFAULT_CONCURRENCY = 4;
|
|
13
|
-
// Errors that apply to the whole batch (out of credits, service down) rather
|
|
14
|
-
// than a single page — hitting one means the rest will fail identically.
|
|
15
|
-
class DeslopAbortError extends Error {
|
|
16
|
-
}
|
|
17
|
-
export function checkPage(subdomain, pagePath, content) {
|
|
18
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
19
|
-
const endpoint = new URL(`${API_URL}/api/cli/deslop`);
|
|
20
|
-
endpoint.searchParams.set('subdomain', subdomain);
|
|
21
|
-
const res = yield authenticatedFetch(endpoint.toString(), {
|
|
22
|
-
method: 'POST',
|
|
23
|
-
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
24
|
-
body: JSON.stringify({ path: pagePath, content }),
|
|
25
|
-
});
|
|
26
|
-
if (res.status === 402) {
|
|
27
|
-
throw new DeslopAbortError('Out of AI credits — upgrade or top up in the Mintlify dashboard.');
|
|
28
|
-
}
|
|
29
|
-
if (res.status === 503) {
|
|
30
|
-
throw new DeslopAbortError('Deslop is temporarily unavailable, try again shortly.');
|
|
31
|
-
}
|
|
32
|
-
if (!res.ok) {
|
|
33
|
-
const body = yield res.text().catch(() => '');
|
|
34
|
-
throw new Error(`API error (${res.status}): ${body || res.statusText}`);
|
|
35
|
-
}
|
|
36
|
-
return res.json();
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
export function checkPages(subdomain_1, pages_1) {
|
|
40
|
-
return __awaiter(this, arguments, void 0, function* (subdomain, pages, concurrency = DEFAULT_CONCURRENCY) {
|
|
41
|
-
const results = new Array(pages.length);
|
|
42
|
-
let next = 0;
|
|
43
|
-
let aborted = false;
|
|
44
|
-
const worker = () => __awaiter(this, void 0, void 0, function* () {
|
|
45
|
-
while (next < pages.length && !aborted) {
|
|
46
|
-
const index = next++;
|
|
47
|
-
const page = pages[index];
|
|
48
|
-
try {
|
|
49
|
-
const response = yield checkPage(subdomain, page.path, page.content);
|
|
50
|
-
results[index] = { file: page.path, response };
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
results[index] = {
|
|
54
|
-
file: page.path,
|
|
55
|
-
error: err instanceof Error ? err.message : 'unknown error',
|
|
56
|
-
};
|
|
57
|
-
if (err instanceof DeslopAbortError)
|
|
58
|
-
aborted = true;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
yield Promise.all(Array.from({ length: Math.min(concurrency, pages.length) }, worker));
|
|
63
|
-
// Pages left unprocessed because the batch was aborted after a global failure.
|
|
64
|
-
for (let index = 0; index < pages.length; index++) {
|
|
65
|
-
if (!results[index]) {
|
|
66
|
-
results[index] = {
|
|
67
|
-
file: pages[index].path,
|
|
68
|
-
error: 'Not checked — stopped after a credits or availability error.',
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return results;
|
|
73
|
-
});
|
|
74
|
-
}
|
package/bin/deslop/index.js
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
11
|
-
import { addLog, ErrorLog, SpinnerLog, removeLastLog } from '@mintlify/previewing';
|
|
12
|
-
import chalk from 'chalk';
|
|
13
|
-
import { Text } from 'ink';
|
|
14
|
-
import fs from 'node:fs/promises';
|
|
15
|
-
import path from 'node:path';
|
|
16
|
-
import { CMD_EXEC_PATH, terminate } from '../helpers.js';
|
|
17
|
-
import { trackEvent } from '../telemetry/track.js';
|
|
18
|
-
import { checkPages } from './client.js';
|
|
19
|
-
import { resolveChangedFiles, resolveExplicitFiles } from './resolveFiles.js';
|
|
20
|
-
import { normalizeWhitespace, summarizeWhitespace } from './whitespace.js';
|
|
21
|
-
function fixWhitespacePass(files, apply) {
|
|
22
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
23
|
-
const changes = [];
|
|
24
|
-
for (const file of files) {
|
|
25
|
-
const fullPath = path.join(CMD_EXEC_PATH, file);
|
|
26
|
-
const content = yield fs.readFile(fullPath, 'utf-8');
|
|
27
|
-
const report = normalizeWhitespace(content);
|
|
28
|
-
const summary = summarizeWhitespace(report.counts);
|
|
29
|
-
if (!report.changed || !summary)
|
|
30
|
-
continue;
|
|
31
|
-
if (apply)
|
|
32
|
-
yield fs.writeFile(fullPath, report.fixed);
|
|
33
|
-
changes.push({ file, summary });
|
|
34
|
-
}
|
|
35
|
-
return changes;
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
const identity = (text) => text;
|
|
39
|
-
const colorPalette = { green: chalk.green, red: chalk.red, dim: chalk.dim };
|
|
40
|
-
const plainPalette = { green: identity, red: identity, dim: identity };
|
|
41
|
-
function resolveFormat(argv) {
|
|
42
|
-
if (argv.format === 'table' || argv.format === 'plain' || argv.format === 'json')
|
|
43
|
-
return argv.format;
|
|
44
|
-
return 'table';
|
|
45
|
-
}
|
|
46
|
-
function output(format, text) {
|
|
47
|
-
if (format === 'table') {
|
|
48
|
-
addLog(_jsx(Text, { children: text }));
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
process.stdout.write(text + '\n');
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
function requireSubdomain(subdomain) {
|
|
55
|
-
if (!subdomain) {
|
|
56
|
-
throw new Error('No subdomain set. Pass --subdomain, or run `mint config set subdomain <name>`.');
|
|
57
|
-
}
|
|
58
|
-
return subdomain;
|
|
59
|
-
}
|
|
60
|
-
function flaggedFraction(response) {
|
|
61
|
-
return response.fractionAi + response.fractionAiAssisted;
|
|
62
|
-
}
|
|
63
|
-
// A fully-human page (fraction 0) never fails, even at --threshold 0.
|
|
64
|
-
function isFailing(fraction, threshold) {
|
|
65
|
-
return fraction > 0 && fraction >= threshold;
|
|
66
|
-
}
|
|
67
|
-
function truncateText(text, max = 100) {
|
|
68
|
-
const flat = text.replace(/\s+/g, ' ').trim();
|
|
69
|
-
return flat.length > max ? `${flat.slice(0, max)}…` : flat;
|
|
70
|
-
}
|
|
71
|
-
function renderResult(result, threshold, palette) {
|
|
72
|
-
var _a;
|
|
73
|
-
if ('error' in result) {
|
|
74
|
-
return [`${palette.red('✗')} ${result.file} — ${palette.red(result.error)}`];
|
|
75
|
-
}
|
|
76
|
-
const response = result.response;
|
|
77
|
-
if (response.skipped !== null) {
|
|
78
|
-
return [`${palette.dim('○')} ${result.file} — ${palette.dim(`skipped (${response.skipped})`)}`];
|
|
79
|
-
}
|
|
80
|
-
const fraction = flaggedFraction(response);
|
|
81
|
-
const failing = isFailing(fraction, threshold);
|
|
82
|
-
const icon = failing ? palette.red('✗') : palette.green('✓');
|
|
83
|
-
const percent = Math.round(fraction * 100);
|
|
84
|
-
const lines = [`${icon} ${result.file} — ${response.predictionShort} (${percent}% AI-written)`];
|
|
85
|
-
if (!failing)
|
|
86
|
-
return lines;
|
|
87
|
-
for (const window of response.windows) {
|
|
88
|
-
lines.push(` L${window.startLine}–${window.endLine} [${window.label} ${window.aiAssistanceScore.toFixed(2)}]`);
|
|
89
|
-
lines.push(` ${palette.dim(truncateText(window.text))}`);
|
|
90
|
-
for (const rewrite of (_a = window.rewrites) !== null && _a !== void 0 ? _a : []) {
|
|
91
|
-
lines.push(` ↻ ${rewrite.text}`);
|
|
92
|
-
lines.push(` ${palette.dim(rewrite.rationale)}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return lines;
|
|
96
|
-
}
|
|
97
|
-
function summarize(results, threshold) {
|
|
98
|
-
const summary = {
|
|
99
|
-
checked: 0,
|
|
100
|
-
passed: 0,
|
|
101
|
-
failed: 0,
|
|
102
|
-
skipped: 0,
|
|
103
|
-
creditsCharged: 0,
|
|
104
|
-
errors: 0,
|
|
105
|
-
};
|
|
106
|
-
for (const result of results) {
|
|
107
|
-
if ('error' in result) {
|
|
108
|
-
summary.errors += 1;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
summary.creditsCharged += result.response.creditsCharged;
|
|
112
|
-
if (result.response.skipped !== null) {
|
|
113
|
-
summary.skipped += 1;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
summary.checked += 1;
|
|
117
|
-
if (isFailing(flaggedFraction(result.response), threshold)) {
|
|
118
|
-
summary.failed += 1;
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
summary.passed += 1;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return summary;
|
|
125
|
-
}
|
|
126
|
-
function renderSummaryLine(summary, palette) {
|
|
127
|
-
const parts = [
|
|
128
|
-
`${summary.checked} checked`,
|
|
129
|
-
palette.green(`${summary.passed} passed`),
|
|
130
|
-
summary.failed > 0 ? palette.red(`${summary.failed} failed`) : `${summary.failed} failed`,
|
|
131
|
-
palette.dim(`${summary.skipped} skipped`),
|
|
132
|
-
`${summary.creditsCharged} ${summary.creditsCharged === 1 ? 'credit' : 'credits'} used`,
|
|
133
|
-
];
|
|
134
|
-
if (summary.errors > 0) {
|
|
135
|
-
parts.push(palette.red(`${summary.errors} ${summary.errors === 1 ? 'error' : 'errors'}`));
|
|
136
|
-
}
|
|
137
|
-
return parts.join(' · ');
|
|
138
|
-
}
|
|
139
|
-
export const deslopHandler = (argv) => __awaiter(void 0, void 0, void 0, function* () {
|
|
140
|
-
var _a;
|
|
141
|
-
const format = resolveFormat(argv);
|
|
142
|
-
let spinnerAdded = false;
|
|
143
|
-
const threshold = typeof argv.threshold === 'number' ? argv.threshold : 0.5;
|
|
144
|
-
try {
|
|
145
|
-
const subdomain = requireSubdomain(argv.subdomain);
|
|
146
|
-
const fileArgs = ((_a = argv.files) !== null && _a !== void 0 ? _a : []).map(String).filter(Boolean);
|
|
147
|
-
const files = fileArgs.length > 0
|
|
148
|
-
? yield resolveExplicitFiles(CMD_EXEC_PATH, fileArgs)
|
|
149
|
-
: yield resolveChangedFiles(CMD_EXEC_PATH);
|
|
150
|
-
if (files.length === 0) {
|
|
151
|
-
if (format === 'json') {
|
|
152
|
-
process.stderr.write('No changed docs pages to check.\n');
|
|
153
|
-
output(format, JSON.stringify({
|
|
154
|
-
threshold,
|
|
155
|
-
results: [],
|
|
156
|
-
summary: {
|
|
157
|
-
checked: 0,
|
|
158
|
-
passed: 0,
|
|
159
|
-
failed: 0,
|
|
160
|
-
skipped: 0,
|
|
161
|
-
errors: 0,
|
|
162
|
-
creditsCharged: 0,
|
|
163
|
-
},
|
|
164
|
-
}, null, 2));
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
output(format, 'No changed docs pages to check.');
|
|
168
|
-
}
|
|
169
|
-
yield terminate(0);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
const whitespaceChanges = argv.fixWhitespace ? yield fixWhitespacePass(files, true) : [];
|
|
173
|
-
const progressMessage = `Checking ${files.length} ${files.length === 1 ? 'page' : 'pages'} for AI-sounding prose...`;
|
|
174
|
-
if (format === 'table') {
|
|
175
|
-
addLog(_jsx(SpinnerLog, { message: progressMessage }));
|
|
176
|
-
spinnerAdded = true;
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
process.stderr.write(progressMessage + '\n');
|
|
180
|
-
}
|
|
181
|
-
const pages = yield Promise.all(files.map((file) => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
|
-
return ({
|
|
183
|
-
path: file,
|
|
184
|
-
content: yield fs.readFile(path.join(CMD_EXEC_PATH, file), 'utf-8'),
|
|
185
|
-
});
|
|
186
|
-
})));
|
|
187
|
-
const results = yield checkPages(subdomain, pages);
|
|
188
|
-
if (spinnerAdded) {
|
|
189
|
-
removeLastLog();
|
|
190
|
-
spinnerAdded = false;
|
|
191
|
-
}
|
|
192
|
-
const summary = summarize(results, threshold);
|
|
193
|
-
void trackEvent('cli.deslop.executed', {
|
|
194
|
-
fileCount: files.length,
|
|
195
|
-
flaggedCount: summary.failed,
|
|
196
|
-
format,
|
|
197
|
-
});
|
|
198
|
-
if (format === 'json') {
|
|
199
|
-
output(format, JSON.stringify({
|
|
200
|
-
threshold,
|
|
201
|
-
results: results.map((result) => 'error' in result ? { path: result.file, error: result.error } : result.response),
|
|
202
|
-
summary: {
|
|
203
|
-
checked: summary.checked,
|
|
204
|
-
passed: summary.passed,
|
|
205
|
-
failed: summary.failed,
|
|
206
|
-
skipped: summary.skipped,
|
|
207
|
-
errors: summary.errors,
|
|
208
|
-
creditsCharged: summary.creditsCharged,
|
|
209
|
-
},
|
|
210
|
-
whitespaceFixed: whitespaceChanges,
|
|
211
|
-
}, null, 2));
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
const palette = format === 'table' ? colorPalette : plainPalette;
|
|
215
|
-
const lines = [];
|
|
216
|
-
for (const change of whitespaceChanges) {
|
|
217
|
-
lines.push(`${palette.green('⇥')} ${change.file} — whitespace fixed (${change.summary})`);
|
|
218
|
-
}
|
|
219
|
-
if (whitespaceChanges.length)
|
|
220
|
-
lines.push('');
|
|
221
|
-
for (const result of results) {
|
|
222
|
-
lines.push(...renderResult(result, threshold, palette));
|
|
223
|
-
}
|
|
224
|
-
lines.push('');
|
|
225
|
-
lines.push(renderSummaryLine(summary, palette));
|
|
226
|
-
output(format, lines.join('\n'));
|
|
227
|
-
}
|
|
228
|
-
yield terminate(summary.failed > 0 || summary.errors > 0 ? 1 : 0);
|
|
229
|
-
}
|
|
230
|
-
catch (err) {
|
|
231
|
-
const message = err instanceof Error ? err.message : 'unknown error';
|
|
232
|
-
if (format === 'table') {
|
|
233
|
-
if (spinnerAdded)
|
|
234
|
-
removeLastLog();
|
|
235
|
-
addLog(_jsx(ErrorLog, { message: message }));
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
process.stderr.write(`Error: ${message}\n`);
|
|
239
|
-
}
|
|
240
|
-
yield terminate(1);
|
|
241
|
-
}
|
|
242
|
-
});
|
package/bin/deslop/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/bin/deslop/whitespace.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
// Unusual space characters that should read as a normal space.
|
|
2
|
-
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
3
|
-
// Zero-width and BOM characters that should be removed outright.
|
|
4
|
-
const ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
|
|
5
|
-
const FENCE = /^(\s*)(`{3,}|~{3,})/;
|
|
6
|
-
// Splits a line so odd indices are inline-code spans (left untouched).
|
|
7
|
-
const INLINE_CODE = /(`+[^`]*`+)/;
|
|
8
|
-
function isFrontmatterFence(line, index) {
|
|
9
|
-
return index === 0 && line.trim() === '---';
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Normalize prose whitespace in an MDX document without touching the bytes of
|
|
13
|
-
* fenced code blocks or YAML frontmatter, where spacing is significant.
|
|
14
|
-
*/
|
|
15
|
-
export function normalizeWhitespace(content) {
|
|
16
|
-
var _a, _b, _c;
|
|
17
|
-
const counts = {
|
|
18
|
-
trailing: 0,
|
|
19
|
-
blankRuns: 0,
|
|
20
|
-
unicodeSpaces: 0,
|
|
21
|
-
zeroWidth: 0,
|
|
22
|
-
spaces: 0,
|
|
23
|
-
finalNewline: false,
|
|
24
|
-
};
|
|
25
|
-
const lines = content.split('\n');
|
|
26
|
-
const out = [];
|
|
27
|
-
const isProtected = [];
|
|
28
|
-
let inFence = false;
|
|
29
|
-
let fenceMarker = '';
|
|
30
|
-
let inFrontmatter = false;
|
|
31
|
-
const pushProtected = (line) => {
|
|
32
|
-
out.push(line);
|
|
33
|
-
isProtected.push(true);
|
|
34
|
-
};
|
|
35
|
-
for (const [index, rawLine] of lines.entries()) {
|
|
36
|
-
if (isFrontmatterFence(rawLine, index)) {
|
|
37
|
-
inFrontmatter = true;
|
|
38
|
-
pushProtected(rawLine);
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (inFrontmatter) {
|
|
42
|
-
pushProtected(rawLine);
|
|
43
|
-
if (rawLine.trim() === '---')
|
|
44
|
-
inFrontmatter = false;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
const fence = rawLine.match(FENCE);
|
|
48
|
-
if (fence) {
|
|
49
|
-
const marker = (_a = fence[2]) !== null && _a !== void 0 ? _a : '';
|
|
50
|
-
if (!inFence) {
|
|
51
|
-
inFence = true;
|
|
52
|
-
fenceMarker = marker;
|
|
53
|
-
}
|
|
54
|
-
else if (marker[0] === fenceMarker[0] && marker.length >= fenceMarker.length) {
|
|
55
|
-
inFence = false;
|
|
56
|
-
}
|
|
57
|
-
pushProtected(rawLine);
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (inFence) {
|
|
61
|
-
pushProtected(rawLine);
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
let line = rawLine;
|
|
65
|
-
// Separate a trailing CR so CRLF files still get their trailing whitespace
|
|
66
|
-
// stripped (and keep the CR so line endings are preserved).
|
|
67
|
-
const cr = line.endsWith('\r') ? '\r' : '';
|
|
68
|
-
if (cr)
|
|
69
|
-
line = line.slice(0, -1);
|
|
70
|
-
// Apply substitutions only outside inline-code spans (odd split indices),
|
|
71
|
-
// so backticked code keeps its exact spacing.
|
|
72
|
-
line = line
|
|
73
|
-
.split(INLINE_CODE)
|
|
74
|
-
.map((seg, i) => {
|
|
75
|
-
if (i % 2 === 1)
|
|
76
|
-
return seg;
|
|
77
|
-
const uni = seg.match(UNICODE_SPACES);
|
|
78
|
-
if (uni)
|
|
79
|
-
counts.unicodeSpaces += uni.length;
|
|
80
|
-
seg = seg.replace(UNICODE_SPACES, ' ');
|
|
81
|
-
const zw = seg.match(ZERO_WIDTH);
|
|
82
|
-
if (zw)
|
|
83
|
-
counts.zeroWidth += zw.length;
|
|
84
|
-
return seg.replace(ZERO_WIDTH, '');
|
|
85
|
-
})
|
|
86
|
-
.join('');
|
|
87
|
-
const trimmed = line.replace(/[ \t]+$/, '');
|
|
88
|
-
if (trimmed !== line)
|
|
89
|
-
counts.trailing += 1;
|
|
90
|
-
line = trimmed;
|
|
91
|
-
// Collapse runs of internal spaces (outside inline code), preserving leading indentation.
|
|
92
|
-
const leading = (_c = (_b = line.match(/^[ \t]*/)) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : '';
|
|
93
|
-
const body = line.slice(leading.length);
|
|
94
|
-
const collapsed = body
|
|
95
|
-
.split(INLINE_CODE)
|
|
96
|
-
.map((seg, i) => (i % 2 === 1 ? seg : seg.replace(/ {2,}/g, ' ')))
|
|
97
|
-
.join('');
|
|
98
|
-
if (collapsed !== body)
|
|
99
|
-
counts.spaces += 1;
|
|
100
|
-
line = leading + collapsed + cr;
|
|
101
|
-
out.push(line);
|
|
102
|
-
isProtected.push(false);
|
|
103
|
-
}
|
|
104
|
-
// Collapse runs of 2 or more consecutive blank lines down to a single blank
|
|
105
|
-
// line, but never inside protected regions (code fences, frontmatter).
|
|
106
|
-
const collapsed = [];
|
|
107
|
-
let blankRun = 0;
|
|
108
|
-
for (const [index, line] of out.entries()) {
|
|
109
|
-
if (!isProtected[index] && line.trim() === '') {
|
|
110
|
-
blankRun += 1;
|
|
111
|
-
if (blankRun <= 1)
|
|
112
|
-
collapsed.push(line);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (blankRun > 1)
|
|
116
|
-
counts.blankRuns += 1;
|
|
117
|
-
blankRun = 0;
|
|
118
|
-
collapsed.push(line);
|
|
119
|
-
}
|
|
120
|
-
if (blankRun > 1)
|
|
121
|
-
counts.blankRuns += 1;
|
|
122
|
-
let fixed = collapsed.join('\n').replace(/\n+$/, '');
|
|
123
|
-
if (content.length > 0) {
|
|
124
|
-
counts.finalNewline = !content.endsWith('\n') || content.endsWith('\n\n');
|
|
125
|
-
fixed += '\n';
|
|
126
|
-
}
|
|
127
|
-
return { fixed, changed: fixed !== content, counts };
|
|
128
|
-
}
|
|
129
|
-
export function summarizeWhitespace(counts) {
|
|
130
|
-
const parts = [];
|
|
131
|
-
if (counts.trailing)
|
|
132
|
-
parts.push(`${counts.trailing} trailing`);
|
|
133
|
-
if (counts.blankRuns)
|
|
134
|
-
parts.push(`${counts.blankRuns} blank-line runs`);
|
|
135
|
-
if (counts.unicodeSpaces)
|
|
136
|
-
parts.push(`${counts.unicodeSpaces} unicode spaces`);
|
|
137
|
-
if (counts.zeroWidth)
|
|
138
|
-
parts.push(`${counts.zeroWidth} zero-width chars`);
|
|
139
|
-
if (counts.spaces)
|
|
140
|
-
parts.push(`${counts.spaces} collapsed spaces`);
|
|
141
|
-
if (counts.finalNewline)
|
|
142
|
-
parts.push('final newline');
|
|
143
|
-
return parts.length ? parts.join(', ') : null;
|
|
144
|
-
}
|
package/src/deslop/client.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { authenticatedFetch } from '../authenticatedFetch.js';
|
|
2
|
-
import { API_URL } from '../constants.js';
|
|
3
|
-
import type { DeslopFileResult, DeslopResponse } from './types.js';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_CONCURRENCY = 4;
|
|
6
|
-
|
|
7
|
-
// Errors that apply to the whole batch (out of credits, service down) rather
|
|
8
|
-
// than a single page — hitting one means the rest will fail identically.
|
|
9
|
-
class DeslopAbortError extends Error {}
|
|
10
|
-
|
|
11
|
-
export async function checkPage(
|
|
12
|
-
subdomain: string,
|
|
13
|
-
pagePath: string,
|
|
14
|
-
content: string
|
|
15
|
-
): Promise<DeslopResponse> {
|
|
16
|
-
const endpoint = new URL(`${API_URL}/api/cli/deslop`);
|
|
17
|
-
endpoint.searchParams.set('subdomain', subdomain);
|
|
18
|
-
|
|
19
|
-
const res = await authenticatedFetch(endpoint.toString(), {
|
|
20
|
-
method: 'POST',
|
|
21
|
-
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
22
|
-
body: JSON.stringify({ path: pagePath, content }),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
if (res.status === 402) {
|
|
26
|
-
throw new DeslopAbortError('Out of AI credits — upgrade or top up in the Mintlify dashboard.');
|
|
27
|
-
}
|
|
28
|
-
if (res.status === 503) {
|
|
29
|
-
throw new DeslopAbortError('Deslop is temporarily unavailable, try again shortly.');
|
|
30
|
-
}
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
const body = await res.text().catch(() => '');
|
|
33
|
-
throw new Error(`API error (${res.status}): ${body || res.statusText}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return res.json() as Promise<DeslopResponse>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function checkPages(
|
|
40
|
-
subdomain: string,
|
|
41
|
-
pages: { path: string; content: string }[],
|
|
42
|
-
concurrency: number = DEFAULT_CONCURRENCY
|
|
43
|
-
): Promise<DeslopFileResult[]> {
|
|
44
|
-
const results = new Array<DeslopFileResult>(pages.length);
|
|
45
|
-
let next = 0;
|
|
46
|
-
let aborted = false;
|
|
47
|
-
|
|
48
|
-
const worker = async () => {
|
|
49
|
-
while (next < pages.length && !aborted) {
|
|
50
|
-
const index = next++;
|
|
51
|
-
const page = pages[index]!;
|
|
52
|
-
try {
|
|
53
|
-
const response = await checkPage(subdomain, page.path, page.content);
|
|
54
|
-
results[index] = { file: page.path, response };
|
|
55
|
-
} catch (err) {
|
|
56
|
-
results[index] = {
|
|
57
|
-
file: page.path,
|
|
58
|
-
error: err instanceof Error ? err.message : 'unknown error',
|
|
59
|
-
};
|
|
60
|
-
if (err instanceof DeslopAbortError) aborted = true;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, pages.length) }, worker));
|
|
66
|
-
|
|
67
|
-
// Pages left unprocessed because the batch was aborted after a global failure.
|
|
68
|
-
for (let index = 0; index < pages.length; index++) {
|
|
69
|
-
if (!results[index]) {
|
|
70
|
-
results[index] = {
|
|
71
|
-
file: pages[index]!.path,
|
|
72
|
-
error: 'Not checked — stopped after a credits or availability error.',
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return results;
|
|
77
|
-
}
|