@mintlify/cli 4.0.1466 → 4.0.1467

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.
Files changed (40) hide show
  1. package/__test__/mintTestFilePreview.test.ts +35 -0
  2. package/__test__/mintTestOutput.test.ts +194 -0
  3. package/__test__/runHistory.test.ts +164 -0
  4. package/bin/agent-harness/agentPreflight.js +24 -0
  5. package/bin/agent-harness/buildTaskPrompt.js +17 -7
  6. package/bin/agent-harness/countReport.js +10 -0
  7. package/bin/agent-harness/executeCodeBlocks.js +53 -10
  8. package/bin/agent-harness/generateTestCode.js +58 -43
  9. package/bin/agent-harness/index.js +14 -5
  10. package/bin/agent-harness/manifest.js +113 -0
  11. package/bin/agent-harness/runHistory.js +252 -0
  12. package/bin/agent-harness/setupFolders.js +20 -6
  13. package/bin/agent-harness/tasks/checkTestability.js +7 -5
  14. package/bin/agent-harness/types.js +33 -2
  15. package/bin/constants.js +3 -3
  16. package/bin/mintTest.js +31 -9
  17. package/bin/mintTestFilePreview.js +39 -0
  18. package/bin/mintTestText.js +31 -0
  19. package/bin/mintTestUi.js +295 -105
  20. package/bin/status.js +26 -0
  21. package/bin/tsconfig.build.tsbuildinfo +1 -1
  22. package/package.json +2 -2
  23. package/src/agent-harness/MINT_TEST_SYSTEM_DESIGN.md +15 -9
  24. package/src/agent-harness/agentPreflight.ts +13 -0
  25. package/src/agent-harness/buildTaskPrompt.ts +18 -7
  26. package/src/agent-harness/countReport.ts +17 -0
  27. package/src/agent-harness/executeCodeBlocks.ts +67 -1
  28. package/src/agent-harness/generateTestCode.ts +81 -51
  29. package/src/agent-harness/index.ts +20 -5
  30. package/src/agent-harness/manifest.ts +116 -0
  31. package/src/agent-harness/runHistory.ts +250 -0
  32. package/src/agent-harness/setupFolders.ts +19 -5
  33. package/src/agent-harness/tasks/checkTestability.ts +15 -6
  34. package/src/agent-harness/types.ts +81 -5
  35. package/src/constants.ts +6 -2
  36. package/src/mintTest.tsx +34 -8
  37. package/src/mintTestFilePreview.ts +32 -0
  38. package/src/mintTestText.ts +35 -0
  39. package/src/mintTestUi.tsx +586 -224
  40. package/src/status.tsx +24 -0
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  const identifier = z.string().trim().min(1).max(120);
3
3
  const commandSchema = z
4
4
  .object({
5
- id: identifier,
5
+ id: identifier.optional(),
6
6
  executable: z.string().trim().min(1).max(512),
7
7
  args: z.array(z.string().max(10000)).max(100),
8
8
  cwd: z.string().max(1024).optional(),
@@ -14,7 +14,7 @@ const commandSchema = z
14
14
  .optional(),
15
15
  })
16
16
  .strict();
17
- export const manifestSchema = z
17
+ const manifestFileSchema = z
18
18
  .object({
19
19
  version: z.literal(1),
20
20
  generatedFiles: z.array(z.string().min(1).max(1024)).max(1000),
@@ -22,6 +22,37 @@ export const manifestSchema = z
22
22
  testCommands: z.array(commandSchema).max(200),
23
23
  })
24
24
  .strict();
25
+ /**
26
+ * Agents often copy the manifest example verbatim and omit command ids. The id only needs to be
27
+ * unique within the manifest, so assign `setup-N` / `test-N` instead of rejecting the whole page.
28
+ */
29
+ export function assignCommandIds(manifest) {
30
+ const taken = new Set([...manifest.setupCommands, ...manifest.testCommands]
31
+ .map((command) => command.id)
32
+ .filter((id) => id !== undefined));
33
+ const withIds = (commands, phase) => {
34
+ let next = 1;
35
+ return commands.map((command) => {
36
+ if (command.id !== undefined)
37
+ return Object.assign(Object.assign({}, command), { id: command.id });
38
+ let id = `${phase}-${next}`;
39
+ while (taken.has(id)) {
40
+ next += 1;
41
+ id = `${phase}-${next}`;
42
+ }
43
+ taken.add(id);
44
+ next += 1;
45
+ return Object.assign(Object.assign({}, command), { id });
46
+ });
47
+ };
48
+ return {
49
+ version: manifest.version,
50
+ generatedFiles: manifest.generatedFiles,
51
+ setupCommands: withIds(manifest.setupCommands, 'setup'),
52
+ testCommands: withIds(manifest.testCommands, 'test'),
53
+ };
54
+ }
55
+ export const manifestSchema = manifestFileSchema.transform(assignCommandIds);
25
56
  export const testCheckSchema = z
