@mintlify/cli 4.0.1315 → 4.0.1317

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,242 @@
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
+ });
@@ -0,0 +1,186 @@
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 { categorizeFilePaths, getMintIgnore } from '@mintlify/prebuild';
11
+ import { execFile } from 'node:child_process';
12
+ import fs from 'node:fs/promises';
13
+ import path from 'node:path';
14
+ const GLOB_CHARS = /[*?]/;
15
+ const DOCS_FILE = /\.mdx?$/i;
16
+ const REGEX_SPECIAL = /[.+^${}()|\\[\]]/;
17
+ const SKIPPED_DIRS = new Set(['.git', 'node_modules']);
18
+ function toPosix(filePath) {
19
+ return filePath.split(path.sep).join('/');
20
+ }
21
+ function normalizeArg(arg) {
22
+ return toPosix(arg).replace(/^\.\//, '');
23
+ }
24
+ function globToRegExp(pattern) {
25
+ let source = '';
26
+ let i = 0;
27
+ while (i < pattern.length) {
28
+ const char = pattern[i];
29
+ if (char === '*') {
30
+ if (pattern[i + 1] === '*') {
31
+ if (pattern[i + 2] === '/') {
32
+ source += '(?:[^/]+/)*';
33
+ i += 3;
34
+ }
35
+ else {
36
+ source += '.*';
37
+ i += 2;
38
+ }
39
+ }
40
+ else {
41
+ source += '[^/]*';
42
+ i += 1;
43
+ }
44
+ }
45
+ else if (char === '?') {
46
+ source += '[^/]';
47
+ i += 1;
48
+ }
49
+ else {
50
+ source += REGEX_SPECIAL.test(char) ? `\\${char}` : char;
51
+ i += 1;
52
+ }
53
+ }
54
+ return new RegExp(`^${source}$`);
55
+ }
56
+ function exists(filePath) {
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ try {
59
+ yield fs.access(filePath);
60
+ return true;
61
+ }
62
+ catch (_a) {
63
+ return false;
64
+ }
65
+ });
66
+ }
67
+ function isFile(filePath) {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ try {
70
+ return (yield fs.stat(filePath)).isFile();
71
+ }
72
+ catch (_a) {
73
+ return false;
74
+ }
75
+ });
76
+ }
77
+ function walk(baseDir_1) {
78
+ return __awaiter(this, arguments, void 0, function* (baseDir, dir = '') {
79
+ const entries = yield fs.readdir(path.join(baseDir, dir), { withFileTypes: true });
80
+ const files = [];
81
+ for (const entry of entries) {
82
+ if (SKIPPED_DIRS.has(entry.name))
83
+ continue;
84
+ const rel = dir ? `${dir}/${entry.name}` : entry.name;
85
+ if (entry.isDirectory()) {
86
+ files.push(...(yield walk(baseDir, rel)));
87
+ }
88
+ else {
89
+ files.push(rel);
90
+ }
91
+ }
92
+ return files;
93
+ });
94
+ }
95
+ export function resolveExplicitFiles(baseDir, args) {
96
+ return __awaiter(this, void 0, void 0, function* () {
97
+ let allFiles = null;
98
+ const matched = [];
99
+ const unmatched = [];
100
+ for (const arg of args) {
101
+ const pattern = normalizeArg(arg);
102
+ if (GLOB_CHARS.test(pattern)) {
103
+ allFiles = allFiles !== null && allFiles !== void 0 ? allFiles : (yield walk(baseDir));
104
+ const regex = globToRegExp(pattern);
105
+ const hits = allFiles.filter((file) => DOCS_FILE.test(file) && regex.test(file)).sort();
106
+ if (hits.length === 0) {
107
+ unmatched.push(arg);
108
+ }
109
+ else {
110
+ matched.push(...hits);
111
+ }
112
+ }
113
+ else if (yield isFile(path.join(baseDir, pattern))) {
114
+ matched.push(pattern);
115
+ }
116
+ else {
117
+ unmatched.push(arg);
118
+ }
119
+ }
120
+ if (unmatched.length > 0) {
121
+ throw new Error(`No matching files for: ${unmatched.join(', ')} (globs match .md and .mdx files)`);
122
+ }
123
+ return [...new Set(matched)];
124
+ });
125
+ }
126
+ function runGit(args, cwd) {
127
+ return new Promise((resolve, reject) => {
128
+ execFile('git', args, { cwd }, (error, stdout) => {
129
+ if (error)
130
+ reject(error);
131
+ else
132
+ resolve(stdout);
133
+ });
134
+ });
135
+ }
136
+ function parseGitLines(stdout) {
137
+ return stdout
138
+ .split('\n')
139
+ .map((line) => line.trim())
140
+ .filter(Boolean);
141
+ }
142
+ export function resolveChangedFiles(baseDir) {
143
+ return __awaiter(this, void 0, void 0, function* () {
144
+ try {
145
+ yield runGit(['rev-parse', '--is-inside-work-tree'], baseDir);
146
+ }
147
+ catch (_a) {
148
+ throw new Error('Not a git repository, so changed pages cannot be detected. Pass explicit paths: `mint deslop <files..>`.');
149
+ }
150
+ let diff;
151
+ let untracked;
152
+ try {
153
+ [diff, untracked] = yield Promise.all([
154
+ runGit(['-c', 'core.quotePath=false', 'diff', '--name-only', '--relative', 'HEAD'], baseDir),
155
+ runGit(['-c', 'core.quotePath=false', 'ls-files', '--others', '--exclude-standard'], baseDir),
156
+ ]);
157
+ }
158
+ catch (_b) {
159
+ throw new Error('Could not detect changed pages (does the repository have any commits?). Pass explicit paths: `mint deslop <files..>`.');
160
+ }
161
+ const candidates = [...new Set([...parseGitLines(diff), ...parseGitLines(untracked)])]
162
+ .map(normalizeArg)
163
+ .filter((file) => DOCS_FILE.test(file));
164
+ const existing = [];
165
+ for (const file of candidates) {
166
+ if (yield exists(path.join(baseDir, file)))
167
+ existing.push(file);
168
+ }
169
+ if (existing.length === 0)
170
+ return [];
171
+ if (!(yield exists(path.join(baseDir, 'docs.json')))) {
172
+ return existing;
173
+ }
174
+ try {
175
+ const mintIgnore = yield getMintIgnore(baseDir);
176
+ const { contentFilenames } = yield categorizeFilePaths(baseDir, mintIgnore);
177
+ const contentSet = new Set(contentFilenames.map((file) => toPosix(file).replace(/^\//, '')));
178
+ return existing.filter((file) => contentSet.has(file));
179
+ }
180
+ catch (_c) {
181
+ // docs.json is present but its navigation could not be resolved. Fail closed
182
+ // rather than sending possibly-ignored files to the credit-consuming API.
183
+ throw new Error('Could not read docs.json to determine which pages to check. Pass explicit paths: `mint deslop <files..>`.');
184
+ }
185
+ });
186
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,144 @@
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
+ }