@mintlify/cli 4.0.1433 → 4.0.1437
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/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/bin/vendor/converter.js +74 -23
- package/package.json +10 -9
- 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
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 { DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CONCURRENCY, clearRunSave, discoverCodeBlocks, loadRunSave, runCodeTests, } from '@mintlify/agent-harness';
|
|
11
|
+
import path from 'node:path';
|
|
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
|
+
}
|