@mintlify/cli 4.0.1409 → 4.0.1411

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.
@@ -1,286 +0,0 @@
1
- import { addLog, ErrorLog, SpinnerLog, removeLastLog } from '@mintlify/previewing';
2
- import chalk from 'chalk';
3
- import { Text } from 'ink';
4
- import fs from 'node:fs/promises';
5
- import path from 'node:path';
6
-
7
- import { CMD_EXEC_PATH, terminate } from '../helpers.js';
8
- import { trackEvent } from '../telemetry/track.js';
9
- import { checkPages } from './client.js';
10
- import { resolveChangedFiles, resolveExplicitFiles } from './resolveFiles.js';
11
- import type { DeslopCheckedResponse, DeslopFileResult } from './types.js';
12
- import { normalizeWhitespace, summarizeWhitespace } from './whitespace.js';
13
-
14
- type OutputFormat = 'table' | 'plain' | 'json';
15
-
16
- export type DeslopArgs = {
17
- files?: (string | number)[];
18
- format?: string;
19
- subdomain?: string;
20
- threshold?: number;
21
- fixWhitespace?: boolean;
22
- };
23
-
24
- async function fixWhitespacePass(
25
- files: string[],
26
- apply: boolean
27
- ): Promise<{ file: string; summary: string }[]> {
28
- const changes: { file: string; summary: string }[] = [];
29
- for (const file of files) {
30
- const fullPath = path.join(CMD_EXEC_PATH, file);
31
- const content = await fs.readFile(fullPath, 'utf-8');
32
- const report = normalizeWhitespace(content);
33
- const summary = summarizeWhitespace(report.counts);
34
- if (!report.changed || !summary) continue;
35
- if (apply) await fs.writeFile(fullPath, report.fixed);
36
- changes.push({ file, summary });
37
- }
38
- return changes;
39
- }
40
-
41
- type Paint = (text: string) => string;
42
-
43
- type Palette = { green: Paint; red: Paint; dim: Paint };
44
-
45
- const identity: Paint = (text) => text;
46
-
47
- const colorPalette: Palette = { green: chalk.green, red: chalk.red, dim: chalk.dim };
48
- const plainPalette: Palette = { green: identity, red: identity, dim: identity };
49
-
50
- function resolveFormat(argv: { format?: string }): OutputFormat {
51
- if (argv.format === 'table' || argv.format === 'plain' || argv.format === 'json')
52
- return argv.format;
53
- return 'table';
54
- }
55
-
56
- function output(format: OutputFormat, text: string) {
57
- if (format === 'table') {
58
- addLog(<Text>{text}</Text>);
59
- } else {
60
- process.stdout.write(text + '\n');
61
- }
62
- }
63
-
64
- function requireSubdomain(subdomain: string | undefined): string {
65
- if (!subdomain) {
66
- throw new Error(
67
- 'No subdomain set. Pass --subdomain, or run `mint config set subdomain <name>`.'
68
- );
69
- }
70
- return subdomain;
71
- }
72
-
73
- function flaggedFraction(response: DeslopCheckedResponse): number {
74
- return response.fractionAi + response.fractionAiAssisted;
75
- }
76
-
77
- // A fully-human page (fraction 0) never fails, even at --threshold 0.
78
- function isFailing(fraction: number, threshold: number): boolean {
79
- return fraction > 0 && fraction >= threshold;
80
- }
81
-
82
- function truncateText(text: string, max = 100): string {
83
- const flat = text.replace(/\s+/g, ' ').trim();
84
- return flat.length > max ? `${flat.slice(0, max)}…` : flat;
85
- }
86
-
87
- function renderResult(result: DeslopFileResult, threshold: number, palette: Palette): string[] {
88
- if ('error' in result) {
89
- return [`${palette.red('✗')} ${result.file} — ${palette.red(result.error)}`];
90
- }
91
- const response = result.response;
92
- if (response.skipped !== null) {
93
- return [`${palette.dim('○')} ${result.file} — ${palette.dim(`skipped (${response.skipped})`)}`];
94
- }
95
- const fraction = flaggedFraction(response);
96
- const failing = isFailing(fraction, threshold);
97
- const icon = failing ? palette.red('✗') : palette.green('✓');
98
- const percent = Math.round(fraction * 100);
99
- const lines = [`${icon} ${result.file} — ${response.predictionShort} (${percent}% AI-written)`];
100
- if (!failing) return lines;
101
- for (const window of response.windows) {
102
- lines.push(
103
- ` L${window.startLine}–${window.endLine} [${window.label} ${window.aiAssistanceScore.toFixed(2)}]`
104
- );
105
- lines.push(` ${palette.dim(truncateText(window.text))}`);
106
- for (const rewrite of window.rewrites ?? []) {
107
- lines.push(` ↻ ${rewrite.text}`);
108
- lines.push(` ${palette.dim(rewrite.rationale)}`);
109
- }
110
- }
111
- return lines;
112
- }
113
-
114
- type Summary = {
115
- checked: number;
116
- passed: number;
117
- failed: number;
118
- skipped: number;
119
- creditsCharged: number;
120
- errors: number;
121
- };
122
-
123
- function summarize(results: DeslopFileResult[], threshold: number): Summary {
124
- const summary: Summary = {
125
- checked: 0,
126
- passed: 0,
127
- failed: 0,
128
- skipped: 0,
129
- creditsCharged: 0,
130
- errors: 0,
131
- };
132
- for (const result of results) {
133
- if ('error' in result) {
134
- summary.errors += 1;
135
- continue;
136
- }
137
- summary.creditsCharged += result.response.creditsCharged;
138
- if (result.response.skipped !== null) {
139
- summary.skipped += 1;
140
- continue;
141
- }
142
- summary.checked += 1;
143
- if (isFailing(flaggedFraction(result.response), threshold)) {
144
- summary.failed += 1;
145
- } else {
146
- summary.passed += 1;
147
- }
148
- }
149
- return summary;
150
- }
151
-
152
- function renderSummaryLine(summary: Summary, palette: Palette): string {
153
- const parts = [
154
- `${summary.checked} checked`,
155
- palette.green(`${summary.passed} passed`),
156
- summary.failed > 0 ? palette.red(`${summary.failed} failed`) : `${summary.failed} failed`,
157
- palette.dim(`${summary.skipped} skipped`),
158
- `${summary.creditsCharged} ${summary.creditsCharged === 1 ? 'credit' : 'credits'} used`,
159
- ];
160
- if (summary.errors > 0) {
161
- parts.push(palette.red(`${summary.errors} ${summary.errors === 1 ? 'error' : 'errors'}`));
162
- }
163
- return parts.join(' · ');
164
- }
165
-
166
- export const deslopHandler = async (argv: DeslopArgs) => {
167
- const format = resolveFormat(argv);
168
- let spinnerAdded = false;
169
- const threshold = typeof argv.threshold === 'number' ? argv.threshold : 0.5;
170
- try {
171
- const subdomain = requireSubdomain(argv.subdomain);
172
- const fileArgs = (argv.files ?? []).map(String).filter(Boolean);
173
- const files =
174
- fileArgs.length > 0
175
- ? await resolveExplicitFiles(CMD_EXEC_PATH, fileArgs)
176
- : await resolveChangedFiles(CMD_EXEC_PATH);
177
-
178
- if (files.length === 0) {
179
- if (format === 'json') {
180
- process.stderr.write('No changed docs pages to check.\n');
181
- output(
182
- format,
183
- JSON.stringify(
184
- {
185
- threshold,
186
- results: [],
187
- summary: {
188
- checked: 0,
189
- passed: 0,
190
- failed: 0,
191
- skipped: 0,
192
- errors: 0,
193
- creditsCharged: 0,
194
- },
195
- },
196
- null,
197
- 2
198
- )
199
- );
200
- } else {
201
- output(format, 'No changed docs pages to check.');
202
- }
203
- await terminate(0);
204
- return;
205
- }
206
-
207
- const whitespaceChanges = argv.fixWhitespace ? await fixWhitespacePass(files, true) : [];
208
-
209
- const progressMessage = `Checking ${files.length} ${files.length === 1 ? 'page' : 'pages'} for AI-sounding prose...`;
210
- if (format === 'table') {
211
- addLog(<SpinnerLog message={progressMessage} />);
212
- spinnerAdded = true;
213
- } else {
214
- process.stderr.write(progressMessage + '\n');
215
- }
216
-
217
- const pages = await Promise.all(
218
- files.map(async (file) => ({
219
- path: file,
220
- content: await fs.readFile(path.join(CMD_EXEC_PATH, file), 'utf-8'),
221
- }))
222
- );
223
- const results = await checkPages(subdomain, pages);
224
- if (spinnerAdded) {
225
- removeLastLog();
226
- spinnerAdded = false;
227
- }
228
-
229
- const summary = summarize(results, threshold);
230
-
231
- void trackEvent('cli.deslop.executed', {
232
- fileCount: files.length,
233
- flaggedCount: summary.failed,
234
- format,
235
- });
236
-
237
- if (format === 'json') {
238
- output(
239
- format,
240
- JSON.stringify(
241
- {
242
- threshold,
243
- results: results.map((result) =>
244
- 'error' in result ? { path: result.file, error: result.error } : result.response
245
- ),
246
- summary: {
247
- checked: summary.checked,
248
- passed: summary.passed,
249
- failed: summary.failed,
250
- skipped: summary.skipped,
251
- errors: summary.errors,
252
- creditsCharged: summary.creditsCharged,
253
- },
254
- whitespaceFixed: whitespaceChanges,
255
- },
256
- null,
257
- 2
258
- )
259
- );
260
- } else {
261
- const palette = format === 'table' ? colorPalette : plainPalette;
262
- const lines: string[] = [];
263
- for (const change of whitespaceChanges) {
264
- lines.push(`${palette.green('⇥')} ${change.file} — whitespace fixed (${change.summary})`);
265
- }
266
- if (whitespaceChanges.length) lines.push('');
267
- for (const result of results) {
268
- lines.push(...renderResult(result, threshold, palette));
269
- }
270
- lines.push('');
271
- lines.push(renderSummaryLine(summary, palette));
272
- output(format, lines.join('\n'));
273
- }
274
-
275
- await terminate(summary.failed > 0 || summary.errors > 0 ? 1 : 0);
276
- } catch (err) {
277
- const message = err instanceof Error ? err.message : 'unknown error';
278
- if (format === 'table') {
279
- if (spinnerAdded) removeLastLog();
280
- addLog(<ErrorLog message={message} />);
281
- } else {
282
- process.stderr.write(`Error: ${message}\n`);
283
- }
284
- await terminate(1);
285
- }
286
- };
@@ -1,39 +0,0 @@
1
- export type PredictionShort = 'AI' | 'AI-Assisted' | 'Human' | 'Mixed';
2
-
3
- export interface Rewrite {
4
- text: string;
5
- rationale: string;
6
- }
7
-
8
- export interface DeslopWindow {
9
- text: string;
10
- label: string;
11
- aiAssistanceScore: number;
12
- confidence: string | number;
13
- startLine: number;
14
- endLine: number;
15
- rewrites?: Rewrite[];
16
- }
17
-
18
- export interface DeslopCheckedResponse {
19
- path: string;
20
- skipped: null;
21
- predictionShort: PredictionShort;
22
- fractionAi: number;
23
- fractionAiAssisted: number;
24
- fractionHuman: number;
25
- windows: DeslopWindow[];
26
- creditsCharged: number;
27
- }
28
-
29
- export interface DeslopSkippedResponse {
30
- path: string;
31
- skipped: string;
32
- creditsCharged: number;
33
- }
34
-
35
- export type DeslopResponse = DeslopCheckedResponse | DeslopSkippedResponse;
36
-
37
- export type DeslopFileResult =
38
- | { file: string; response: DeslopResponse }
39
- | { file: string; error: string };
@@ -1,155 +0,0 @@
1
- export interface WhitespaceReport {
2
- fixed: string;
3
- changed: boolean;
4
- counts: {
5
- trailing: number;
6
- blankRuns: number;
7
- unicodeSpaces: number;
8
- zeroWidth: number;
9
- spaces: number;
10
- finalNewline: boolean;
11
- };
12
- }
13
-
14
- // Unusual space characters that should read as a normal space.
15
- const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
16
- // Zero-width and BOM characters that should be removed outright.
17
- const ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
18
-
19
- const FENCE = /^(\s*)(`{3,}|~{3,})/;
20
- // Splits a line so odd indices are inline-code spans (left untouched).
21
- const INLINE_CODE = /(`+[^`]*`+)/;
22
-
23
- function isFrontmatterFence(line: string, index: number): boolean {
24
- return index === 0 && line.trim() === '---';
25
- }
26
-
27
- /**
28
- * Normalize prose whitespace in an MDX document without touching the bytes of
29
- * fenced code blocks or YAML frontmatter, where spacing is significant.
30
- */
31
- export function normalizeWhitespace(content: string): WhitespaceReport {
32
- const counts = {
33
- trailing: 0,
34
- blankRuns: 0,
35
- unicodeSpaces: 0,
36
- zeroWidth: 0,
37
- spaces: 0,
38
- finalNewline: false,
39
- };
40
-
41
- const lines = content.split('\n');
42
- const out: string[] = [];
43
- const isProtected: boolean[] = [];
44
- let inFence = false;
45
- let fenceMarker = '';
46
- let inFrontmatter = false;
47
-
48
- const pushProtected = (line: string) => {
49
- out.push(line);
50
- isProtected.push(true);
51
- };
52
-
53
- for (const [index, rawLine] of lines.entries()) {
54
- if (isFrontmatterFence(rawLine, index)) {
55
- inFrontmatter = true;
56
- pushProtected(rawLine);
57
- continue;
58
- }
59
- if (inFrontmatter) {
60
- pushProtected(rawLine);
61
- if (rawLine.trim() === '---') inFrontmatter = false;
62
- continue;
63
- }
64
-
65
- const fence = rawLine.match(FENCE);
66
- if (fence) {
67
- const marker = fence[2] ?? '';
68
- if (!inFence) {
69
- inFence = true;
70
- fenceMarker = marker;
71
- } else if (marker[0] === fenceMarker[0] && marker.length >= fenceMarker.length) {
72
- inFence = false;
73
- }
74
- pushProtected(rawLine);
75
- continue;
76
- }
77
- if (inFence) {
78
- pushProtected(rawLine);
79
- continue;
80
- }
81
-
82
- let line = rawLine;
83
- // Separate a trailing CR so CRLF files still get their trailing whitespace
84
- // stripped (and keep the CR so line endings are preserved).
85
- const cr = line.endsWith('\r') ? '\r' : '';
86
- if (cr) line = line.slice(0, -1);
87
-
88
- // Apply substitutions only outside inline-code spans (odd split indices),
89
- // so backticked code keeps its exact spacing.
90
- line = line
91
- .split(INLINE_CODE)
92
- .map((seg, i) => {
93
- if (i % 2 === 1) return seg;
94
- const uni = seg.match(UNICODE_SPACES);
95
- if (uni) counts.unicodeSpaces += uni.length;
96
- seg = seg.replace(UNICODE_SPACES, ' ');
97
- const zw = seg.match(ZERO_WIDTH);
98
- if (zw) counts.zeroWidth += zw.length;
99
- return seg.replace(ZERO_WIDTH, '');
100
- })
101
- .join('');
102
-
103
- const trimmed = line.replace(/[ \t]+$/, '');
104
- if (trimmed !== line) counts.trailing += 1;
105
- line = trimmed;
106
-
107
- // Collapse runs of internal spaces (outside inline code), preserving leading indentation.
108
- const leading = line.match(/^[ \t]*/)?.[0] ?? '';
109
- const body = line.slice(leading.length);
110
- const collapsed = body
111
- .split(INLINE_CODE)
112
- .map((seg, i) => (i % 2 === 1 ? seg : seg.replace(/ {2,}/g, ' ')))
113
- .join('');
114
- if (collapsed !== body) counts.spaces += 1;
115
- line = leading + collapsed + cr;
116
-
117
- out.push(line);
118
- isProtected.push(false);
119
- }
120
-
121
- // Collapse runs of 2 or more consecutive blank lines down to a single blank
122
- // line, but never inside protected regions (code fences, frontmatter).
123
- const collapsed: string[] = [];
124
- let blankRun = 0;
125
- for (const [index, line] of out.entries()) {
126
- if (!isProtected[index] && line.trim() === '') {
127
- blankRun += 1;
128
- if (blankRun <= 1) collapsed.push(line);
129
- continue;
130
- }
131
- if (blankRun > 1) counts.blankRuns += 1;
132
- blankRun = 0;
133
- collapsed.push(line);
134
- }
135
- if (blankRun > 1) counts.blankRuns += 1;
136
-
137
- let fixed = collapsed.join('\n').replace(/\n+$/, '');
138
- if (content.length > 0) {
139
- counts.finalNewline = !content.endsWith('\n') || content.endsWith('\n\n');
140
- fixed += '\n';
141
- }
142
-
143
- return { fixed, changed: fixed !== content, counts };
144
- }
145
-
146
- export function summarizeWhitespace(counts: WhitespaceReport['counts']): string | null {
147
- const parts: string[] = [];
148
- if (counts.trailing) parts.push(`${counts.trailing} trailing`);
149
- if (counts.blankRuns) parts.push(`${counts.blankRuns} blank-line runs`);
150
- if (counts.unicodeSpaces) parts.push(`${counts.unicodeSpaces} unicode spaces`);
151
- if (counts.zeroWidth) parts.push(`${counts.zeroWidth} zero-width chars`);
152
- if (counts.spaces) parts.push(`${counts.spaces} collapsed spaces`);
153
- if (counts.finalNewline) parts.push('final newline');
154
- return parts.length ? parts.join(', ') : null;
155
- }