@mintlify/cli 4.0.1122 → 4.0.1124
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 +0 -11
- package/bin/score/client.js +16 -1
- package/bin/score/index.js +85 -20
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/cli.tsx +0 -17
- package/src/score/client.ts +19 -2
- package/src/score/index.tsx +82 -23
- package/src/score/types.ts +16 -2
- package/__test__/workflow.test.ts +0 -320
- package/bin/workflow.js +0 -150
- package/src/workflow.tsx +0 -191
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mintlify/cli",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1124",
|
|
4
4
|
"description": "The Mintlify CLI",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -93,5 +93,5 @@
|
|
|
93
93
|
"vitest": "2.1.9",
|
|
94
94
|
"vitest-mock-process": "1.0.4"
|
|
95
95
|
},
|
|
96
|
-
"gitHead": "
|
|
96
|
+
"gitHead": "8bfaa040d399554049b90d0780deebcfc26a624c"
|
|
97
97
|
}
|
package/src/cli.tsx
CHANGED
|
@@ -44,7 +44,6 @@ import { scoreHandler } from './score/index.js';
|
|
|
44
44
|
import { status, getCliSubdomains } from './status.js';
|
|
45
45
|
import { trackTelemetryPreferenceChange } from './telemetry/track.js';
|
|
46
46
|
import { update } from './update.js';
|
|
47
|
-
import { addWorkflow } from './workflow.js';
|
|
48
47
|
|
|
49
48
|
export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
|
|
50
49
|
const telemetryMiddleware = createTelemetryMiddleware();
|
|
@@ -554,22 +553,6 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
|
|
|
554
553
|
}
|
|
555
554
|
}
|
|
556
555
|
)
|
|
557
|
-
.command(
|
|
558
|
-
'workflow',
|
|
559
|
-
'Add a workflow to your documentation repository',
|
|
560
|
-
() => undefined,
|
|
561
|
-
async () => {
|
|
562
|
-
try {
|
|
563
|
-
await addWorkflow();
|
|
564
|
-
await terminate(0);
|
|
565
|
-
} catch (error) {
|
|
566
|
-
addLog(
|
|
567
|
-
<ErrorLog message={error instanceof Error ? error.message : 'error occurred'} />
|
|
568
|
-
);
|
|
569
|
-
await terminate(1);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
)
|
|
573
556
|
.command('analytics', 'View analytics for your documentation', analyticsBuilder)
|
|
574
557
|
.command(
|
|
575
558
|
'score <url>',
|
package/src/score/client.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { authenticatedFetch } from '../authenticatedFetch.js';
|
|
2
2
|
import { API_URL } from '../constants.js';
|
|
3
|
-
import type { ScoreResponse } from './types.js';
|
|
3
|
+
import type { ResolveResult, ScoreResponse } from './types.js';
|
|
4
4
|
|
|
5
|
-
export async function
|
|
5
|
+
export async function resolveScoreForUrl(url: string): Promise<ResolveResult> {
|
|
6
6
|
const endpoint = new URL(`${API_URL}/api/cli/score`);
|
|
7
7
|
endpoint.searchParams.set('url', url);
|
|
8
8
|
|
|
@@ -10,6 +10,23 @@ export async function getScore(url: string): Promise<ScoreResponse> {
|
|
|
10
10
|
headers: { Accept: 'application/json' },
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
if (res.status !== 200 && res.status !== 202) {
|
|
14
|
+
const body = await res.text().catch(() => '');
|
|
15
|
+
throw new Error(`API error (${res.status}): ${body || res.statusText}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return res.json() as Promise<ResolveResult>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function fetchScoreBySlug(slug: string): Promise<ScoreResponse | null> {
|
|
22
|
+
const endpoint = new URL(`${API_URL}/api/cli/score/${encodeURIComponent(slug)}`);
|
|
23
|
+
|
|
24
|
+
const res = await authenticatedFetch(endpoint.toString(), {
|
|
25
|
+
headers: { Accept: 'application/json' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (res.status === 202) return null;
|
|
29
|
+
|
|
13
30
|
if (!res.ok) {
|
|
14
31
|
const body = await res.text().catch(() => '');
|
|
15
32
|
throw new Error(`API error (${res.status}): ${body || res.statusText}`);
|
package/src/score/index.tsx
CHANGED
|
@@ -4,11 +4,14 @@ import { Text } from 'ink';
|
|
|
4
4
|
|
|
5
5
|
import { terminate } from '../helpers.js';
|
|
6
6
|
import { trackEvent } from '../telemetry/track.js';
|
|
7
|
-
import {
|
|
8
|
-
import type { Check } from './types.js';
|
|
7
|
+
import { fetchScoreBySlug, resolveScoreForUrl } from './client.js';
|
|
8
|
+
import type { Check, ScoreResponse } from './types.js';
|
|
9
9
|
|
|
10
10
|
type OutputFormat = 'table' | 'plain' | 'json';
|
|
11
11
|
|
|
12
|
+
const POLL_INTERVAL_MS = 3_000;
|
|
13
|
+
const POLL_TIMEOUT_MS = 120_000;
|
|
14
|
+
|
|
12
15
|
function resolveFormat(argv: { format?: string }): OutputFormat {
|
|
13
16
|
if (argv.format === 'table' || argv.format === 'plain' || argv.format === 'json')
|
|
14
17
|
return argv.format;
|
|
@@ -49,6 +52,21 @@ function renderPlain(checks: Check[], prefix = ''): string[] {
|
|
|
49
52
|
return lines;
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
function renderTable(score: ScoreResponse, opts?: { stale?: boolean }): string {
|
|
56
|
+
const scoreColor =
|
|
57
|
+
score.overallScore >= 70 ? 'green' : score.overallScore >= 40 ? 'yellow' : 'red';
|
|
58
|
+
const header = opts?.stale
|
|
59
|
+
? chalk.bold(`\nAgent Readiness Score — ${score.canonicalUrl}`) +
|
|
60
|
+
chalk.dim(` (last run ${new Date(score.computedAt).toLocaleString()})\n`)
|
|
61
|
+
: chalk.bold(`\nAgent Readiness Score — ${score.canonicalUrl}\n`);
|
|
62
|
+
|
|
63
|
+
const lines: string[] = [header];
|
|
64
|
+
lines.push(` Score: ${chalk[scoreColor].bold(`${score.overallScore}%`)}\n`);
|
|
65
|
+
lines.push(chalk.bold(' Checks'));
|
|
66
|
+
lines.push(...renderChecks(score.checks));
|
|
67
|
+
return lines.join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
function output(format: OutputFormat, text: string) {
|
|
53
71
|
if (format === 'table') {
|
|
54
72
|
addLog(<Text>{text}</Text>);
|
|
@@ -57,39 +75,80 @@ function output(format: OutputFormat, text: string) {
|
|
|
57
75
|
}
|
|
58
76
|
}
|
|
59
77
|
|
|
78
|
+
async function sleep(ms: number): Promise<void> {
|
|
79
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function pollForScore(
|
|
83
|
+
slug: string,
|
|
84
|
+
afterIso: string | null,
|
|
85
|
+
onAttempt?: () => void
|
|
86
|
+
): Promise<ScoreResponse> {
|
|
87
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
88
|
+
while (Date.now() < deadline) {
|
|
89
|
+
await sleep(POLL_INTERVAL_MS);
|
|
90
|
+
onAttempt?.();
|
|
91
|
+
const score = await fetchScoreBySlug(slug);
|
|
92
|
+
if (!score) continue;
|
|
93
|
+
if (afterIso === null) return score;
|
|
94
|
+
if (new Date(score.computedAt).getTime() > new Date(afterIso).getTime()) return score;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Score did not complete within ${POLL_TIMEOUT_MS / 1000}s`);
|
|
97
|
+
}
|
|
98
|
+
|
|
60
99
|
export const scoreHandler = async (argv: { url: string; format?: string }) => {
|
|
61
100
|
const format = resolveFormat(argv);
|
|
62
101
|
try {
|
|
63
|
-
if (format === 'table') addLog(<SpinnerLog message="
|
|
64
|
-
const
|
|
102
|
+
if (format === 'table') addLog(<SpinnerLog message="Checking agent readiness..." />);
|
|
103
|
+
const resolved = await resolveScoreForUrl(argv.url);
|
|
104
|
+
|
|
105
|
+
let final: ScoreResponse;
|
|
106
|
+
|
|
107
|
+
if (resolved.status === 'ready') {
|
|
108
|
+
const score = await fetchScoreBySlug(resolved.slug);
|
|
109
|
+
if (format === 'table') removeLastLog();
|
|
110
|
+
if (!score) throw new Error('Score was unexpectedly unavailable');
|
|
111
|
+
final = score;
|
|
112
|
+
} else if (resolved.status === 'stale_refresh_queued') {
|
|
113
|
+
const stale = await fetchScoreBySlug(resolved.slug);
|
|
114
|
+
if (format === 'table') removeLastLog();
|
|
115
|
+
if (!stale) throw new Error('Stale score was unexpectedly unavailable');
|
|
116
|
+
|
|
117
|
+
if (format === 'table') {
|
|
118
|
+
output('table', renderTable(stale, { stale: true }));
|
|
119
|
+
addLog(<SpinnerLog message="Refreshing score in the background..." />);
|
|
120
|
+
} else {
|
|
121
|
+
process.stderr.write('Refreshing score in the background...\n');
|
|
122
|
+
}
|
|
123
|
+
final = await pollForScore(resolved.slug, stale.computedAt);
|
|
124
|
+
if (format === 'table') removeLastLog();
|
|
125
|
+
} else {
|
|
126
|
+
if (format === 'table') {
|
|
127
|
+
removeLastLog();
|
|
128
|
+
addLog(<SpinnerLog message="Queued a new scoring run. Waiting for first result..." />);
|
|
129
|
+
} else {
|
|
130
|
+
process.stderr.write('Queued a new scoring run. Waiting for first result...\n');
|
|
131
|
+
}
|
|
132
|
+
final = await pollForScore(resolved.slug, null);
|
|
133
|
+
if (format === 'table') removeLastLog();
|
|
134
|
+
}
|
|
135
|
+
|
|
65
136
|
void trackEvent('cli.score.executed', {
|
|
66
137
|
url: argv.url,
|
|
67
|
-
score:
|
|
138
|
+
score: final.overallScore,
|
|
68
139
|
format,
|
|
140
|
+
status: resolved.status,
|
|
69
141
|
});
|
|
70
|
-
if (format === 'table') removeLastLog();
|
|
71
142
|
|
|
72
143
|
if (format === 'json') {
|
|
73
|
-
output(format, JSON.stringify(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (format === 'plain') {
|
|
79
|
-
const lines = [`SCORE\t${data.overallScore}`, ...renderPlain(data.checks)];
|
|
144
|
+
output(format, JSON.stringify(final, null, 2));
|
|
145
|
+
} else if (format === 'plain') {
|
|
146
|
+
const lines = [`SCORE\t${final.overallScore}`, ...renderPlain(final.checks)];
|
|
80
147
|
output(format, lines.join('\n'));
|
|
81
|
-
|
|
82
|
-
|
|
148
|
+
} else {
|
|
149
|
+
output(format, renderTable(final));
|
|
83
150
|
}
|
|
84
151
|
|
|
85
|
-
const scoreColor =
|
|
86
|
-
data.overallScore >= 70 ? 'green' : data.overallScore >= 40 ? 'yellow' : 'red';
|
|
87
|
-
const lines: string[] = [];
|
|
88
|
-
lines.push(chalk.bold(`\nAgent Readiness Score — ${data.url}\n`));
|
|
89
|
-
lines.push(` Score: ${chalk[scoreColor].bold(`${data.overallScore}%`)}\n`);
|
|
90
|
-
lines.push(chalk.bold(' Checks'));
|
|
91
|
-
lines.push(...renderChecks(data.checks));
|
|
92
|
-
output(format, lines.join('\n'));
|
|
93
152
|
await terminate(0);
|
|
94
153
|
} catch (err) {
|
|
95
154
|
const message = err instanceof Error ? err.message : 'unknown error';
|
package/src/score/types.ts
CHANGED
|
@@ -5,8 +5,22 @@ export type Check = {
|
|
|
5
5
|
children?: Check[];
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
export type ResolveStatus = 'ready' | 'stale_refresh_queued' | 'queued';
|
|
9
|
+
|
|
10
|
+
export type ResolveResult = {
|
|
11
|
+
canonicalUrl: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
status: ResolveStatus;
|
|
14
|
+
trackedSiteId: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
8
17
|
export type ScoreResponse = {
|
|
9
|
-
|
|
10
|
-
overallScore: number;
|
|
18
|
+
canonicalUrl: string;
|
|
11
19
|
checks: Check[];
|
|
20
|
+
computedAt: string;
|
|
21
|
+
name: string;
|
|
22
|
+
overallScore: number;
|
|
23
|
+
passedChecks: number;
|
|
24
|
+
slug: string;
|
|
25
|
+
totalChecks: number;
|
|
12
26
|
};
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import * as previewing from '@mintlify/previewing';
|
|
2
|
-
import fse from 'fs-extra';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
import { addWorkflow, slugify, buildFrontmatter, isValidCron } from '../src/workflow.js';
|
|
6
|
-
|
|
7
|
-
const FAKE_PROJECT = '/fake/project';
|
|
8
|
-
const WORKFLOWS_DIR = path.join(FAKE_PROJECT, '.mintlify', 'workflows');
|
|
9
|
-
|
|
10
|
-
vi.mock('@inquirer/prompts', () => ({
|
|
11
|
-
select: vi.fn(),
|
|
12
|
-
input: vi.fn(),
|
|
13
|
-
editor: vi.fn(),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
vi.mock('@mintlify/previewing', () => ({
|
|
17
|
-
addLog: vi.fn(),
|
|
18
|
-
addLogs: vi.fn(),
|
|
19
|
-
SuccessLog: vi.fn(),
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
vi.mock('fs-extra', () => ({
|
|
23
|
-
default: {
|
|
24
|
-
pathExists: vi.fn(),
|
|
25
|
-
ensureDir: vi.fn().mockResolvedValue(undefined),
|
|
26
|
-
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
27
|
-
},
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
vi.mock('../src/helpers.js', () => ({
|
|
31
|
-
CMD_EXEC_PATH: '/fake/project',
|
|
32
|
-
isAI: () =>
|
|
33
|
-
!process.stdin.isTTY || process.env.CLAUDECODE === '1' || process.env.TERM_PROGRAM === 'claude',
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
const addLogSpy = vi.mocked(previewing.addLog);
|
|
37
|
-
|
|
38
|
-
describe('slugify', () => {
|
|
39
|
-
it('converts spaces to hyphens', () => {
|
|
40
|
-
expect(slugify('Update changelog')).toBe('update-changelog');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('replaces special characters with hyphens', () => {
|
|
44
|
-
expect(slugify('My Workflow!@#$%')).toBe('my-workflow');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('replaces slashes with hyphens like the dashboard', () => {
|
|
48
|
-
expect(slugify('a/b')).toBe('a-b');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('collapses multiple hyphens', () => {
|
|
52
|
-
expect(slugify('a b---c')).toBe('a-b-c');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('trims leading and trailing hyphens', () => {
|
|
56
|
-
expect(slugify(' --hello-- ')).toBe('hello');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('lowercases the result', () => {
|
|
60
|
-
expect(slugify('UPPER CASE')).toBe('upper-case');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('handles single word', () => {
|
|
64
|
-
expect(slugify('deploy')).toBe('deploy');
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('isValidCron', () => {
|
|
69
|
-
it('accepts standard 5-field expressions', () => {
|
|
70
|
-
expect(isValidCron('0 9 * * 1')).toBe(true);
|
|
71
|
-
expect(isValidCron('*/15 * * * *')).toBe(true);
|
|
72
|
-
expect(isValidCron('0 0 1 1 *')).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('accepts ranges and lists', () => {
|
|
76
|
-
expect(isValidCron('0 9-17 * * 1-5')).toBe(true);
|
|
77
|
-
expect(isValidCron('0,30 * * * *')).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('rejects invalid expressions', () => {
|
|
81
|
-
expect(isValidCron('0 xd f gasf')).toBe(false);
|
|
82
|
-
expect(isValidCron('not a cron')).toBe(false);
|
|
83
|
-
expect(isValidCron('')).toBe(false);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('rejects wrong number of fields', () => {
|
|
87
|
-
expect(isValidCron('* * *')).toBe(false);
|
|
88
|
-
expect(isValidCron('* * * * * *')).toBe(false);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('buildFrontmatter', () => {
|
|
93
|
-
it('builds cron trigger frontmatter', () => {
|
|
94
|
-
const result = buildFrontmatter({
|
|
95
|
-
name: 'Update changelog',
|
|
96
|
-
triggerType: 'cron',
|
|
97
|
-
cronExpression: '0 9 * * 1',
|
|
98
|
-
automerge: false,
|
|
99
|
-
});
|
|
100
|
-
expect(result).toBe('---\nname: "Update changelog"\non:\n cron: "0 9 * * 1"\n---');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('builds push trigger frontmatter with repos', () => {
|
|
104
|
-
const result = buildFrontmatter({
|
|
105
|
-
name: 'Deploy docs',
|
|
106
|
-
triggerType: 'push',
|
|
107
|
-
triggerRepos: ['org/docs', 'org/api'],
|
|
108
|
-
automerge: false,
|
|
109
|
-
});
|
|
110
|
-
expect(result).toBe(
|
|
111
|
-
'---\nname: "Deploy docs"\non:\n push:\n - repo: "org/docs"\n - repo: "org/api"\n---'
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('builds push trigger with no repos', () => {
|
|
116
|
-
const result = buildFrontmatter({
|
|
117
|
-
name: 'Deploy',
|
|
118
|
-
triggerType: 'push',
|
|
119
|
-
automerge: false,
|
|
120
|
-
});
|
|
121
|
-
expect(result).toBe('---\nname: "Deploy"\non:\n push:\n---');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('includes context repos', () => {
|
|
125
|
-
const result = buildFrontmatter({
|
|
126
|
-
name: 'Test',
|
|
127
|
-
triggerType: 'cron',
|
|
128
|
-
cronExpression: '0 9 * * 1',
|
|
129
|
-
contextRepos: ['org/repo1', 'org/repo2'],
|
|
130
|
-
automerge: false,
|
|
131
|
-
});
|
|
132
|
-
expect(result).toContain('context:\n - repo: "org/repo1"\n - repo: "org/repo2"');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('includes automerge only when true', () => {
|
|
136
|
-
const result = buildFrontmatter({
|
|
137
|
-
name: 'Test',
|
|
138
|
-
triggerType: 'cron',
|
|
139
|
-
cronExpression: '0 9 * * 1',
|
|
140
|
-
automerge: true,
|
|
141
|
-
});
|
|
142
|
-
expect(result).toContain('automerge: true');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('omits automerge when false', () => {
|
|
146
|
-
const result = buildFrontmatter({
|
|
147
|
-
name: 'Test',
|
|
148
|
-
triggerType: 'cron',
|
|
149
|
-
cronExpression: '0 9 * * 1',
|
|
150
|
-
automerge: false,
|
|
151
|
-
});
|
|
152
|
-
expect(result).not.toContain('automerge');
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('escapes quotes in name', () => {
|
|
156
|
-
const result = buildFrontmatter({
|
|
157
|
-
name: 'My "Test" Workflow',
|
|
158
|
-
triggerType: 'cron',
|
|
159
|
-
cronExpression: '0 9 * * 1',
|
|
160
|
-
automerge: false,
|
|
161
|
-
});
|
|
162
|
-
expect(result).toContain('name: "My \\"Test\\" Workflow"');
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe('addWorkflow', () => {
|
|
167
|
-
beforeEach(() => {
|
|
168
|
-
vi.clearAllMocks();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('throws when docs.json does not exist', async () => {
|
|
172
|
-
vi.mocked(fse.pathExists).mockResolvedValue(false as never);
|
|
173
|
-
|
|
174
|
-
await expect(addWorkflow()).rejects.toThrow(
|
|
175
|
-
'docs.json not found in the current directory. Please run this command from your docs repository root.'
|
|
176
|
-
);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('outputs AI usage message when not interactive', async () => {
|
|
180
|
-
vi.mocked(fse.pathExists).mockResolvedValue(true as never);
|
|
181
|
-
const originalIsTTY = process.stdin.isTTY;
|
|
182
|
-
process.stdin.isTTY = false;
|
|
183
|
-
|
|
184
|
-
await addWorkflow();
|
|
185
|
-
|
|
186
|
-
expect(previewing.addLogs).toHaveBeenCalled();
|
|
187
|
-
|
|
188
|
-
process.stdin.isTTY = originalIsTTY;
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('throws when workflow name has no alphanumeric characters', async () => {
|
|
192
|
-
vi.mocked(fse.pathExists).mockResolvedValue(true as never);
|
|
193
|
-
const originalIsTTY = process.stdin.isTTY;
|
|
194
|
-
const originalClaudeCode = process.env.CLAUDECODE;
|
|
195
|
-
process.stdin.isTTY = true;
|
|
196
|
-
delete process.env.CLAUDECODE;
|
|
197
|
-
|
|
198
|
-
const { input } = await import('@inquirer/prompts');
|
|
199
|
-
vi.mocked(input).mockResolvedValueOnce('!!!');
|
|
200
|
-
|
|
201
|
-
await expect(addWorkflow()).rejects.toThrow(
|
|
202
|
-
'Workflow name must contain at least one alphanumeric character.'
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
process.stdin.isTTY = originalIsTTY;
|
|
206
|
-
if (originalClaudeCode === undefined) {
|
|
207
|
-
delete process.env.CLAUDECODE;
|
|
208
|
-
} else {
|
|
209
|
-
process.env.CLAUDECODE = originalClaudeCode;
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('throws when workflow file already exists', async () => {
|
|
214
|
-
// pathExists returns true for both docs.json and the workflow file
|
|
215
|
-
vi.mocked(fse.pathExists).mockResolvedValue(true as never);
|
|
216
|
-
const originalIsTTY = process.stdin.isTTY;
|
|
217
|
-
const originalClaudeCode = process.env.CLAUDECODE;
|
|
218
|
-
process.stdin.isTTY = true;
|
|
219
|
-
delete process.env.CLAUDECODE;
|
|
220
|
-
|
|
221
|
-
const { input, select, editor } = await import('@inquirer/prompts');
|
|
222
|
-
vi.mocked(input)
|
|
223
|
-
.mockResolvedValueOnce('My Workflow') // workflow name
|
|
224
|
-
.mockResolvedValueOnce('0 9 * * 1') // cron expression
|
|
225
|
-
.mockResolvedValueOnce(''); // context repos
|
|
226
|
-
vi.mocked(select)
|
|
227
|
-
.mockResolvedValueOnce('cron') // trigger type
|
|
228
|
-
.mockResolvedValueOnce('no'); // automerge
|
|
229
|
-
vi.mocked(editor).mockResolvedValueOnce('Do the thing');
|
|
230
|
-
|
|
231
|
-
const expectedRelative = path.join('.mintlify', 'workflows', 'my-workflow.md');
|
|
232
|
-
await expect(addWorkflow()).rejects.toThrow(
|
|
233
|
-
`A workflow already exists at ${expectedRelative}. Please choose a different name or delete the existing file.`
|
|
234
|
-
);
|
|
235
|
-
expect(fse.writeFile).not.toHaveBeenCalled();
|
|
236
|
-
|
|
237
|
-
process.stdin.isTTY = originalIsTTY;
|
|
238
|
-
if (originalClaudeCode === undefined) {
|
|
239
|
-
delete process.env.CLAUDECODE;
|
|
240
|
-
} else {
|
|
241
|
-
process.env.CLAUDECODE = originalClaudeCode;
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('creates cron workflow file with correct content', async () => {
|
|
246
|
-
// true for docs.json, false for workflow file existence check
|
|
247
|
-
vi.mocked(fse.pathExists)
|
|
248
|
-
.mockResolvedValueOnce(true as never)
|
|
249
|
-
.mockResolvedValueOnce(false as never);
|
|
250
|
-
const originalIsTTY = process.stdin.isTTY;
|
|
251
|
-
const originalClaudeCode = process.env.CLAUDECODE;
|
|
252
|
-
process.stdin.isTTY = true;
|
|
253
|
-
delete process.env.CLAUDECODE;
|
|
254
|
-
|
|
255
|
-
const { input, select, editor } = await import('@inquirer/prompts');
|
|
256
|
-
vi.mocked(input)
|
|
257
|
-
.mockResolvedValueOnce('My Test Workflow') // workflow name
|
|
258
|
-
.mockResolvedValueOnce('0 9 * * 1') // cron expression
|
|
259
|
-
.mockResolvedValueOnce(''); // context repos
|
|
260
|
-
vi.mocked(select)
|
|
261
|
-
.mockResolvedValueOnce('cron') // trigger type
|
|
262
|
-
.mockResolvedValueOnce('no'); // automerge
|
|
263
|
-
vi.mocked(editor).mockResolvedValueOnce('Do the thing');
|
|
264
|
-
|
|
265
|
-
await addWorkflow();
|
|
266
|
-
|
|
267
|
-
expect(fse.ensureDir).toHaveBeenCalledWith(WORKFLOWS_DIR);
|
|
268
|
-
expect(fse.writeFile).toHaveBeenCalledWith(
|
|
269
|
-
path.join(WORKFLOWS_DIR, 'my-test-workflow.md'),
|
|
270
|
-
'---\nname: "My Test Workflow"\non:\n cron: "0 9 * * 1"\n---\n\nDo the thing\n'
|
|
271
|
-
);
|
|
272
|
-
const expectedRelative = path.join('.mintlify', 'workflows', 'my-test-workflow.md');
|
|
273
|
-
expect(addLogSpy).toHaveBeenCalledWith(
|
|
274
|
-
expect.objectContaining({
|
|
275
|
-
props: { message: `Workflow created at ${expectedRelative}` },
|
|
276
|
-
})
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
process.stdin.isTTY = originalIsTTY;
|
|
280
|
-
if (originalClaudeCode === undefined) {
|
|
281
|
-
delete process.env.CLAUDECODE;
|
|
282
|
-
} else {
|
|
283
|
-
process.env.CLAUDECODE = originalClaudeCode;
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('creates push trigger workflow with automerge and context', async () => {
|
|
288
|
-
vi.mocked(fse.pathExists)
|
|
289
|
-
.mockResolvedValueOnce(true as never)
|
|
290
|
-
.mockResolvedValueOnce(false as never);
|
|
291
|
-
const originalIsTTY = process.stdin.isTTY;
|
|
292
|
-
const originalClaudeCode = process.env.CLAUDECODE;
|
|
293
|
-
process.stdin.isTTY = true;
|
|
294
|
-
delete process.env.CLAUDECODE;
|
|
295
|
-
|
|
296
|
-
const { input, select, editor } = await import('@inquirer/prompts');
|
|
297
|
-
vi.mocked(input)
|
|
298
|
-
.mockResolvedValueOnce('Deploy Docs') // workflow name
|
|
299
|
-
.mockResolvedValueOnce('org/docs') // trigger repos
|
|
300
|
-
.mockResolvedValueOnce('org/server'); // context repos
|
|
301
|
-
vi.mocked(select)
|
|
302
|
-
.mockResolvedValueOnce('push') // trigger type
|
|
303
|
-
.mockResolvedValueOnce('yes'); // automerge
|
|
304
|
-
vi.mocked(editor).mockResolvedValueOnce('Deploy the docs');
|
|
305
|
-
|
|
306
|
-
await addWorkflow();
|
|
307
|
-
|
|
308
|
-
expect(fse.writeFile).toHaveBeenCalledWith(
|
|
309
|
-
path.join(WORKFLOWS_DIR, 'deploy-docs.md'),
|
|
310
|
-
'---\nname: "Deploy Docs"\non:\n push:\n - repo: "org/docs"\ncontext:\n - repo: "org/server"\nautomerge: true\n---\n\nDeploy the docs\n'
|
|
311
|
-
);
|
|
312
|
-
|
|
313
|
-
process.stdin.isTTY = originalIsTTY;
|
|
314
|
-
if (originalClaudeCode === undefined) {
|
|
315
|
-
delete process.env.CLAUDECODE;
|
|
316
|
-
} else {
|
|
317
|
-
process.env.CLAUDECODE = originalClaudeCode;
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
});
|