@mintlify/cli 4.0.1434 → 4.0.1438
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/bin/agent-harness/buildTaskPrompt.js +76 -0
- package/bin/agent-harness/discoverCodeBlocks.js +101 -0
- package/bin/agent-harness/executeCodeBlocks.js +220 -0
- package/bin/agent-harness/filterSelectedPages.js +10 -0
- package/bin/agent-harness/generateTestCode.js +293 -0
- package/bin/agent-harness/index.js +85 -0
- package/bin/agent-harness/loadAgentSdk.js +46 -0
- package/bin/agent-harness/mapWithConcurrency.js +26 -0
- package/bin/agent-harness/runSave.js +47 -0
- package/bin/agent-harness/setupFolders.js +59 -0
- package/bin/agent-harness/tasks/checkTestability.js +226 -0
- package/bin/agent-harness/types.js +35 -0
- package/bin/cli.js +10 -4
- package/bin/mintTest.js +96 -0
- package/bin/mintTestNav.js +35 -0
- package/bin/mintTestTree.js +94 -0
- package/bin/mintTestUi.js +610 -0
- package/bin/status.js +11 -4
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +13 -9
- package/src/agent-harness/MINT_TEST_SYSTEM_DESIGN.md +58 -0
- package/src/agent-harness/buildTaskPrompt.ts +82 -0
- package/src/agent-harness/discoverCodeBlocks.ts +90 -0
- package/src/agent-harness/executeCodeBlocks.ts +285 -0
- package/src/agent-harness/filterSelectedPages.ts +14 -0
- package/src/agent-harness/generateTestCode.ts +332 -0
- package/src/agent-harness/index.ts +109 -0
- package/src/agent-harness/loadAgentSdk.ts +35 -0
- package/src/agent-harness/mapWithConcurrency.ts +21 -0
- package/src/agent-harness/runSave.ts +38 -0
- package/src/agent-harness/setupFolders.ts +73 -0
- package/src/agent-harness/tasks/checkTestability.ts +247 -0
- package/src/agent-harness/types.ts +152 -0
- package/src/cli.tsx +15 -9
- package/src/mintTest.tsx +102 -0
- package/src/mintTestNav.ts +31 -0
- package/src/mintTestTree.ts +134 -0
- package/src/mintTestUi.tsx +988 -0
- package/src/status.tsx +11 -4
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
11
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
12
|
+
var m = o[Symbol.asyncIterator], i;
|
|
13
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
14
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
15
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
16
|
+
};
|
|
17
|
+
import fs from 'node:fs/promises';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import z from 'zod';
|
|
20
|
+
import { buildCheckPrompt, CHECK_JSON_INSTRUCTION, CHECK_SYSTEM_PROMPT, CHECK_TOOL_INSTRUCTION, } from '../buildTaskPrompt.js';
|
|
21
|
+
import { loadClaudeAgentSdk, loadCodexSdk } from '../loadAgentSdk.js';
|
|
22
|
+
import { testCheckSchema, } from '../types.js';
|
|
23
|
+
const CHECK_FILENAME = 'mint-test-check.json';
|
|
24
|
+
const CHECK_MODEL = 'claude-haiku-4-5-20251001';
|
|
25
|
+
function readValidCheck(directory) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
try {
|
|
28
|
+
const check = testCheckSchema.parse(JSON.parse(yield fs.readFile(path.join(directory, CHECK_FILENAME), 'utf8')));
|
|
29
|
+
return validateCheck(check).length === 0 ? check : null;
|
|
30
|
+
}
|
|
31
|
+
catch (_a) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function validateCheck(check) {
|
|
37
|
+
return !check.testable && !check.reason ? ['Not-testable page needs a reason'] : [];
|
|
38
|
+
}
|
|
39
|
+
function parseJsonReply(reply) {
|
|
40
|
+
var _a;
|
|
41
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(reply);
|
|
42
|
+
const raw = (_a = fenced === null || fenced === void 0 ? void 0 : fenced[1]) !== null && _a !== void 0 ? _a : reply;
|
|
43
|
+
const start = raw.indexOf('{');
|
|
44
|
+
const end = raw.lastIndexOf('}');
|
|
45
|
+
if (start === -1 || end === -1)
|
|
46
|
+
throw new Error('Reply contained no JSON object');
|
|
47
|
+
return JSON.parse(raw.slice(start, end + 1));
|
|
48
|
+
}
|
|
49
|
+
function runTestabilityCheck(_a) {
|
|
50
|
+
return __awaiter(this, arguments, void 0, function* ({ file, content, agent, outputPath, signal, }) {
|
|
51
|
+
const prompt = buildCheckPrompt(file, content);
|
|
52
|
+
const workingDirectory = path.dirname(outputPath);
|
|
53
|
+
let tokens = 0;
|
|
54
|
+
let raw;
|
|
55
|
+
if (agent === 'claude') {
|
|
56
|
+
const reply = yield promptClaudeCheck({ prompt, workingDirectory, signal });
|
|
57
|
+
tokens = reply.tokens;
|
|
58
|
+
if (reply.captured === null)
|
|
59
|
+
throw new Error('agent never called report_testability');
|
|
60
|
+
raw = reply.captured;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const reply = yield promptCodex({
|
|
64
|
+
prompt,
|
|
65
|
+
systemPrompt: CHECK_SYSTEM_PROMPT + CHECK_JSON_INSTRUCTION,
|
|
66
|
+
workingDirectory,
|
|
67
|
+
signal,
|
|
68
|
+
});
|
|
69
|
+
tokens = reply.tokens;
|
|
70
|
+
raw = parseJsonReply(reply.result);
|
|
71
|
+
}
|
|
72
|
+
const check = testCheckSchema.parse(raw);
|
|
73
|
+
const errors = validateCheck(check);
|
|
74
|
+
if (errors.length > 0)
|
|
75
|
+
throw new Error(errors.join('\n'));
|
|
76
|
+
yield fs.writeFile(outputPath, `${JSON.stringify(check, null, 2)}\n`);
|
|
77
|
+
return { tokens, check };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export const checkFileTestability = (task_1, _a) => __awaiter(void 0, [task_1, _a], void 0, function* (task, { onTaskUpdate, signal }) {
|
|
81
|
+
const emit = (phase, tokens) => onTaskUpdate({
|
|
82
|
+
id: task.id,
|
|
83
|
+
agent: task.agent,
|
|
84
|
+
file: task.file,
|
|
85
|
+
phase,
|
|
86
|
+
tokens,
|
|
87
|
+
});
|
|
88
|
+
let attempts = 0;
|
|
89
|
+
let tokens = 0;
|
|
90
|
+
let lastError = 'run cancelled';
|
|
91
|
+
let check = yield readValidCheck(task.directory);
|
|
92
|
+
if (!check) {
|
|
93
|
+
emit('checking', tokens);
|
|
94
|
+
const content = yield fs.readFile(path.join(task.docsRoot, task.file), 'utf8');
|
|
95
|
+
for (let attempt = 1; attempt <= 2 && !signal.aborted && !check; attempt++) {
|
|
96
|
+
attempts = attempt;
|
|
97
|
+
try {
|
|
98
|
+
const reply = yield runTestabilityCheck({
|
|
99
|
+
file: task.file,
|
|
100
|
+
content,
|
|
101
|
+
agent: task.agent,
|
|
102
|
+
outputPath: path.join(task.directory, CHECK_FILENAME),
|
|
103
|
+
signal,
|
|
104
|
+
});
|
|
105
|
+
tokens += reply.tokens;
|
|
106
|
+
check = reply.check;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!check) {
|
|
113
|
+
if (signal.aborted) {
|
|
114
|
+
emit('cancelled', tokens);
|
|
115
|
+
return Object.assign(Object.assign({}, task), { attempts, tokens, error: 'run cancelled' });
|
|
116
|
+
}
|
|
117
|
+
emit('agent_error', tokens);
|
|
118
|
+
return Object.assign(Object.assign({}, task), { attempts, tokens, error: lastError });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!check.testable) {
|
|
122
|
+
emit('not_testable', tokens);
|
|
123
|
+
return Object.assign(Object.assign({}, task), { attempts, tokens, testable: false });
|
|
124
|
+
}
|
|
125
|
+
emit('checked', tokens);
|
|
126
|
+
return Object.assign(Object.assign({}, task), { attempts, tokens, testable: true });
|
|
127
|
+
});
|
|
128
|
+
function promptClaudeCheck(_a) {
|
|
129
|
+
return __awaiter(this, arguments, void 0, function* ({ prompt, workingDirectory, signal, }) {
|
|
130
|
+
var _b, e_1, _c, _d;
|
|
131
|
+
const { query, tool, createSdkMcpServer } = yield loadClaudeAgentSdk();
|
|
132
|
+
const abortController = new AbortController();
|
|
133
|
+
const abort = () => abortController.abort(signal.reason);
|
|
134
|
+
if (signal.aborted)
|
|
135
|
+
abort();
|
|
136
|
+
else
|
|
137
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
138
|
+
let captured = null;
|
|
139
|
+
const server = createSdkMcpServer({
|
|
140
|
+
name: 'mint-test',
|
|
141
|
+
tools: [
|
|
142
|
+
tool('report_testability', 'Report whether the documentation page is testable. Call exactly once.', {
|
|
143
|
+
version: z.literal(1),
|
|
144
|
+
testable: z.boolean(),
|
|
145
|
+
reason: z.string().optional(),
|
|
146
|
+
}, (input) => __awaiter(this, void 0, void 0, function* () {
|
|
147
|
+
captured = input;
|
|
148
|
+
return { content: [{ type: 'text', text: 'recorded' }] };
|
|
149
|
+
})),
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
const session = query({
|
|
153
|
+
prompt,
|
|
154
|
+
options: {
|
|
155
|
+
abortController,
|
|
156
|
+
allowedTools: ['mcp__mint-test__report_testability'],
|
|
157
|
+
cwd: workingDirectory,
|
|
158
|
+
mcpServers: { 'mint-test': server },
|
|
159
|
+
model: CHECK_MODEL,
|
|
160
|
+
permissionMode: 'dontAsk',
|
|
161
|
+
persistSession: false,
|
|
162
|
+
settingSources: [],
|
|
163
|
+
strictMcpConfig: true,
|
|
164
|
+
systemPrompt: CHECK_SYSTEM_PROMPT + CHECK_TOOL_INSTRUCTION,
|
|
165
|
+
tools: [],
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
try {
|
|
169
|
+
try {
|
|
170
|
+
for (var _e = true, session_1 = __asyncValues(session), session_1_1; session_1_1 = yield session_1.next(), _b = session_1_1.done, !_b; _e = true) {
|
|
171
|
+
_d = session_1_1.value;
|
|
172
|
+
_e = false;
|
|
173
|
+
const message = _d;
|
|
174
|
+
if (message.type !== 'result')
|
|
175
|
+
continue;
|
|
176
|
+
if (message.subtype !== 'success' || message.is_error) {
|
|
177
|
+
const details = message.subtype === 'success' ? message.result : message.errors.join('; ');
|
|
178
|
+
throw new Error(details || `Claude stopped with ${message.subtype}`);
|
|
179
|
+
}
|
|
180
|
+
const usage = message.usage;
|
|
181
|
+
return {
|
|
182
|
+
tokens: usage.input_tokens +
|
|
183
|
+
usage.output_tokens +
|
|
184
|
+
usage.cache_creation_input_tokens +
|
|
185
|
+
usage.cache_read_input_tokens,
|
|
186
|
+
captured,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
191
|
+
finally {
|
|
192
|
+
try {
|
|
193
|
+
if (!_e && !_b && (_c = session_1.return)) yield _c.call(session_1);
|
|
194
|
+
}
|
|
195
|
+
finally { if (e_1) throw e_1.error; }
|
|
196
|
+
}
|
|
197
|
+
throw new Error('Claude finished without returning a result');
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
signal.removeEventListener('abort', abort);
|
|
201
|
+
session.close();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function promptCodex(_a) {
|
|
206
|
+
return __awaiter(this, arguments, void 0, function* ({ prompt, systemPrompt, workingDirectory, signal, }) {
|
|
207
|
+
const { Codex } = yield loadCodexSdk();
|
|
208
|
+
const thread = new Codex().startThread({
|
|
209
|
+
approvalPolicy: 'never',
|
|
210
|
+
networkAccessEnabled: true,
|
|
211
|
+
sandboxMode: 'workspace-write',
|
|
212
|
+
skipGitRepoCheck: true,
|
|
213
|
+
workingDirectory,
|
|
214
|
+
});
|
|
215
|
+
const result = yield thread.run(`${systemPrompt}\n\n${prompt}`, { signal });
|
|
216
|
+
const usage = result.usage;
|
|
217
|
+
const tokens = usage
|
|
218
|
+
? usage.input_tokens +
|
|
219
|
+
usage.cached_input_tokens +
|
|
220
|
+
usage.cache_write_input_tokens +
|
|
221
|
+
usage.output_tokens +
|
|
222
|
+
usage.reasoning_output_tokens
|
|
223
|
+
: 0;
|
|
224
|
+
return { tokens, result: result.finalResponse };
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const identifier = z.string().trim().min(1).max(120);
|
|
3
|
+
const commandSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
id: identifier,
|
|
6
|
+
executable: z.string().trim().min(1).max(512),
|
|
7
|
+
args: z.array(z.string().max(10000)).max(100),
|
|
8
|
+
cwd: z.string().max(1024).optional(),
|
|
9
|
+
timeoutMs: z
|
|
10
|
+
.number()
|
|
11
|
+
.int()
|
|
12
|
+
.min(1000)
|
|
13
|
+
.max(30 * 60 * 1000)
|
|
14
|
+
.optional(),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
export const manifestSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
version: z.literal(1),
|
|
20
|
+
generatedFiles: z.array(z.string().min(1).max(1024)).max(1000),
|
|
21
|
+
setupCommands: z.array(commandSchema).max(50),
|
|
22
|
+
testCommands: z.array(commandSchema).max(200),
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
export const testCheckSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
version: z.literal(1),
|
|
28
|
+
testable: z.boolean(),
|
|
29
|
+
reason: z
|
|
30
|
+
.string()
|
|
31
|
+
.max(2000)
|
|
32
|
+
.optional()
|
|
33
|
+
.transform((value) => ((value === null || value === void 0 ? void 0 : value.trim()) ? value.trim() : undefined)),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
package/bin/cli.js
CHANGED
|
@@ -31,6 +31,7 @@ import { login } from './login.js';
|
|
|
31
31
|
import { logout } from './logout.js';
|
|
32
32
|
import { mdxLinter } from './mdxLinter.js';
|
|
33
33
|
import { createTelemetryMiddleware } from './middlewares/telemetryMiddleware.js';
|
|
34
|
+
import { mintTestHandler } from './mintTest.js';
|
|
34
35
|
import { checkOpenApiFile, getOpenApiFilenamesFromDocsConfig } from './openApiCheck.js';
|
|
35
36
|
import { resolveExplicitFiles } from './resolveFiles.js';
|
|
36
37
|
import { scoreHandler } from './score/index.js';
|
|
@@ -69,12 +70,15 @@ const showWelcome = (packageName) => __awaiter(void 0, void 0, void 0, function*
|
|
|
69
70
|
yield terminate(0);
|
|
70
71
|
});
|
|
71
72
|
export const cli = ({ packageName = 'mint' }) => {
|
|
72
|
-
|
|
73
|
+
const commandArguments = hideBin(process.argv);
|
|
74
|
+
if (shouldShowWelcome(commandArguments)) {
|
|
73
75
|
return showWelcome(packageName);
|
|
74
76
|
}
|
|
75
77
|
const telemetryMiddleware = createTelemetryMiddleware();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
if (commandArguments.find((argument) => !argument.startsWith('-')) !== 'test') {
|
|
79
|
+
render(_jsx(Logs, {}));
|
|
80
|
+
}
|
|
81
|
+
return (yargs(commandArguments)
|
|
78
82
|
.scriptName(packageName)
|
|
79
83
|
.option('telemetry', {
|
|
80
84
|
type: 'boolean',
|
|
@@ -196,6 +200,9 @@ export const cli = ({ packageName = 'mint' }) => {
|
|
|
196
200
|
const { cli: cliVersion } = getVersions(packageName);
|
|
197
201
|
yield validateBuild(Object.assign(Object.assign({}, argv), { packageName,
|
|
198
202
|
cliVersion }));
|
|
203
|
+
}))
|
|
204
|
+
.command('test', 'Test runnable code blocks with a local coding agent', (yargs) => yargs.usage('usage: mint test'), () => __awaiter(void 0, void 0, void 0, function* () {
|
|
205
|
+
yield mintTestHandler();
|
|
199
206
|
}))
|
|
200
207
|
.command('export', 'Export a static site for air-gapped deployment', (yargs) => yargs
|
|
201
208
|
.option('output', {
|
|
@@ -551,7 +558,6 @@ export const cli = ({ packageName = 'mint' }) => {
|
|
|
551
558
|
.example('mint format', 'format all MDX files in the current directory'), formatHandler)
|
|
552
559
|
// Coming soon commands — visible in help, tracked via telemetry to gauge interest.
|
|
553
560
|
.command('ai', '[Coming soon] AI-powered documentation (run mint ai to vote)', () => undefined, comingSoon('ai', packageName))
|
|
554
|
-
.command('test', '[Coming soon] Test your documentation (run mint test to vote)', () => undefined, comingSoon('test', packageName))
|
|
555
561
|
.command('mcp', '[Coming soon] MCP server for documentation (run mint mcp to vote)', () => undefined, comingSoon('mcp', packageName))
|
|
556
562
|
// Print the help menu when the user enters an invalid command.
|
|
557
563
|
.strictCommands()
|
package/bin/mintTest.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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 path from 'node:path';
|
|
11
|
+
import { DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CONCURRENCY, clearRunSave, discoverCodeBlocks, loadRunSave, runCodeTests, } from './agent-harness/index.js';
|
|
12
|
+
import { CMD_EXEC_PATH, findDocsRoot, isAI, terminate } from './helpers.js';
|
|
13
|
+
import { getAccessToken } from './keyring.js';
|
|
14
|
+
import { fileInNavPaths, loadDocsNavScope } from './mintTestNav.js';
|
|
15
|
+
import { countReport, formatDuration, runMintTestUi } from './mintTestUi.js';
|
|
16
|
+
import { getCliStatus } from './status.js';
|
|
17
|
+
export function mintTestHandler() {
|
|
18
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19
|
+
var _a, _b, _c, _d;
|
|
20
|
+
try {
|
|
21
|
+
const accessToken = yield getAccessToken().catch(() => null);
|
|
22
|
+
if (!accessToken) {
|
|
23
|
+
process.stderr.write('not logged in. Run `mint login` to authenticate.\n');
|
|
24
|
+
yield terminate(1);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const cliStatus = yield getCliStatus(accessToken);
|
|
28
|
+
if ((cliStatus === null || cliStatus === void 0 ? void 0 : cliStatus.org.name.toLowerCase()) !== 'mintlify') {
|
|
29
|
+
process.stderr.write('mint test is not available for your organization.\n');
|
|
30
|
+
yield terminate(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const targetPath = yield findDocsRoot(CMD_EXEC_PATH);
|
|
34
|
+
const interactive = process.stdin.isTTY === true &&
|
|
35
|
+
process.stdout.isTTY === true &&
|
|
36
|
+
!isAI() &&
|
|
37
|
+
!process.env.CI &&
|
|
38
|
+
process.env.CLI_TEST_MODE !== 'true';
|
|
39
|
+
const { docsRoot, pages: discoveredPages } = yield discoverCodeBlocks(targetPath);
|
|
40
|
+
const scope = yield loadDocsNavScope(docsRoot);
|
|
41
|
+
const pages = scope
|
|
42
|
+
? discoveredPages.filter((page) => fileInNavPaths(page.file, scope.navPaths))
|
|
43
|
+
: discoveredPages;
|
|
44
|
+
const autoSelectedFiles = (scope ? pages.filter((page) => fileInNavPaths(page.file, scope.autoSelectedPaths)) : pages).map((page) => page.file);
|
|
45
|
+
const totalPages = scope ? scope.navPaths.size : pages.length;
|
|
46
|
+
const outputDirectory = path.join(docsRoot, 'tests', 'mint-test');
|
|
47
|
+
let save = yield loadRunSave(outputDirectory);
|
|
48
|
+
if (save) {
|
|
49
|
+
const known = new Set(pages.map((page) => page.file));
|
|
50
|
+
const selectedFiles = save.selectedFiles.filter((file) => known.has(file));
|
|
51
|
+
if (selectedFiles.length === 0) {
|
|
52
|
+
yield clearRunSave(outputDirectory);
|
|
53
|
+
save = null;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
save = Object.assign(Object.assign({}, save), { selectedFiles });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (interactive) {
|
|
60
|
+
const exitCode = yield runMintTestUi({
|
|
61
|
+
targetPath,
|
|
62
|
+
docsRoot,
|
|
63
|
+
pages,
|
|
64
|
+
autoSelectedFiles,
|
|
65
|
+
totalPages,
|
|
66
|
+
save,
|
|
67
|
+
});
|
|
68
|
+
yield terminate(exitCode);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (save)
|
|
72
|
+
process.stdout.write(`resuming run ${save.runId}\n`);
|
|
73
|
+
const report = yield runCodeTests({
|
|
74
|
+
path: targetPath,
|
|
75
|
+
agents: (_a = save === null || save === void 0 ? void 0 : save.agents) !== null && _a !== void 0 ? _a : ['claude'],
|
|
76
|
+
model: (_b = save === null || save === void 0 ? void 0 : save.model) !== null && _b !== void 0 ? _b : 'claude-opus-4-8',
|
|
77
|
+
selectedFiles: (_c = save === null || save === void 0 ? void 0 : save.selectedFiles) !== null && _c !== void 0 ? _c : autoSelectedFiles,
|
|
78
|
+
outputDirectory,
|
|
79
|
+
runId: (_d = save === null || save === void 0 ? void 0 : save.runId) !== null && _d !== void 0 ? _d : null,
|
|
80
|
+
concurrency: DEFAULT_CONCURRENCY,
|
|
81
|
+
commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
|
|
82
|
+
signal: new AbortController().signal,
|
|
83
|
+
onTaskUpdate: () => { },
|
|
84
|
+
});
|
|
85
|
+
const counts = countReport(report.tasks);
|
|
86
|
+
process.stdout.write(`mint test ${report.status}: ${counts.passed} passed, ${counts.failed} failed, ${counts.agentErrors} agent errors\n`);
|
|
87
|
+
process.stdout.write(`finished in ${formatDuration(report.durationMs)}\n`);
|
|
88
|
+
process.stdout.write(`report: ${report.reportPath}\n`);
|
|
89
|
+
yield terminate(report.status === 'passed' ? 0 : 1);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
process.stderr.write(`mint test failed: ${error instanceof Error ? error.message : 'unknown error'}\n`);
|
|
93
|
+
yield terminate(1);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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 { getAllPathsInDocsNav } from '@mintlify/common';
|
|
11
|
+
import { getConfigObj } from '@mintlify/prebuild';
|
|
12
|
+
export function loadDocsNavScope(docsRoot) {
|
|
13
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
const config = (yield getConfigObj(docsRoot, 'docs').catch(() => null));
|
|
15
|
+
const navigation = config === null || config === void 0 ? void 0 : config.navigation;
|
|
16
|
+
if (!navigation)
|
|
17
|
+
return null;
|
|
18
|
+
const navPaths = new Set(getAllPathsInDocsNav(navigation));
|
|
19
|
+
if (navPaths.size === 0)
|
|
20
|
+
return null;
|
|
21
|
+
const firstLanguage = 'languages' in navigation ? navigation.languages[0] : null;
|
|
22
|
+
const autoSelectedPaths = firstLanguage ? new Set(getAllPathsInDocsNav(firstLanguage)) : navPaths;
|
|
23
|
+
return { navPaths, autoSelectedPaths };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function navCandidatesForFile(file) {
|
|
27
|
+
const base = file.replace(/\.mdx?$/i, '');
|
|
28
|
+
const candidates = [base];
|
|
29
|
+
if (base.endsWith('/index'))
|
|
30
|
+
candidates.push(base.slice(0, -'/index'.length));
|
|
31
|
+
return candidates;
|
|
32
|
+
}
|
|
33
|
+
export function fileInNavPaths(file, paths) {
|
|
34
|
+
return navCandidatesForFile(file).some((candidate) => paths.has(candidate));
|
|
35
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
function buildTree(pages) {
|
|
2
|
+
var _a;
|
|
3
|
+
const root = { id: 'dir:', kind: 'directory', label: '.', blockCount: 0, children: [] };
|
|
4
|
+
const directories = new Map([['', root]]);
|
|
5
|
+
for (const page of pages) {
|
|
6
|
+
const parts = page.file.split('/');
|
|
7
|
+
let parent = root;
|
|
8
|
+
let directoryPath = '';
|
|
9
|
+
for (const part of parts.slice(0, -1)) {
|
|
10
|
+
directoryPath = directoryPath ? `${directoryPath}/${part}` : part;
|
|
11
|
+
let directory = directories.get(directoryPath);
|
|
12
|
+
if (!directory) {
|
|
13
|
+
directory = {
|
|
14
|
+
id: `dir:${directoryPath}`,
|
|
15
|
+
kind: 'directory',
|
|
16
|
+
label: part,
|
|
17
|
+
blockCount: 0,
|
|
18
|
+
children: [],
|
|
19
|
+
};
|
|
20
|
+
directories.set(directoryPath, directory);
|
|
21
|
+
parent.children.push(directory);
|
|
22
|
+
}
|
|
23
|
+
parent = directory;
|
|
24
|
+
}
|
|
25
|
+
parent.children.push({
|
|
26
|
+
id: `file:${page.file}`,
|
|
27
|
+
kind: 'file',
|
|
28
|
+
label: (_a = parts[parts.length - 1]) !== null && _a !== void 0 ? _a : page.file,
|
|
29
|
+
file: page.file,
|
|
30
|
+
blockCount: page.blockCount,
|
|
31
|
+
children: [],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const sumBlocks = (node) => {
|
|
35
|
+
if (node.kind === 'file')
|
|
36
|
+
return node.blockCount;
|
|
37
|
+
node.blockCount = node.children.reduce((total, child) => total + sumBlocks(child), 0);
|
|
38
|
+
return node.blockCount;
|
|
39
|
+
};
|
|
40
|
+
sumBlocks(root);
|
|
41
|
+
const sortChildren = (node) => {
|
|
42
|
+
node.children.sort((left, right) => {
|
|
43
|
+
if (left.kind !== right.kind)
|
|
44
|
+
return left.kind === 'directory' ? -1 : 1;
|
|
45
|
+
return left.label.localeCompare(right.label);
|
|
46
|
+
});
|
|
47
|
+
for (const child of node.children)
|
|
48
|
+
sortChildren(child);
|
|
49
|
+
};
|
|
50
|
+
sortChildren(root);
|
|
51
|
+
return root;
|
|
52
|
+
}
|
|
53
|
+
function descendantFiles(node) {
|
|
54
|
+
if (node.kind === 'file')
|
|
55
|
+
return node.file ? [node.file] : [];
|
|
56
|
+
return node.children.flatMap(descendantFiles);
|
|
57
|
+
}
|
|
58
|
+
function selectionState(files, selectedFiles) {
|
|
59
|
+
if (files.length === 0)
|
|
60
|
+
return 'none';
|
|
61
|
+
const selectedCount = files.filter((file) => selectedFiles.has(file)).length;
|
|
62
|
+
if (selectedCount === 0)
|
|
63
|
+
return 'none';
|
|
64
|
+
return selectedCount === files.length ? 'all' : 'some';
|
|
65
|
+
}
|
|
66
|
+
export function visibleScopeRows(pages, expandedDirectories, selectedFiles) {
|
|
67
|
+
const root = buildTree(pages);
|
|
68
|
+
const rows = [];
|
|
69
|
+
const visit = (node, depth) => {
|
|
70
|
+
const files = descendantFiles(node);
|
|
71
|
+
const expanded = node.kind === 'directory' ? expandedDirectories.has(node.id) : undefined;
|
|
72
|
+
rows.push(Object.assign(Object.assign(Object.assign({ id: node.id, kind: node.kind, label: node.label, depth }, (node.file ? { file: node.file } : {})), { blockCount: node.blockCount, selectableFiles: files, selection: selectionState(files, selectedFiles) }), (expanded !== undefined ? { expanded } : {})));
|
|
73
|
+
if (node.kind === 'directory' && expanded) {
|
|
74
|
+
for (const child of node.children)
|
|
75
|
+
visit(child, depth + 1);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
for (const child of root.children)
|
|
79
|
+
visit(child, 0);
|
|
80
|
+
return rows;
|
|
81
|
+
}
|
|
82
|
+
export function toggleScopeSelection(selectedFiles, row) {
|
|
83
|
+
const next = new Set(selectedFiles);
|
|
84
|
+
if (row.selectableFiles.length === 0)
|
|
85
|
+
return next;
|
|
86
|
+
const shouldSelect = row.selectableFiles.some((file) => !next.has(file));
|
|
87
|
+
for (const file of row.selectableFiles) {
|
|
88
|
+
if (shouldSelect)
|
|
89
|
+
next.add(file);
|
|
90
|
+
else
|
|
91
|
+
next.delete(file);
|
|
92
|
+
}
|
|
93
|
+
return next;
|
|
94
|
+
}
|