@mintlify/cli 4.0.1122 → 4.0.1123

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mintlify/cli",
3
- "version": "4.0.1122",
3
+ "version": "4.0.1123",
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": "794b8b7473bf08e20dea96efe72921c44a52b194"
96
+ "gitHead": "511d45e72c42590140bd4568456f75bc65658752"
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>',
@@ -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
- });
package/bin/workflow.js DELETED
@@ -1,150 +0,0 @@
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 { jsx as _jsx } from "react/jsx-runtime";
11
- import { select, input, editor } from '@inquirer/prompts';
12
- import { addLog, addLogs, SuccessLog } from '@mintlify/previewing';
13
- import fse from 'fs-extra';
14
- import { Text } from 'ink';
15
- import path from 'path';
16
- import { CMD_EXEC_PATH, isAI } from './helpers.js';
17
- export function slugify(name) {
18
- return name
19
- .toLowerCase()
20
- .replace(/[^a-z0-9]+/g, '-')
21
- .replace(/^-|-$/g, '');
22
- }
23
- const CRON_FIELD = /^(\*|(\*\/\d+)|(\d+(-\d+)?(,\d+(-\d+)?)*))$/;
24
- export function isValidCron(expr) {
25
- const fields = expr.trim().split(/\s+/);
26
- if (fields.length !== 5)
27
- return false;
28
- return fields.every((f) => CRON_FIELD.test(f));
29
- }
30
- function escapeYaml(value) {
31
- return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
32
- }
33
- export function buildFrontmatter(config) {
34
- var _a;
35
- const lines = [];
36
- lines.push(`name: "${escapeYaml(config.name)}"`);
37
- if (config.triggerType === 'push') {
38
- lines.push('on:');
39
- lines.push(' push:');
40
- if (config.triggerRepos) {
41
- for (const repo of config.triggerRepos) {
42
- lines.push(` - repo: "${escapeYaml(repo)}"`);
43
- }
44
- }
45
- }
46
- else {
47
- lines.push('on:');
48
- lines.push(` cron: "${escapeYaml((_a = config.cronExpression) !== null && _a !== void 0 ? _a : '')}"`);
49
- }
50
- if (config.contextRepos && config.contextRepos.length > 0) {
51
- lines.push('context:');
52
- for (const repo of config.contextRepos) {
53
- lines.push(` - repo: "${escapeYaml(repo)}"`);
54
- }
55
- }
56
- if (config.automerge) {
57
- lines.push('automerge: true');
58
- }
59
- return `---\n${lines.join('\n')}\n---`;
60
- }
61
- const sendUsageMessageForAI = () => {
62
- addLogs(_jsx(Text, { children: "Agent Detected - sending AI friendly prompt" }), _jsx(Text, { children: '<system-message>' }), _jsx(Text, { children: "Example usage of `mintlify workflow`. This command is interactive and creates a workflow file in `.mintlify/workflows/`." }), _jsx(Text, { children: "Workflow files are Markdown files with YAML frontmatter. Instead of running this command, you can directly create a `.mintlify/workflows/your-workflow.md` file." }), _jsx(Text, { children: "Frontmatter fields: name (string), on.cron (string) or on.push with repo list, context (array of objects with repo key), automerge (boolean, only when true)." }), _jsx(Text, { children: "The Markdown body contains the agent instructions/prompt." }), _jsx(Text, { children: '</system-message>' }));
63
- };
64
- export function addWorkflow() {
65
- return __awaiter(this, void 0, void 0, function* () {
66
- const docsJsonPath = path.join(CMD_EXEC_PATH, 'docs.json');
67
- if (!(yield fse.pathExists(docsJsonPath))) {
68
- throw new Error('docs.json not found in the current directory. Please run this command from your docs repository root.');
69
- }
70
- if (isAI()) {
71
- sendUsageMessageForAI();
72
- return;
73
- }
74
- const workflowName = yield input({
75
- message: 'Workflow name',
76
- default: 'Update changelog',
77
- });
78
- if (!workflowName.trim()) {
79
- throw new Error('Workflow name cannot be empty.');
80
- }
81
- const slug = slugify(workflowName);
82
- if (!slug) {
83
- throw new Error('Workflow name must contain at least one alphanumeric character.');
84
- }
85
- const triggerType = yield select({
86
- message: 'Trigger type',
87
- choices: [
88
- { name: 'Cron (scheduled)', value: 'cron' },
89
- { name: 'Push (on push to repo)', value: 'push' },
90
- ],
91
- });
92
- let cronExpression;
93
- let triggerRepos = [];
94
- if (triggerType === 'cron') {
95
- cronExpression = yield input({
96
- message: 'Cron expression',
97
- default: '0 9 * * 1',
98
- validate: (value) => isValidCron(value) ||
99
- 'Invalid cron expression. Expected 5 fields: minute hour day-of-month month day-of-week (e.g. 0 9 * * 1).',
100
- });
101
- }
102
- else {
103
- const triggerReposInput = yield input({
104
- message: 'Trigger repos (comma-separated, e.g. your-org/your-docs)',
105
- default: '',
106
- });
107
- triggerRepos = triggerReposInput
108
- .split(',')
109
- .map((r) => r.trim())
110
- .filter(Boolean);
111
- }
112
- const contextReposInput = yield input({
113
- message: 'Context repos (comma-separated, e.g. your-org/your-product, optional)',
114
- default: '',
115
- });
116
- const contextRepos = contextReposInput
117
- .split(',')
118
- .map((r) => r.trim())
119
- .filter(Boolean);
120
- const automergeChoice = yield select({
121
- message: 'Enable automerge?',
122
- choices: [
123
- { name: 'Yes', value: 'yes' },
124
- { name: 'No', value: 'no' },
125
- ],
126
- });
127
- const instructions = yield editor({
128
- message: 'Agent instructions (opens your default editor)',
129
- default: '# Agent Instructions\n\nDescribe what this workflow should do.\n\nFor example:\n- Update the changelog based on recent commits\n- Summarize recent PRs\n',
130
- });
131
- const frontmatter = buildFrontmatter({
132
- name: workflowName,
133
- triggerType,
134
- cronExpression,
135
- triggerRepos,
136
- contextRepos: contextRepos.length > 0 ? contextRepos : undefined,
137
- automerge: automergeChoice === 'yes',
138
- });
139
- const filename = slug + '.md';
140
- const workflowDir = path.join(CMD_EXEC_PATH, '.mintlify', 'workflows');
141
- const filePath = path.join(workflowDir, filename);
142
- const relativePath = path.relative(CMD_EXEC_PATH, filePath);
143
- yield fse.ensureDir(workflowDir);
144
- if (yield fse.pathExists(filePath)) {
145
- throw new Error(`A workflow already exists at ${relativePath}. Please choose a different name or delete the existing file.`);
146
- }
147
- yield fse.writeFile(filePath, frontmatter + '\n\n' + instructions.trim() + '\n');
148
- addLog(_jsx(SuccessLog, { message: `Workflow created at ${relativePath}` }));
149
- });
150
- }