26
57
  .object({
27
58
  version: z.literal(1),
package/bin/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- var _a, _b;
1
+ var _a, _b, _c, _d;
2
2
  import { LOCAL_LINKED_CLI_VERSION } from '@mintlify/previewing';
3
3
  import os from 'os';
4
4
  import path from 'path';
@@ -15,6 +15,6 @@ const DEV_TOKEN_ENDPOINT = 'https://test.stytch.com/v1/public/project-test-2d863
15
15
  const DEV_STYTCH_CLIENT_ID = 'connected-app-test-b597afb3-304a-420f-bc13-dacca566c59f';
16
16
  const PROD_TOKEN_ENDPOINT = 'https://api.stytch.com/v1/public/project-live-731b7a04-9ac3-4923-90b8-0806d4aa29d4/oauth2/token';
17
17
  const PROD_STYTCH_CLIENT_ID = 'connected-app-live-d813eedd-dbb0-434b-a1f9-2ce69e5efc49';
18
- export const TOKEN_ENDPOINT = IS_LOCAL_BUILD ? DEV_TOKEN_ENDPOINT : PROD_TOKEN_ENDPOINT;
19
- export const STYTCH_CLIENT_ID = IS_LOCAL_BUILD ? DEV_STYTCH_CLIENT_ID : PROD_STYTCH_CLIENT_ID;
18
+ export const TOKEN_ENDPOINT = (_c = process.env.MINTLIFY_TOKEN_ENDPOINT) !== null && _c !== void 0 ? _c : (IS_LOCAL_BUILD ? DEV_TOKEN_ENDPOINT : PROD_TOKEN_ENDPOINT);
19
+ export const STYTCH_CLIENT_ID = (_d = process.env.MINTLIFY_STYTCH_CLIENT_ID) !== null && _d !== void 0 ? _d : (IS_LOCAL_BUILD ? DEV_STYTCH_CLIENT_ID : PROD_STYTCH_CLIENT_ID);
20
20
  export const CUSTOM_DOMAIN_CNAME_TARGET = 'cname.mintlify.builders';
package/bin/mintTest.js CHANGED
@@ -8,12 +8,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import path from 'node:path';
11
- import { DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CONCURRENCY, clearRunSave, discoverCodeBlocks, loadRunSave, runCodeTests, } from './agent-harness/index.js';
11
+ import { DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CONCURRENCY, clearRunSave, countReport, discoverCodeBlocks, loadLatestTestRun, loadRunSave, runCodeTests, } from './agent-harness/index.js';
12
+ import { API_URL } from './constants.js';
12
13
  import { CMD_EXEC_PATH, findDocsRoot, isAI, terminate } from './helpers.js';
13
14
  import { getAccessToken } from './keyring.js';
14
15
  import { fileInNavPaths, loadDocsNavScope } from './mintTestNav.js';
15
- import { countReport, formatDuration, runMintTestUi } from './mintTestUi.js';
16
- import { getCliStatus } from './status.js';
16
+ import { firstLine, taskProblemLabel } from './mintTestText.js';
17
+ import { formatDuration, runMintTestUi } from './mintTestUi.js';
18
+ import { fetchCliStatus } from './status.js';
19
+ const ALLOWED_ORGS = new Set(['mintlify', 'mintlify internal']);
17
20
  export function mintTestHandler() {
18
21
  return __awaiter(this, void 0, void 0, function* () {
19
22
  var _a, _b, _c, _d;
@@ -24,10 +27,20 @@ export function mintTestHandler() {
24
27
  yield terminate(1);
25
28
  return;
26
29
  }
27
- const cliStatus = yield getCliStatus(accessToken);
28
- const orgName = cliStatus === null || cliStatus === void 0 ? void 0 : cliStatus.org.name.toLowerCase();
29
- if (orgName !== 'mintlify' && orgName !== 'mintlify internal') {
30
- process.stderr.write('mint test is not available for your organization.\n');
30
+ const cliStatus = yield fetchCliStatus();
31
+ if (!cliStatus.ok) {
32
+ const message = cliStatus.reason === 'unauthenticated'
33
+ ? 'your session has expired. Run `mint login` to authenticate.'
34
+ : cliStatus.reason === 'unreachable'
35
+ ? `could not reach Mintlify at ${API_URL}${cliStatus.detail ? ` (${cliStatus.detail})` : ''}.`
36
+ : 'unexpected response from Mintlify. Please try again.';
37
+ process.stderr.write(`${message}\n`);
38
+ yield terminate(1);
39
+ return;
40
+ }
41
+ const orgName = cliStatus.status.org.name;
42
+ if (!ALLOWED_ORGS.has(orgName.toLowerCase())) {
43
+ process.stderr.write(`mint test is not available for your organization (${orgName}).\n`);
31
44
  yield terminate(1);
32
45
  return;
33
46
  }
@@ -43,7 +56,6 @@ export function mintTestHandler() {
43
56
  ? discoveredPages.filter((page) => fileInNavPaths(page.file, scope.navPaths))
44
57
  : discoveredPages;
45
58
  const autoSelectedFiles = (scope ? pages.filter((page) => fileInNavPaths(page.file, scope.autoSelectedPaths)) : pages).map((page) => page.file);
46
- const totalPages = scope ? scope.navPaths.size : pages.length;
47
59
  const outputDirectory = path.join(docsRoot, 'tests', 'mint-test');
48
60
  let save = yield loadRunSave(outputDirectory);
49
61
  if (save) {
@@ -57,14 +69,15 @@ export function mintTestHandler() {
57
69
  save = Object.assign(Object.assign({}, save), { selectedFiles });
58
70
  }
59
71
  }
72
+ const lastReport = yield loadLatestTestRun(docsRoot);
60
73
  if (interactive) {
61
74
  const exitCode = yield runMintTestUi({
62
75
  targetPath,
63
76
  docsRoot,
64
77
  pages,
65
78
  autoSelectedFiles,
66
- totalPages,
67
79
  save,
80
+ lastReport,
68
81
  });
69
82
  yield terminate(exitCode);
70
83
  return;
@@ -87,6 +100,15 @@ export function mintTestHandler() {
87
100
  process.stdout.write(`mint test ${report.status}: ${counts.passed} passed, ${counts.failed} failed, ${counts.agentErrors} agent errors\n`);
88
101
  process.stdout.write(`finished in ${formatDuration(report.durationMs)}\n`);
89
102
  process.stdout.write(`report: ${report.reportPath}\n`);
103
+ for (const task of report.tasks) {
104
+ if (task.status !== 'failed' && task.status !== 'agent_error')
105
+ continue;
106
+ const reason = firstLine(task.error);
107
+ process.stdout.write(` ✗ ${task.file}: ${taskProblemLabel(task.status)}${reason ? ` (${reason})` : ''}\n`);
108
+ process.stdout.write(` output: ${task.directory}\n`);
109
+ }
110
+ if (report.historyError)
111
+ process.stderr.write(`warning: ${report.historyError}\n`);
90
112
  yield terminate(report.status === 'passed' ? 0 : 1);
91
113
  }
92
114
  catch (error) {
@@ -0,0 +1,39 @@
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
+ export const PREVIEW_BYTE_LIMIT = 64 * 1024;
12
+ export function readFilePreview(filePath) {
13
+ return __awaiter(this, void 0, void 0, function* () {
14
+ const buffer = new Uint8Array(PREVIEW_BYTE_LIMIT + 1);
15
+ const file = yield fs.open(filePath, 'r');
16
+ let bytesRead = 0;
17
+ try {
18
+ while (bytesRead < buffer.length) {
19
+ const result = yield file.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
20
+ if (result.bytesRead === 0)
21
+ break;
22
+ bytesRead += result.bytesRead;
23
+ }
24
+ }
25
+ finally {
26
+ yield file.close();
27
+ }
28
+ const contents = buffer.subarray(0, bytesRead);
29
+ if (contents.includes(0))
30
+ return { binary: true };
31
+ return {
32
+ binary: false,
33
+ text: new TextDecoder()
34
+ .decode(contents.subarray(0, PREVIEW_BYTE_LIMIT))
35
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '�'),
36
+ truncated: bytesRead > PREVIEW_BYTE_LIMIT,
37
+ };
38
+ });
39
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Split a footer hint such as `↑/↓/j/k move · Enter exit` into key/action pairs so the key can
3
+ * be rendered bold and the action plain.
4
+ */
5
+ export function footerSegments(text) {
6
+ return text
7
+ .split(' · ')
8
+ .map((segment) => segment.trim())
9
+ .filter((segment) => segment.length > 0)
10
+ .map((segment) => {
11
+ const split = segment.indexOf(' ');
12
+ if (split === -1)
13
+ return { key: segment, action: '' };
14
+ return { key: segment.slice(0, split), action: segment.slice(split + 1).trim() };
15
+ });
16
+ }
17
+ /** First non-empty line of a multi-line message, trimmed to fit a terminal row. */
18
+ export function firstLine(text, maxLength = 160) {
19
+ if (!text)
20
+ return '';
21
+ const line = text
22
+ .split(/\r?\n/)
23
+ .map((candidate) => candidate.trim())
24
+ .find((candidate) => candidate.length > 0);
25
+ if (!line)
26
+ return '';
27
+ return line.length > maxLength ? `${line.slice(0, maxLength - 1)}…` : line;
28
+ }
29
+ export function taskProblemLabel(status) {
30
+ return status === 'agent_error' ? 'test could not be generated' : 'tests failed';
31
+ }