@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/vendor/converter.js
CHANGED
|
@@ -1033,6 +1033,7 @@ function getImageWidthFromStyle(style) {
|
|
|
1033
1033
|
}
|
|
1034
1034
|
|
|
1035
1035
|
// ../editor/dist/converter/parse/handlers/media.js
|
|
1036
|
+
var isAttributeRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1036
1037
|
function parseImgNode(node, spreadAttrs = false) {
|
|
1037
1038
|
const attributes = parseMdxAttributes(node.attributes);
|
|
1038
1039
|
const className = attributes.className;
|
|
@@ -1042,7 +1043,7 @@ function parseImgNode(node, spreadAttrs = false) {
|
|
|
1042
1043
|
}
|
|
1043
1044
|
const style = attributes.style;
|
|
1044
1045
|
let width;
|
|
1045
|
-
if (style
|
|
1046
|
+
if (isAttributeRecord(style)) {
|
|
1046
1047
|
width = getImageWidthFromStyle(style);
|
|
1047
1048
|
}
|
|
1048
1049
|
return {
|
|
@@ -1050,8 +1051,18 @@ function parseImgNode(node, spreadAttrs = false) {
|
|
|
1050
1051
|
attrs: { imgAttributes: spreadAttrs ? Object.assign({}, attributes) : attributes, align, width }
|
|
1051
1052
|
};
|
|
1052
1053
|
}
|
|
1054
|
+
function isVideoSourceElement(child) {
|
|
1055
|
+
return (child.type === "mdxJsxTextElement" || child.type === "mdxJsxFlowElement") && child.name === "source";
|
|
1056
|
+
}
|
|
1053
1057
|
function parseVideoNode(node) {
|
|
1054
|
-
const
|
|
1058
|
+
const editable = node.children.every((child) => isVideoSourceElement(child) || child.type === "text" && child.value.trim() === "");
|
|
1059
|
+
if (!editable) {
|
|
1060
|
+
return {
|
|
1061
|
+
type: node.type === "mdxJsxTextElement" ? BLOCK_TYPES.inlineUnknownTag : BLOCK_TYPES.unknownTag,
|
|
1062
|
+
attrs: { mdxJsxNode: node }
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
const sources = node.children.filter(isVideoSourceElement).map((child) => parseMdxAttributes(child.attributes));
|
|
1055
1066
|
return {
|
|
1056
1067
|
type: BLOCK_TYPES.video,
|
|
1057
1068
|
attrs: {
|
|
@@ -1061,9 +1072,15 @@ function parseVideoNode(node) {
|
|
|
1061
1072
|
};
|
|
1062
1073
|
}
|
|
1063
1074
|
var DARK_ONLY_CLASSES = /* @__PURE__ */ new Set(["hidden", "dark:block"]);
|
|
1075
|
+
var LIGHT_ONLY_CLASSES = /* @__PURE__ */ new Set(["dark:hidden"]);
|
|
1076
|
+
function readImageAttributes(node) {
|
|
1077
|
+
var _a;
|
|
1078
|
+
const attributes = (_a = node.attrs) === null || _a === void 0 ? void 0 : _a.imgAttributes;
|
|
1079
|
+
return isAttributeRecord(attributes) ? attributes : null;
|
|
1080
|
+
}
|
|
1064
1081
|
function getImageClassName(node) {
|
|
1065
|
-
var _a
|
|
1066
|
-
const className = (
|
|
1082
|
+
var _a;
|
|
1083
|
+
const className = (_a = readImageAttributes(node)) === null || _a === void 0 ? void 0 : _a.className;
|
|
1067
1084
|
return typeof className === "string" ? className : void 0;
|
|
1068
1085
|
}
|
|
1069
1086
|
function isDarkOnlyImage(node) {
|
|
@@ -1083,18 +1100,35 @@ function isLightOnlyImage(node) {
|
|
|
1083
1100
|
return false;
|
|
1084
1101
|
return className.split(/\s+/).includes("dark:hidden");
|
|
1085
1102
|
}
|
|
1086
|
-
function stripThemeClasses(className) {
|
|
1087
|
-
|
|
1103
|
+
function stripThemeClasses(className, role) {
|
|
1104
|
+
if (!className)
|
|
1105
|
+
return void 0;
|
|
1106
|
+
const themeClasses = role === "dark" ? DARK_ONLY_CLASSES : LIGHT_ONLY_CLASSES;
|
|
1107
|
+
const cleaned = className.split(/\s+/).filter((c) => !themeClasses.has(c)).join(" ").trim();
|
|
1088
1108
|
return cleaned || void 0;
|
|
1089
1109
|
}
|
|
1110
|
+
function nonThemeAttributeEntries(attrs2) {
|
|
1111
|
+
return Object.entries(attrs2).filter(([key]) => key !== "src" && key !== "className").sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1112
|
+
}
|
|
1113
|
+
function canMergeAsThemePair(lightImgAttrs, darkImgAttrs, lightClassName, darkClassName) {
|
|
1114
|
+
const lightBase = stripThemeClasses(lightClassName, "light");
|
|
1115
|
+
const darkBase = stripThemeClasses(darkClassName, "dark");
|
|
1116
|
+
const lightBaseWithoutBlock = (lightBase === null || lightBase === void 0 ? void 0 : lightBase.split(/\s+/).filter((c) => c !== "block").join(" ").trim()) || void 0;
|
|
1117
|
+
if (darkBase !== lightBase && darkBase !== lightBaseWithoutBlock) {
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
const lightEntries = nonThemeAttributeEntries(lightImgAttrs);
|
|
1121
|
+
const darkEntries = nonThemeAttributeEntries(darkImgAttrs);
|
|
1122
|
+
return lightEntries.length === darkEntries.length && JSON.stringify(lightEntries) === JSON.stringify(darkEntries);
|
|
1123
|
+
}
|
|
1090
1124
|
function mergeDarkLightImagePairs(nodes2) {
|
|
1091
|
-
var _a, _b, _c, _d, _e, _f
|
|
1125
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1092
1126
|
const result = [];
|
|
1093
1127
|
let i = 0;
|
|
1094
1128
|
while (i < nodes2.length) {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1129
|
+
const current = nodes2[i];
|
|
1130
|
+
const next = nodes2[i + 1];
|
|
1131
|
+
if (current && next) {
|
|
1098
1132
|
let darkNode;
|
|
1099
1133
|
let lightNode;
|
|
1100
1134
|
if (isDarkOnlyImage(current) && isLightOnlyImage(next)) {
|
|
@@ -1104,12 +1138,12 @@ function mergeDarkLightImagePairs(nodes2) {
|
|
|
1104
1138
|
lightNode = current;
|
|
1105
1139
|
darkNode = next;
|
|
1106
1140
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
const baseClassName = stripThemeClasses(
|
|
1141
|
+
const darkImgAttrs = darkNode ? readImageAttributes(darkNode) : null;
|
|
1142
|
+
const lightImgAttrs = lightNode ? readImageAttributes(lightNode) : null;
|
|
1143
|
+
const darkSrc = darkImgAttrs === null || darkImgAttrs === void 0 ? void 0 : darkImgAttrs.src;
|
|
1144
|
+
const lightSrc = lightImgAttrs === null || lightImgAttrs === void 0 ? void 0 : lightImgAttrs.src;
|
|
1145
|
+
if (darkNode && lightNode && darkImgAttrs && lightImgAttrs && typeof darkSrc === "string" && typeof lightSrc === "string" && canMergeAsThemePair(lightImgAttrs, darkImgAttrs, getImageClassName(lightNode), getImageClassName(darkNode))) {
|
|
1146
|
+
const baseClassName = stripThemeClasses(getImageClassName(lightNode), "light");
|
|
1113
1147
|
const mergedImgAttrs = Object.assign({}, lightImgAttrs);
|
|
1114
1148
|
delete mergedImgAttrs.src;
|
|
1115
1149
|
if (baseClassName) {
|
|
@@ -1123,15 +1157,16 @@ function mergeDarkLightImagePairs(nodes2) {
|
|
|
1123
1157
|
imgAttributes: mergedImgAttrs,
|
|
1124
1158
|
lightSrc,
|
|
1125
1159
|
darkSrc,
|
|
1126
|
-
align: (
|
|
1127
|
-
width: (
|
|
1160
|
+
align: (_b = (_a = lightNode.attrs) === null || _a === void 0 ? void 0 : _a.align) !== null && _b !== void 0 ? _b : (_c = darkNode.attrs) === null || _c === void 0 ? void 0 : _c.align,
|
|
1161
|
+
width: (_e = (_d = lightNode.attrs) === null || _d === void 0 ? void 0 : _d.width) !== null && _e !== void 0 ? _e : (_f = darkNode.attrs) === null || _f === void 0 ? void 0 : _f.width
|
|
1128
1162
|
}
|
|
1129
1163
|
});
|
|
1130
1164
|
i += 2;
|
|
1131
1165
|
continue;
|
|
1132
1166
|
}
|
|
1133
1167
|
}
|
|
1134
|
-
|
|
1168
|
+
if (current)
|
|
1169
|
+
result.push(current);
|
|
1135
1170
|
i++;
|
|
1136
1171
|
}
|
|
1137
1172
|
return result;
|
|
@@ -1158,7 +1193,7 @@ function convertToInlineImage(node) {
|
|
|
1158
1193
|
let width = 100;
|
|
1159
1194
|
if (attributes.width && typeof attributes.width === "number") {
|
|
1160
1195
|
width = attributes.width;
|
|
1161
|
-
} else if (attributes.style
|
|
1196
|
+
} else if (isAttributeRecord(attributes.style)) {
|
|
1162
1197
|
const styleWidth = getImageWidthFromStyle(attributes.style);
|
|
1163
1198
|
if (styleWidth)
|
|
1164
1199
|
width = styleWidth;
|
|
@@ -2713,10 +2748,10 @@ function serializeJsxAttribute(attrs2, key, value) {
|
|
|
2713
2748
|
const serialized = serializePropValue(value);
|
|
2714
2749
|
return serialized ? `${key}=${serialized}` : null;
|
|
2715
2750
|
}
|
|
2716
|
-
var
|
|
2751
|
+
var isAttributeRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2717
2752
|
function getNamedExpressionAttr(attrs2, key) {
|
|
2718
2753
|
const raw = attrs2[NAMED_EXPRESSION_ATTRS_KEY];
|
|
2719
|
-
if (!
|
|
2754
|
+
if (!isAttributeRecord2(raw))
|
|
2720
2755
|
return void 0;
|
|
2721
2756
|
const value = raw[key];
|
|
2722
2757
|
return typeof value === "string" ? value : void 0;
|
|
@@ -2809,7 +2844,7 @@ function renderImage(state, node) {
|
|
|
2809
2844
|
return;
|
|
2810
2845
|
}
|
|
2811
2846
|
const hasExtraAttrs = Object.keys(imgAttributes).some((attr) => attr !== "src" && attr !== "alt" && attr !== "title");
|
|
2812
|
-
if (hasExtraAttrs || width || align) {
|
|
2847
|
+
if (hasExtraAttrs || width || align || !fitsMarkdownImageValues(Object.assign(Object.assign({}, imgAttributes), { src }))) {
|
|
2813
2848
|
const finalAttrs = Object.assign(Object.assign({}, imgAttributes), { src });
|
|
2814
2849
|
if (align) {
|
|
2815
2850
|
finalAttrs["className"] = addAlignClass(finalAttrs["className"], align);
|
|
@@ -2869,11 +2904,27 @@ function writeMarkdownImage(state, imgAttributes, src) {
|
|
|
2869
2904
|
state.write(``);
|
|
2870
2905
|
}
|
|
2871
2906
|
}
|
|
2907
|
+
var MARKDOWN_UNSAFE_SRC = /[\s<>\u0000-\u001F]/;
|
|
2908
|
+
var MARKDOWN_UNSAFE_TEXT = /[\r\n]/;
|
|
2909
|
+
function fitsMarkdownImageValues(attrs2) {
|
|
2910
|
+
const src = attrs2["src"];
|
|
2911
|
+
if (typeof src !== "string" || !src || MARKDOWN_UNSAFE_SRC.test(src))
|
|
2912
|
+
return false;
|
|
2913
|
+
const alt = attrs2["alt"];
|
|
2914
|
+
if (typeof alt === "string" && MARKDOWN_UNSAFE_TEXT.test(alt))
|
|
2915
|
+
return false;
|
|
2916
|
+
const title = attrs2["title"];
|
|
2917
|
+
if (typeof title === "string" && MARKDOWN_UNSAFE_TEXT.test(title))
|
|
2918
|
+
return false;
|
|
2919
|
+
return true;
|
|
2920
|
+
}
|
|
2872
2921
|
function fitsMarkdownImageSyntax(node, attrs2) {
|
|
2873
2922
|
if (!node.marks.some((mark) => mark.type.name === "link"))
|
|
2874
2923
|
return false;
|
|
2875
2924
|
if (typeof attrs2["src"] !== "string" || !attrs2["src"])
|
|
2876
2925
|
return false;
|
|
2926
|
+
if (!fitsMarkdownImageValues(attrs2))
|
|
2927
|
+
return false;
|
|
2877
2928
|
return Object.entries(attrs2).every(([key, value]) => key === "src" || key === "alt" || key === "title" || value == null);
|
|
2878
2929
|
}
|
|
2879
2930
|
function renderInlineImage(state, node) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mintlify/cli",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1437",
|
|
4
4
|
"description": "The Mintlify CLI",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -45,12 +45,13 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@inquirer/prompts": "7.9.0",
|
|
48
|
-
"@mintlify/
|
|
49
|
-
"@mintlify/
|
|
50
|
-
"@mintlify/
|
|
51
|
-
"@mintlify/
|
|
52
|
-
"@mintlify/
|
|
53
|
-
"@mintlify/
|
|
48
|
+
"@mintlify/agent-harness": "0.0.3",
|
|
49
|
+
"@mintlify/common": "1.0.1111",
|
|
50
|
+
"@mintlify/link-rot": "3.0.1313",
|
|
51
|
+
"@mintlify/models": "0.0.351",
|
|
52
|
+
"@mintlify/prebuild": "1.0.1265",
|
|
53
|
+
"@mintlify/previewing": "4.0.1337",
|
|
54
|
+
"@mintlify/validation": "0.1.842",
|
|
54
55
|
"adm-zip": "0.6.0",
|
|
55
56
|
"chalk": "5.2.0",
|
|
56
57
|
"color": "4.2.3",
|
|
@@ -82,7 +83,7 @@
|
|
|
82
83
|
"keytar": "7.9.0"
|
|
83
84
|
},
|
|
84
85
|
"devDependencies": {
|
|
85
|
-
"@mintlify/editor": "0.0.
|
|
86
|
+
"@mintlify/editor": "0.0.328",
|
|
86
87
|
"@mintlify/ts-config": "2.0.2",
|
|
87
88
|
"@tsconfig/recommended": "1.0.2",
|
|
88
89
|
"@types/detect-port": "1.3.2",
|
|
@@ -101,5 +102,5 @@
|
|
|
101
102
|
"vitest": "2.1.9",
|
|
102
103
|
"vitest-mock-process": "1.0.4"
|
|
103
104
|
},
|
|
104
|
-
"gitHead": "
|
|
105
|
+
"gitHead": "246494b8852335337e48cd0518b54abb2fb3f774"
|
|
105
106
|
}
|
package/src/cli.tsx
CHANGED
|
@@ -44,6 +44,7 @@ import { login } from './login.js';
|
|
|
44
44
|
import { logout } from './logout.js';
|
|
45
45
|
import { mdxLinter } from './mdxLinter.js';
|
|
46
46
|
import { createTelemetryMiddleware } from './middlewares/telemetryMiddleware.js';
|
|
47
|
+
import { mintTestHandler } from './mintTest.js';
|
|
47
48
|
import { checkOpenApiFile, getOpenApiFilenamesFromDocsConfig } from './openApiCheck.js';
|
|
48
49
|
import { resolveExplicitFiles } from './resolveFiles.js';
|
|
49
50
|
import { scoreHandler } from './score/index.js';
|
|
@@ -85,15 +86,18 @@ const showWelcome = async (packageName: string): Promise<void> => {
|
|
|
85
86
|
};
|
|
86
87
|
|
|
87
88
|
export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
|
|
88
|
-
|
|
89
|
+
const commandArguments = hideBin(process.argv);
|
|
90
|
+
if (shouldShowWelcome(commandArguments)) {
|
|
89
91
|
return showWelcome(packageName);
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
const telemetryMiddleware = createTelemetryMiddleware();
|
|
93
|
-
|
|
95
|
+
if (commandArguments.find((argument) => !argument.startsWith('-')) !== 'test') {
|
|
96
|
+
render(<Logs />);
|
|
97
|
+
}
|
|
94
98
|
|
|
95
99
|
return (
|
|
96
|
-
yargs(
|
|
100
|
+
yargs(commandArguments)
|
|
97
101
|
.scriptName(packageName)
|
|
98
102
|
.option('telemetry', {
|
|
99
103
|
type: 'boolean',
|
|
@@ -239,6 +243,14 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
|
|
|
239
243
|
});
|
|
240
244
|
}
|
|
241
245
|
)
|
|
246
|
+
.command(
|
|
247
|
+
'test',
|
|
248
|
+
'Test runnable code blocks with a local coding agent',
|
|
249
|
+
(yargs) => yargs.usage('usage: mint test'),
|
|
250
|
+
async () => {
|
|
251
|
+
await mintTestHandler();
|
|
252
|
+
}
|
|
253
|
+
)
|
|
242
254
|
.command(
|
|
243
255
|
'export',
|
|
244
256
|
'Export a static site for air-gapped deployment',
|
|
@@ -762,12 +774,6 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
|
|
|
762
774
|
() => undefined,
|
|
763
775
|
comingSoon('ai', packageName)
|
|
764
776
|
)
|
|
765
|
-
.command(
|
|
766
|
-
'test',
|
|
767
|
-
'[Coming soon] Test your documentation (run mint test to vote)',
|
|
768
|
-
() => undefined,
|
|
769
|
-
comingSoon('test', packageName)
|
|
770
|
-
)
|
|
771
777
|
.command(
|
|
772
778
|
'mcp',
|
|
773
779
|
'[Coming soon] MCP server for documentation (run mint mcp to vote)',
|
package/src/mintTest.tsx
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
3
|
+
DEFAULT_CONCURRENCY,
|
|
4
|
+
clearRunSave,
|
|
5
|
+
discoverCodeBlocks,
|
|
6
|
+
loadRunSave,
|
|
7
|
+
runCodeTests,
|
|
8
|
+
type RunSave,
|
|
9
|
+
} from '@mintlify/agent-harness';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
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
|
+
|
|
18
|
+
export async function mintTestHandler(): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
const accessToken = await getAccessToken().catch(() => null);
|
|
21
|
+
if (!accessToken) {
|
|
22
|
+
process.stderr.write('not logged in. Run `mint login` to authenticate.\n');
|
|
23
|
+
await terminate(1);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const cliStatus = await getCliStatus(accessToken);
|
|
27
|
+
if (cliStatus?.org.name.toLowerCase() !== 'mintlify') {
|
|
28
|
+
process.stderr.write('mint test is not available for your organization.\n');
|
|
29
|
+
await terminate(1);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const targetPath = await findDocsRoot(CMD_EXEC_PATH);
|
|
33
|
+
const interactive =
|
|
34
|
+
process.stdin.isTTY === true &&
|
|
35
|
+
process.stdout.isTTY === true &&
|
|
36
|
+
!isAI() &&
|
|
37
|
+
!process.env.CI &&
|
|
38
|
+
process.env.CLI_TEST_MODE !== 'true';
|
|
39
|
+
|
|
40
|
+
const { docsRoot, pages: discoveredPages } = await discoverCodeBlocks(targetPath);
|
|
41
|
+
const scope = await loadDocsNavScope(docsRoot);
|
|
42
|
+
const pages = scope
|
|
43
|
+
? discoveredPages.filter((page) => fileInNavPaths(page.file, scope.navPaths))
|
|
44
|
+
: discoveredPages;
|
|
45
|
+
const autoSelectedFiles = (
|
|
46
|
+
scope ? pages.filter((page) => fileInNavPaths(page.file, scope.autoSelectedPaths)) : pages
|
|
47
|
+
).map((page) => page.file);
|
|
48
|
+
const totalPages = scope ? scope.navPaths.size : pages.length;
|
|
49
|
+
const outputDirectory = path.join(docsRoot, 'tests', 'mint-test');
|
|
50
|
+
|
|
51
|
+
let save: RunSave | null = await loadRunSave(outputDirectory);
|
|
52
|
+
if (save) {
|
|
53
|
+
const known = new Set(pages.map((page) => page.file));
|
|
54
|
+
const selectedFiles = save.selectedFiles.filter((file) => known.has(file));
|
|
55
|
+
if (selectedFiles.length === 0) {
|
|
56
|
+
await clearRunSave(outputDirectory);
|
|
57
|
+
save = null;
|
|
58
|
+
} else {
|
|
59
|
+
save = { ...save, selectedFiles };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (interactive) {
|
|
64
|
+
const exitCode = await runMintTestUi({
|
|
65
|
+
targetPath,
|
|
66
|
+
docsRoot,
|
|
67
|
+
pages,
|
|
68
|
+
autoSelectedFiles,
|
|
69
|
+
totalPages,
|
|
70
|
+
save,
|
|
71
|
+
});
|
|
72
|
+
await terminate(exitCode);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (save) process.stdout.write(`resuming run ${save.runId}\n`);
|
|
77
|
+
const report = await runCodeTests({
|
|
78
|
+
path: targetPath,
|
|
79
|
+
agents: save?.agents ?? ['claude'],
|
|
80
|
+
model: save?.model ?? 'claude-opus-4-8',
|
|
81
|
+
selectedFiles: save?.selectedFiles ?? autoSelectedFiles,
|
|
82
|
+
outputDirectory,
|
|
83
|
+
runId: save?.runId ?? null,
|
|
84
|
+
concurrency: DEFAULT_CONCURRENCY,
|
|
85
|
+
commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
|
|
86
|
+
signal: new AbortController().signal,
|
|
87
|
+
onTaskUpdate: () => {},
|
|
88
|
+
});
|
|
89
|
+
const counts = countReport(report.tasks);
|
|
90
|
+
process.stdout.write(
|
|
91
|
+
`mint test ${report.status}: ${counts.passed} passed, ${counts.failed} failed, ${counts.agentErrors} agent errors\n`
|
|
92
|
+
);
|
|
93
|
+
process.stdout.write(`finished in ${formatDuration(report.durationMs)}\n`);
|
|
94
|
+
process.stdout.write(`report: ${report.reportPath}\n`);
|
|
95
|
+
await terminate(report.status === 'passed' ? 0 : 1);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
process.stderr.write(
|
|
98
|
+
`mint test failed: ${error instanceof Error ? error.message : 'unknown error'}\n`
|
|
99
|
+
);
|
|
100
|
+
await terminate(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getAllPathsInDocsNav } from '@mintlify/common';
|
|
2
|
+
import { getConfigObj } from '@mintlify/prebuild';
|
|
3
|
+
import type { DocsConfig, NavigationConfig } from '@mintlify/validation';
|
|
4
|
+
|
|
5
|
+
export interface DocsNavScope {
|
|
6
|
+
navPaths: Set<string>;
|
|
7
|
+
autoSelectedPaths: Set<string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function loadDocsNavScope(docsRoot: string): Promise<DocsNavScope | null> {
|
|
11
|
+
const config = (await getConfigObj(docsRoot, 'docs').catch(() => null)) as DocsConfig | null;
|
|
12
|
+
const navigation = config?.navigation;
|
|
13
|
+
if (!navigation) return null;
|
|
14
|
+
const navPaths = new Set(getAllPathsInDocsNav(navigation));
|
|
15
|
+
if (navPaths.size === 0) return null;
|
|
16
|
+
const firstLanguage =
|
|
17
|
+
'languages' in navigation ? (navigation.languages[0] as NavigationConfig | undefined) : null;
|
|
18
|
+
const autoSelectedPaths = firstLanguage ? new Set(getAllPathsInDocsNav(firstLanguage)) : navPaths;
|
|
19
|
+
return { navPaths, autoSelectedPaths };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function navCandidatesForFile(file: string): string[] {
|
|
23
|
+
const base = file.replace(/\.mdx?$/i, '');
|
|
24
|
+
const candidates = [base];
|
|
25
|
+
if (base.endsWith('/index')) candidates.push(base.slice(0, -'/index'.length));
|
|
26
|
+
return candidates;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function fileInNavPaths(file: string, paths: Set<string>): boolean {
|
|
30
|
+
return navCandidatesForFile(file).some((candidate) => paths.has(candidate));
|
|
31
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { DocsPage } from '@mintlify/agent-harness';
|
|
2
|
+
|
|
3
|
+
export type ScopeSelectionState = 'all' | 'some' | 'none';
|
|
4
|
+
|
|
5
|
+
export interface ScopeTreeRow {
|
|
6
|
+
id: string;
|
|
7
|
+
kind: 'directory' | 'file';
|
|
8
|
+
label: string;
|
|
9
|
+
depth: number;
|
|
10
|
+
file?: string;
|
|
11
|
+
blockCount: number;
|
|
12
|
+
selectableFiles: string[];
|
|
13
|
+
selection: ScopeSelectionState;
|
|
14
|
+
expanded?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TreeNode {
|
|
18
|
+
id: string;
|
|
19
|
+
kind: 'directory' | 'file';
|
|
20
|
+
label: string;
|
|
21
|
+
file?: string;
|
|
22
|
+
blockCount: number;
|
|
23
|
+
children: TreeNode[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildTree(pages: DocsPage[]): TreeNode {
|
|
27
|
+
const root: TreeNode = { id: 'dir:', kind: 'directory', label: '.', blockCount: 0, children: [] };
|
|
28
|
+
const directories = new Map<string, TreeNode>([['', root]]);
|
|
29
|
+
|
|
30
|
+
for (const page of pages) {
|
|
31
|
+
const parts = page.file.split('/');
|
|
32
|
+
let parent = root;
|
|
33
|
+
let directoryPath = '';
|
|
34
|
+
for (const part of parts.slice(0, -1)) {
|
|
35
|
+
directoryPath = directoryPath ? `${directoryPath}/${part}` : part;
|
|
36
|
+
let directory = directories.get(directoryPath);
|
|
37
|
+
if (!directory) {
|
|
38
|
+
directory = {
|
|
39
|
+
id: `dir:${directoryPath}`,
|
|
40
|
+
kind: 'directory',
|
|
41
|
+
label: part,
|
|
42
|
+
blockCount: 0,
|
|
43
|
+
children: [],
|
|
44
|
+
};
|
|
45
|
+
directories.set(directoryPath, directory);
|
|
46
|
+
parent.children.push(directory);
|
|
47
|
+
}
|
|
48
|
+
parent = directory;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
parent.children.push({
|
|
52
|
+
id: `file:${page.file}`,
|
|
53
|
+
kind: 'file',
|
|
54
|
+
label: parts[parts.length - 1] ?? page.file,
|
|
55
|
+
file: page.file,
|
|
56
|
+
blockCount: page.blockCount,
|
|
57
|
+
children: [],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sumBlocks = (node: TreeNode): number => {
|
|
62
|
+
if (node.kind === 'file') return node.blockCount;
|
|
63
|
+
node.blockCount = node.children.reduce((total, child) => total + sumBlocks(child), 0);
|
|
64
|
+
return node.blockCount;
|
|
65
|
+
};
|
|
66
|
+
sumBlocks(root);
|
|
67
|
+
|
|
68
|
+
const sortChildren = (node: TreeNode): void => {
|
|
69
|
+
node.children.sort((left, right) => {
|
|
70
|
+
if (left.kind !== right.kind) return left.kind === 'directory' ? -1 : 1;
|
|
71
|
+
return left.label.localeCompare(right.label);
|
|
72
|
+
});
|
|
73
|
+
for (const child of node.children) sortChildren(child);
|
|
74
|
+
};
|
|
75
|
+
sortChildren(root);
|
|
76
|
+
return root;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function descendantFiles(node: TreeNode): string[] {
|
|
80
|
+
if (node.kind === 'file') return node.file ? [node.file] : [];
|
|
81
|
+
return node.children.flatMap(descendantFiles);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function selectionState(files: string[], selectedFiles: ReadonlySet<string>): ScopeSelectionState {
|
|
85
|
+
if (files.length === 0) return 'none';
|
|
86
|
+
const selectedCount = files.filter((file) => selectedFiles.has(file)).length;
|
|
87
|
+
if (selectedCount === 0) return 'none';
|
|
88
|
+
return selectedCount === files.length ? 'all' : 'some';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function visibleScopeRows(
|
|
92
|
+
pages: DocsPage[],
|
|
93
|
+
expandedDirectories: ReadonlySet<string>,
|
|
94
|
+
selectedFiles: ReadonlySet<string>
|
|
95
|
+
): ScopeTreeRow[] {
|
|
96
|
+
const root = buildTree(pages);
|
|
97
|
+
const rows: ScopeTreeRow[] = [];
|
|
98
|
+
|
|
99
|
+
const visit = (node: TreeNode, depth: number) => {
|
|
100
|
+
const files = descendantFiles(node);
|
|
101
|
+
const expanded = node.kind === 'directory' ? expandedDirectories.has(node.id) : undefined;
|
|
102
|
+
rows.push({
|
|
103
|
+
id: node.id,
|
|
104
|
+
kind: node.kind,
|
|
105
|
+
label: node.label,
|
|
106
|
+
depth,
|
|
107
|
+
...(node.file ? { file: node.file } : {}),
|
|
108
|
+
blockCount: node.blockCount,
|
|
109
|
+
selectableFiles: files,
|
|
110
|
+
selection: selectionState(files, selectedFiles),
|
|
111
|
+
...(expanded !== undefined ? { expanded } : {}),
|
|
112
|
+
});
|
|
113
|
+
if (node.kind === 'directory' && expanded) {
|
|
114
|
+
for (const child of node.children) visit(child, depth + 1);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
for (const child of root.children) visit(child, 0);
|
|
119
|
+
return rows;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function toggleScopeSelection(
|
|
123
|
+
selectedFiles: ReadonlySet<string>,
|
|
124
|
+
row: ScopeTreeRow
|
|
125
|
+
): Set<string> {
|
|
126
|
+
const next = new Set(selectedFiles);
|
|
127
|
+
if (row.selectableFiles.length === 0) return next;
|
|
128
|
+
const shouldSelect = row.selectableFiles.some((file) => !next.has(file));
|
|
129
|
+
for (const file of row.selectableFiles) {
|
|
130
|
+
if (shouldSelect) next.add(file);
|
|
131
|
+
else next.delete(file);
|
|
132
|
+
}
|
|
133
|
+
return next;
|
|
134
|
+
}
|