@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,76 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export const SYSTEM_PROMPT = `# Mint test generator
|
|
3
|
+
|
|
4
|
+
You will be provided a markdown page guide that contians codeblocks. Follow the guide and implement
|
|
5
|
+
the code as instrucuted by the guide into a given output directory.
|
|
6
|
+
|
|
7
|
+
Once finished, create a mint-test.json file in the current working directory with this shape:
|
|
8
|
+
|
|
9
|
+
{
|
|
10
|
+
"version": 1,
|
|
11
|
+
"generatedFiles": ["tests/example.test.js", "tests/test_example.py"],
|
|
12
|
+
"setupCommands": [],
|
|
13
|
+
"testCommands": [
|
|
14
|
+
{
|
|
15
|
+
"executable": "node",
|
|
16
|
+
"args": ["--test", "tests/"],
|
|
17
|
+
"cwd": ".",
|
|
18
|
+
"timeoutMs": 120000
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"executable": "python3",
|
|
22
|
+
"args": ["-m", "unittest", "discover", "-s", "tests"],
|
|
23
|
+
"cwd": ".",
|
|
24
|
+
"timeoutMs": 120000
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Command cwd values must be relative to the current working directory. Finish with a short plain-text summary.
|
|
30
|
+
|
|
31
|
+
Rules:
|
|
32
|
+
- Read only the page named in the task. You may also read docs.json and an OpenAPI file referenced by that page.
|
|
33
|
+
- The output directory is already scaffolded: package.json exists with "npm test" wired to "node --test tests/", and a tests/ directory is ready. Do not recreate or rewrite this scaffolding.
|
|
34
|
+
- Write JavaScript and TypeScript tests as ES modules named tests/*.test.js using the Node built-in test runner (node:test and node:assert). Write Python tests as tests/test_*.py using the standard library unittest module, run with python3 -m unittest discover -s tests.
|
|
35
|
+
- Prefer the standard library over installing dependencies; add a setup command only when a test truly cannot run without one.
|
|
36
|
+
- Never write README, docs, or explanation files.
|
|
37
|
+
- Keep helpers inline in the test file; do not create separate helper modules.
|
|
38
|
+
- Write one test file per page unless the page genuinely needs more.
|
|
39
|
+
- Create every additional source file and fixture inside the current working directory.
|
|
40
|
+
- Never modify the documentation source. Do not commit, push, or open a pull request.
|
|
41
|
+
- Cover fenced code blocks inside Mintlify components such as CodeGroup, Tabs, Steps, RequestExample, and ResponseExample.
|
|
42
|
+
- If the page has openapi frontmatter, you may resolve its spec relative to the docs root and add tests for request snippets.
|
|
43
|
+
- Do not install dependencies or run the generated tests. The Mint CLI handles setup and execution.
|
|
44
|
+
- Never copy secrets into a file. In your code pull secrets from .env
|
|
45
|
+
`;
|
|
46
|
+
export function buildTaskPrompt(docsRoot, file, directory) {
|
|
47
|
+
return `
|
|
48
|
+
Use the guide at "${JSON.stringify(path.join(docsRoot, file))}"
|
|
49
|
+
|
|
50
|
+
Write the required tests and mint-test.json output in the "${JSON.stringify(directory)}" directory.
|
|
51
|
+
Record your report here "${JSON.stringify(file)}"`;
|
|
52
|
+
}
|
|
53
|
+
export const CHECK_SYSTEM_PROMPT = `# Mint test triage
|
|
54
|
+
You are given a markdown page guide. Decide if the given file is able to be tested.
|
|
55
|
+
|
|
56
|
+
Testable pages are pages that include any runnable code (python, typescript, bash) and any logic that could fail if the documented code was wrong
|
|
57
|
+
|
|
58
|
+
Code blocks are not testable when it is used for formatting, showing logs, configuration, pseudo-code or a diff, an illustrative request or response shape, or a UI-only example.
|
|
59
|
+
|
|
60
|
+
When unsure, mark it not testable.
|
|
61
|
+
|
|
62
|
+
Include "reason" only when the page is not testable; omit it entirely when testable is true.`;
|
|
63
|
+
export const CHECK_TOOL_INSTRUCTION = `
|
|
64
|
+
|
|
65
|
+
Report your classification by calling the report_testability tool`;
|
|
66
|
+
export const CHECK_JSON_INSTRUCTION = `
|
|
67
|
+
|
|
68
|
+
Respond with only this JSON object and nothing else:
|
|
69
|
+
{ "version": 1, "testable": false, "reason": "expected output, not runnable code" }`;
|
|
70
|
+
export function buildCheckPrompt(file, fileContnet) {
|
|
71
|
+
return `Report if the file at filePath ${JSON.stringify(file)} is testable
|
|
72
|
+
|
|
73
|
+
<file>
|
|
74
|
+
${fileContnet}
|
|
75
|
+
</file>`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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 fs from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
const PAGE_EXTENSIONS = new Set(['.md', '.mdx']);
|
|
13
|
+
const IGNORED_DIRECTORIES = new Set(['.git', '.next', 'build', 'dist', 'node_modules']);
|
|
14
|
+
const OPENING_FENCE_PATTERN = /^\s*(`{3,}|~{3,})/;
|
|
15
|
+
function countFencedBlocks(content) {
|
|
16
|
+
var _a, _b;
|
|
17
|
+
const lines = content.split(/\r?\n/);
|
|
18
|
+
let count = 0;
|
|
19
|
+
for (let index = 0; index < lines.length; index++) {
|
|
20
|
+
const opening = OPENING_FENCE_PATTERN.exec((_a = lines[index]) !== null && _a !== void 0 ? _a : '');
|
|
21
|
+
const marker = opening === null || opening === void 0 ? void 0 : opening[1];
|
|
22
|
+
if (!marker)
|
|
23
|
+
continue;
|
|
24
|
+
const closingPattern = new RegExp(`^\\s*${marker[0]}{${marker.length},}\\s*$`);
|
|
25
|
+
let closingIndex = index + 1;
|
|
26
|
+
while (closingIndex < lines.length && !closingPattern.test((_b = lines[closingIndex]) !== null && _b !== void 0 ? _b : '')) {
|
|
27
|
+
closingIndex++;
|
|
28
|
+
}
|
|
29
|
+
if (closingIndex >= lines.length)
|
|
30
|
+
break;
|
|
31
|
+
count++;
|
|
32
|
+
index = closingIndex;
|
|
33
|
+
}
|
|
34
|
+
return count;
|
|
35
|
+
}
|
|
36
|
+
function findSiteRoot(startDirectory) {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
const { root } = path.parse(startDirectory);
|
|
39
|
+
let directory = startDirectory;
|
|
40
|
+
let previous = null;
|
|
41
|
+
while (directory !== previous) {
|
|
42
|
+
for (const configName of ['docs.json', 'mint.json']) {
|
|
43
|
+
const stats = yield fs.stat(path.join(directory, configName)).catch(() => null);
|
|
44
|
+
if (stats === null || stats === void 0 ? void 0 : stats.isFile())
|
|
45
|
+
return directory;
|
|
46
|
+
}
|
|
47
|
+
if (directory === root)
|
|
48
|
+
return null;
|
|
49
|
+
previous = directory;
|
|
50
|
+
directory = path.dirname(directory);
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function discoverCodeBlocks(inputPath) {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
var _a;
|
|
58
|
+
const resolvedPath = path.resolve(inputPath);
|
|
59
|
+
const stats = yield fs.stat(resolvedPath);
|
|
60
|
+
const baseDirectory = stats.isDirectory() ? resolvedPath : path.dirname(resolvedPath);
|
|
61
|
+
const docsRoot = (_a = (yield findSiteRoot(baseDirectory))) !== null && _a !== void 0 ? _a : baseDirectory;
|
|
62
|
+
const filePaths = [];
|
|
63
|
+
if (stats.isDirectory()) {
|
|
64
|
+
const directories = [resolvedPath];
|
|
65
|
+
while (directories.length > 0) {
|
|
66
|
+
const directory = directories.pop();
|
|
67
|
+
if (!directory)
|
|
68
|
+
break;
|
|
69
|
+
for (const entry of yield fs.readdir(directory, { withFileTypes: true })) {
|
|
70
|
+
const entryPath = path.join(directory, entry.name);
|
|
71
|
+
if (entry.isDirectory() &&
|
|
72
|
+
!IGNORED_DIRECTORIES.has(entry.name) &&
|
|
73
|
+
!(entry.name === 'mint-test' && path.basename(directory) === 'tests')) {
|
|
74
|
+
directories.push(entryPath);
|
|
75
|
+
}
|
|
76
|
+
else if (entry.isFile() && PAGE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
|
|
77
|
+
filePaths.push(entryPath);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (stats.isFile() && PAGE_EXTENSIONS.has(path.extname(resolvedPath).toLowerCase())) {
|
|
83
|
+
filePaths.push(resolvedPath);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
throw new Error(`Expected a docs directory or an MD/MDX file: ${resolvedPath}`);
|
|
87
|
+
}
|
|
88
|
+
const pages = [];
|
|
89
|
+
for (const filePath of filePaths.sort()) {
|
|
90
|
+
const content = yield fs.readFile(filePath, 'utf8');
|
|
91
|
+
const blockCount = countFencedBlocks(content);
|
|
92
|
+
if (blockCount > 0) {
|
|
93
|
+
pages.push({
|
|
94
|
+
file: path.relative(docsRoot, filePath).split(path.sep).join('/'),
|
|
95
|
+
blockCount,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { docsRoot, pages };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
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 { spawn } from 'node:child_process';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { mapWithConcurrency } from './mapWithConcurrency.js';
|
|
13
|
+
const MAX_STREAM_BYTES = 128 * 1024;
|
|
14
|
+
const KILL_GRACE_MS = 2000;
|
|
15
|
+
const FAILURE_PRECEDENCE = ['failed', 'cancelled'];
|
|
16
|
+
const SECRET_NAME_PATTERN = /(TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE_KEY|CREDENTIAL)/i;
|
|
17
|
+
function redactSecrets(value) {
|
|
18
|
+
let redacted = value;
|
|
19
|
+
const secrets = Object.entries(process.env)
|
|
20
|
+
.filter(([name, secret]) => SECRET_NAME_PATTERN.test(name) && typeof secret === 'string' && secret.length >= 8)
|
|
21
|
+
.map(([, secret]) => secret)
|
|
22
|
+
.sort((left, right) => right.length - left.length);
|
|
23
|
+
for (const secret of secrets)
|
|
24
|
+
redacted = redacted.split(secret).join('[REDACTED]');
|
|
25
|
+
return redacted
|
|
26
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, 'Bearer [REDACTED]')
|
|
27
|
+
.replace(/\b(?:sk|sess)-[A-Za-z0-9._-]{12,}\b/gi, '[REDACTED]');
|
|
28
|
+
}
|
|
29
|
+
function outputCollector() {
|
|
30
|
+
let output = '';
|
|
31
|
+
let bytes = 0;
|
|
32
|
+
let truncated = false;
|
|
33
|
+
return {
|
|
34
|
+
append(chunk) {
|
|
35
|
+
if (truncated)
|
|
36
|
+
return;
|
|
37
|
+
const value = chunk.toString();
|
|
38
|
+
const remaining = MAX_STREAM_BYTES - bytes;
|
|
39
|
+
if (Buffer.byteLength(value) <= remaining) {
|
|
40
|
+
output += value;
|
|
41
|
+
bytes += Buffer.byteLength(value);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
output += Buffer.from(value).subarray(0, Math.max(0, remaining)).toString();
|
|
45
|
+
output += '\n[mint test: output truncated]\n';
|
|
46
|
+
truncated = true;
|
|
47
|
+
},
|
|
48
|
+
value: () => output,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function unrunCommandResult(command, phase, cwd, status, error) {
|
|
52
|
+
return {
|
|
53
|
+
id: command.id,
|
|
54
|
+
phase,
|
|
55
|
+
status,
|
|
56
|
+
executable: command.executable,
|
|
57
|
+
args: command.args,
|
|
58
|
+
cwd,
|
|
59
|
+
durationMs: 0,
|
|
60
|
+
exitCode: null,
|
|
61
|
+
stdout: '',
|
|
62
|
+
stderr: '',
|
|
63
|
+
error,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function spawnCommand({ command, phase, cwd, timeoutMs, signal, docsRootForCommands, }) {
|
|
67
|
+
var _a;
|
|
68
|
+
const startedAt = Date.now();
|
|
69
|
+
const base = {
|
|
70
|
+
id: command.id,
|
|
71
|
+
phase,
|
|
72
|
+
executable: command.executable,
|
|
73
|
+
args: command.args,
|
|
74
|
+
cwd,
|
|
75
|
+
};
|
|
76
|
+
const commandTimeoutMs = (_a = command.timeoutMs) !== null && _a !== void 0 ? _a : timeoutMs;
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const stdout = outputCollector();
|
|
79
|
+
const stderr = outputCollector();
|
|
80
|
+
let completed = false;
|
|
81
|
+
let timedOut = false;
|
|
82
|
+
let cancelled = false;
|
|
83
|
+
let forceKillTimer;
|
|
84
|
+
const child = spawn(command.executable, command.args, {
|
|
85
|
+
cwd,
|
|
86
|
+
detached: true,
|
|
87
|
+
env: Object.assign(Object.assign({}, process.env), { MINT_DOCS_ROOT: docsRootForCommands }),
|
|
88
|
+
shell: false,
|
|
89
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
|
+
});
|
|
91
|
+
child.stdout.on('data', stdout.append);
|
|
92
|
+
child.stderr.on('data', stderr.append);
|
|
93
|
+
const killTree = (killSignal) => {
|
|
94
|
+
if (child.pid) {
|
|
95
|
+
try {
|
|
96
|
+
process.kill(-child.pid, killSignal);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
catch (_a) {
|
|
100
|
+
/* noop */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
child.kill(killSignal);
|
|
104
|
+
};
|
|
105
|
+
const kill = () => {
|
|
106
|
+
killTree('SIGTERM');
|
|
107
|
+
forceKillTimer = setTimeout(() => killTree('SIGKILL'), KILL_GRACE_MS);
|
|
108
|
+
};
|
|
109
|
+
const timeout = setTimeout(() => {
|
|
110
|
+
timedOut = true;
|
|
111
|
+
kill();
|
|
112
|
+
}, commandTimeoutMs);
|
|
113
|
+
const abort = () => {
|
|
114
|
+
cancelled = true;
|
|
115
|
+
kill();
|
|
116
|
+
};
|
|
117
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
118
|
+
const finish = (exitCode, spawnError) => {
|
|
119
|
+
if (completed)
|
|
120
|
+
return;
|
|
121
|
+
completed = true;
|
|
122
|
+
clearTimeout(timeout);
|
|
123
|
+
if (forceKillTimer)
|
|
124
|
+
clearTimeout(forceKillTimer);
|
|
125
|
+
signal.removeEventListener('abort', abort);
|
|
126
|
+
const error = cancelled
|
|
127
|
+
? 'run cancelled'
|
|
128
|
+
: timedOut
|
|
129
|
+
? `command timed out after ${commandTimeoutMs}ms`
|
|
130
|
+
: spawnError;
|
|
131
|
+
resolve(Object.assign(Object.assign(Object.assign({}, base), { status: cancelled ? 'cancelled' : exitCode === 0 && !error ? 'passed' : 'failed', durationMs: Date.now() - startedAt, exitCode, stdout: redactSecrets(stdout.value()), stderr: redactSecrets(stderr.value()) }), (error ? { error } : {})));
|
|
132
|
+
};
|
|
133
|
+
child.once('error', (error) => finish(null, error.message));
|
|
134
|
+
child.once('close', (exitCode) => finish(exitCode));
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
export function executeCodeBlocks(_a) {
|
|
138
|
+
return __awaiter(this, arguments, void 0, function* ({ tasks, docsRoot, concurrency, commandTimeoutMs, signal, onTaskUpdate, }) {
|
|
139
|
+
return mapWithConcurrency(tasks, concurrency, (task) => __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
var _a;
|
|
141
|
+
const base = {
|
|
142
|
+
id: task.id,
|
|
143
|
+
agent: task.agent,
|
|
144
|
+
file: task.file,
|
|
145
|
+
directory: task.directory,
|
|
146
|
+
attempts: task.attempts,
|
|
147
|
+
tokens: task.tokens,
|
|
148
|
+
};
|
|
149
|
+
const manifest = task.manifest;
|
|
150
|
+
if (!manifest) {
|
|
151
|
+
if (!task.error) {
|
|
152
|
+
onTaskUpdate({
|
|
153
|
+
id: task.id,
|
|
154
|
+
agent: task.agent,
|
|
155
|
+
file: task.file,
|
|
156
|
+
phase: 'not_testable',
|
|
157
|
+
tokens: task.tokens,
|
|
158
|
+
});
|
|
159
|
+
return Object.assign(Object.assign({}, base), { status: 'passed', commands: [] });
|
|
160
|
+
}
|
|
161
|
+
const status = signal.aborted ? 'cancelled' : 'agent_error';
|
|
162
|
+
onTaskUpdate({
|
|
163
|
+
id: task.id,
|
|
164
|
+
agent: task.agent,
|
|
165
|
+
file: task.file,
|
|
166
|
+
phase: status,
|
|
167
|
+
tokens: task.tokens,
|
|
168
|
+
});
|
|
169
|
+
return Object.assign(Object.assign({}, base), { status, error: task.error, commands: [] });
|
|
170
|
+
}
|
|
171
|
+
onTaskUpdate({
|
|
172
|
+
id: task.id,
|
|
173
|
+
agent: task.agent,
|
|
174
|
+
file: task.file,
|
|
175
|
+
phase: 'running',
|
|
176
|
+
tokens: task.tokens,
|
|
177
|
+
});
|
|
178
|
+
const commands = [];
|
|
179
|
+
let setupFailure;
|
|
180
|
+
const run = (command, phase) => __awaiter(this, void 0, void 0, function* () {
|
|
181
|
+
var _a;
|
|
182
|
+
const cwd = path.resolve(task.directory, (_a = command.cwd) !== null && _a !== void 0 ? _a : '.');
|
|
183
|
+
if (signal.aborted) {
|
|
184
|
+
return unrunCommandResult(command, phase, cwd, 'cancelled', 'run cancelled');
|
|
185
|
+
}
|
|
186
|
+
if (phase === 'test' && setupFailure) {
|
|
187
|
+
return unrunCommandResult(command, phase, cwd, setupFailure, `a setup command ${setupFailure === 'cancelled' ? 'was cancelled' : 'failed'}`);
|
|
188
|
+
}
|
|
189
|
+
return spawnCommand({
|
|
190
|
+
command,
|
|
191
|
+
phase,
|
|
192
|
+
cwd,
|
|
193
|
+
timeoutMs: commandTimeoutMs,
|
|
194
|
+
signal,
|
|
195
|
+
docsRootForCommands: docsRoot,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
for (const command of manifest.setupCommands) {
|
|
199
|
+
const result = yield run(command, 'setup');
|
|
200
|
+
commands.push(result);
|
|
201
|
+
if (result.status !== 'passed') {
|
|
202
|
+
setupFailure = result.status;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const command of manifest.testCommands) {
|
|
207
|
+
commands.push(yield run(command, 'test'));
|
|
208
|
+
}
|
|
209
|
+
const status = (_a = FAILURE_PRECEDENCE.find((candidate) => commands.some((command) => command.status === candidate))) !== null && _a !== void 0 ? _a : 'passed';
|
|
210
|
+
onTaskUpdate({
|
|
211
|
+
id: task.id,
|
|
212
|
+
agent: task.agent,
|
|
213
|
+
file: task.file,
|
|
214
|
+
phase: status,
|
|
215
|
+
tokens: task.tokens,
|
|
216
|
+
});
|
|
217
|
+
return Object.assign(Object.assign({}, base), { status, commands });
|
|
218
|
+
}));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function filterSelectedPages(pages, selectedFiles) {
|
|
3
|
+
const selected = new Set(selectedFiles.map((file) => file.split(path.sep).join('/').replace(/^\.\//, '')));
|
|
4
|
+
const known = new Set(pages.map((page) => page.file));
|
|
5
|
+
for (const file of selected) {
|
|
6
|
+
if (!known.has(file))
|
|
7
|
+
throw new Error(`Selected docs file was not found: ${file}`);
|
|
8
|
+
}
|
|
9
|
+
return pages.filter((page) => selected.has(page.file));
|
|
10
|
+
}
|