@mintlify/cli 4.0.1762 → 4.0.1764

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 (49) hide show
  1. package/README.md +18 -0
  2. package/__test__/agent-setup-config.test.ts +314 -0
  3. package/__test__/agent-setup.test.ts +207 -0
  4. package/__test__/migrate.test.ts +186 -0
  5. package/__test__/validateRules.test.ts +36 -0
  6. package/bin/agent-setup-config.js +141 -0
  7. package/bin/agent-setup.js +146 -0
  8. package/bin/cli.js +65 -0
  9. package/bin/migrate.js +54 -0
  10. package/bin/tsconfig.build.tsbuildinfo +1 -1
  11. package/bin/validate/files.js +16 -4
  12. package/bin/validate/mdx.js +1 -1
  13. package/bin/welcome.js +1 -0
  14. package/migrate/index.md +56 -0
  15. package/migrate/sources/confluence.md +120 -0
  16. package/migrate/sources/docsify.md +128 -0
  17. package/migrate/sources/document360.md +117 -0
  18. package/migrate/sources/docus.md +136 -0
  19. package/migrate/sources/docusaurus.md +143 -0
  20. package/migrate/sources/fern.md +130 -0
  21. package/migrate/sources/fumadocs.md +141 -0
  22. package/migrate/sources/gitbook.md +120 -0
  23. package/migrate/sources/github-wiki.md +90 -0
  24. package/migrate/sources/jekyll.md +104 -0
  25. package/migrate/sources/mdbook.md +98 -0
  26. package/migrate/sources/mkdocs.md +113 -0
  27. package/migrate/sources/nextra.md +144 -0
  28. package/migrate/sources/notion.md +99 -0
  29. package/migrate/sources/nuxt.md +113 -0
  30. package/migrate/sources/pdf.md +99 -0
  31. package/migrate/sources/readme.md +118 -0
  32. package/migrate/sources/redocly.md +114 -0
  33. package/migrate/sources/starlight.md +136 -0
  34. package/migrate/sources/vitepress.md +143 -0
  35. package/migrate/sources/vuepress.md +153 -0
  36. package/migrate/topics/api.md +76 -0
  37. package/migrate/topics/changelog.md +47 -0
  38. package/migrate/topics/conversion.md +194 -0
  39. package/migrate/topics/discovery.md +93 -0
  40. package/migrate/topics/navigation.md +82 -0
  41. package/migrate/topics/verify.md +58 -0
  42. package/package.json +12 -10
  43. package/src/agent-setup-config.ts +144 -0
  44. package/src/agent-setup.ts +165 -0
  45. package/src/cli.tsx +91 -0
  46. package/src/migrate.ts +76 -0
  47. package/src/validate/files.ts +23 -3
  48. package/src/validate/mdx.ts +1 -1
  49. package/src/welcome.ts +1 -0
@@ -0,0 +1,186 @@
1
+ import { isBuiltInComponent } from '@mintlify/common';
2
+ import { addLog } from '@mintlify/previewing';
3
+ import { spawn, type ChildProcess } from 'node:child_process';
4
+ import { EventEmitter } from 'node:events';
5
+ import { readdirSync, readFileSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { mockProcessExit } from 'vitest-mock-process';
8
+
9
+ import { windowsCompatibleSpawnOptions } from '../src/helpers.js';
10
+ import {
11
+ agentArgs,
12
+ agents,
13
+ listMigrationDocs,
14
+ migrateWithAgent,
15
+ migrationInstructions,
16
+ } from '../src/migrate.js';
17
+ import { runCommand } from './utils.js';
18
+
19
+ vi.mock('node:child_process', async (importOriginal) => ({
20
+ ...(await importOriginal<typeof import('node:child_process')>()),
21
+ spawn: vi.fn(),
22
+ }));
23
+ vi.mock('@mintlify/previewing', async (importOriginal) => ({
24
+ ...(await importOriginal<typeof import('@mintlify/previewing')>()),
25
+ addLog: vi.fn(),
26
+ }));
27
+
28
+ const spawnMock = vi.mocked(spawn);
29
+ const child = (emit: (child: EventEmitter) => void) => {
30
+ spawnMock.mockImplementationOnce(() => {
31
+ const proc = new EventEmitter();
32
+ setImmediate(() => emit(proc));
33
+ return proc as ChildProcess;
34
+ });
35
+ };
36
+ const enoent = (proc: EventEmitter) =>
37
+ proc.emit('error', Object.assign(new Error('spawn ENOENT'), { code: 'ENOENT' }));
38
+
39
+ const root = path.join(__dirname, '../migrate');
40
+ const names = (section: string) =>
41
+ readdirSync(path.join(root, section))
42
+ .filter((file) => file.endsWith('.md'))
43
+ .map((file) => file.slice(0, -3));
44
+ const read = (...segments: string[]) => readFileSync(path.join(root, ...segments), 'utf8');
45
+ const docs = [
46
+ 'index.md',
47
+ ...names('topics').map((name) => `topics/${name}.md`),
48
+ ...names('sources').map((name) => `sources/${name}.md`),
49
+ ];
50
+ const prose = (markdown: string) => markdown.replace(/^(`{3,}).*\n[\s\S]*?^\1\s*$/gm, '');
51
+
52
+ const template = [
53
+ '## Detect',
54
+ '## Get the source',
55
+ '### From the repo',
56
+ '### From an export',
57
+ '### From the live site',
58
+ '## Config → docs.json',
59
+ '## Navigation',
60
+ '## Content conversion',
61
+ '## Assets',
62
+ '## URLs and redirects',
63
+ '## Known gaps',
64
+ ];
65
+
66
+ const components = (text: string) => [...text.matchAll(/<([A-Z][\w.]+)/g)].map(([, name]) => name!);
67
+ const targets = (markdown: string) =>
68
+ prose(markdown)
69
+ .split('\n')
70
+ .flatMap((line) =>
71
+ line.startsWith('|')
72
+ ? [line.split('|').slice(1)]
73
+ : line.split(/[;.] /).map((clause) => clause.split('→'))
74
+ )
75
+ .flatMap(([source = '', ...rest]) =>
76
+ components(rest.join(' ')).filter((name) => !components(source).includes(name))
77
+ );
78
+
79
+ const exit = mockProcessExit();
80
+
81
+ describe('migrate', () => {
82
+ it('lists topics and sources from disk', () => {
83
+ expect(listMigrationDocs('topics')).toEqual(names('topics').sort());
84
+ expect(listMigrationDocs('sources')).toEqual(names('sources').sort());
85
+ expect(listMigrationDocs('sources')).toContain('nuxt');
86
+ });
87
+
88
+ it('prints the overview with every topic and source', () => {
89
+ const output = migrationInstructions({});
90
+ expect(output.startsWith(read('index.md').trimEnd())).toBe(true);
91
+ expect(output).toContain(`Topics: ${names('topics').sort().join(', ')}.`);
92
+ expect(output).toContain(`Sources: ${names('sources').sort().join(', ')}.`);
93
+ });
94
+
95
+ it('appends the source guide to the overview', () => {
96
+ const output = migrationInstructions({ from: 'nuxt' });
97
+ expect(output.startsWith(read('index.md').trimEnd())).toBe(true);
98
+ expect(output.endsWith(read('sources', 'nuxt.md'))).toBe(true);
99
+ });
100
+
101
+ it('prints only the topic', () => {
102
+ expect(migrationInstructions({ topic: 'verify' })).toBe(read('topics', 'verify.md'));
103
+ });
104
+
105
+ it.each(names('sources'))('source %s follows the template', (name) => {
106
+ const headings = prose(read('sources', `${name}.md`)).match(/^#{1,3} .*/gm) ?? [];
107
+ expect(headings[0]).toMatch(/^# \S/);
108
+ expect(headings.slice(1)).toEqual(template);
109
+ });
110
+
111
+ it.each(docs)('%s references only existing topics and sources', (doc) => {
112
+ for (const [, flag, name] of read(doc).matchAll(/--(topic|from) ([a-z][\w-]*)/g)) {
113
+ expect(names(flag === 'topic' ? 'topics' : 'sources'), `${doc}: --${flag} ${name}`).toContain(
114
+ name
115
+ );
116
+ }
117
+ });
118
+
119
+ it.each(docs)('%s maps only to Mintlify components', (doc) => {
120
+ expect(targets(read(doc)).filter((name) => !isBuiltInComponent(name))).toEqual([]);
121
+ });
122
+
123
+ it('rejects unknown values with the valid choices', async () => {
124
+ const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
125
+ await runCommand('migrate', '--from', 'nope').catch(() => undefined);
126
+ const output = stderr.mock.calls.map(([chunk]) => String(chunk)).join('');
127
+ expect(output).toContain('nope');
128
+ expect(output).toContain('nuxt');
129
+ stderr.mockRestore();
130
+ });
131
+
132
+ it('offers every agent as an --agent choice', async () => {
133
+ const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
134
+ await runCommand('migrate', '--agent', 'nope').catch(() => undefined);
135
+ const output = stderr.mock.calls.map(([chunk]) => String(chunk)).join('');
136
+ expect(output).toContain(
137
+ `Choices: ${Object.keys(agents)
138
+ .map((name) => `"${name}"`)
139
+ .join(', ')}`
140
+ );
141
+ stderr.mockRestore();
142
+ });
143
+
144
+ const base = 'Migrate a documentation site to Mintlify in the current directory. Run';
145
+ const plain = `${base} \`mint migrate\` and follow the instructions it prints.`;
146
+ const nuxt = `${base} \`mint migrate --from nuxt\` and follow the instructions it prints.`;
147
+
148
+ it.each([
149
+ ['claude', [plain], [nuxt]],
150
+ ['codex', [plain], [nuxt]],
151
+ ['cursor', [plain], [nuxt]],
152
+ ['gemini', ['-i', plain], ['-i', nuxt]],
153
+ ] as const)('%s builds its argv', (name, without, withFrom) => {
154
+ expect(agentArgs(name)).toEqual(without);
155
+ expect(agentArgs(name, 'nuxt')).toEqual(withFrom);
156
+ });
157
+
158
+ it('--claude runs claude', async () => {
159
+ child((proc) => proc.emit('close', 0));
160
+ await runCommand('migrate', '--claude', '--from', 'nuxt');
161
+ expect(spawnMock).toHaveBeenCalledWith(
162
+ 'claude',
163
+ [windowsCompatibleSpawnOptions.shell ? `"${nuxt}"` : nuxt],
164
+ expect.objectContaining({ stdio: 'inherit' })
165
+ );
166
+ expect(exit).toHaveBeenCalledWith(0);
167
+ });
168
+
169
+ it('rejects a missing binary with its install command', async () => {
170
+ child(enoent);
171
+ await expect(migrateWithAgent('gemini')).rejects.toThrow(
172
+ `gemini not found. Install it with: ${agents.gemini.install}`
173
+ );
174
+ });
175
+
176
+ it('prints the install command and exits nonzero for a missing binary', async () => {
177
+ child(enoent);
178
+ await runCommand('migrate', '--agent', 'cursor');
179
+ expect(vi.mocked(addLog)).toHaveBeenCalledWith(
180
+ expect.objectContaining({
181
+ props: { message: `cursor-agent not found. Install it with: ${agents.cursor.install}` },
182
+ })
183
+ );
184
+ expect(exit).toHaveBeenCalledWith(1);
185
+ });
186
+ });
@@ -838,6 +838,42 @@ describe('validate rules', () => {
838
838
  expect(hasErrors(result.issues)).toBe(true);
839
839
  });
840
840
 
841
+ it('suggests escaping a < that cannot start a tag', async () => {
842
+ write(dir, {
843
+ 'index.mdx': '---\ntitle: Home\n---\n\nLatency is <5ms.\n',
844
+ 'end.mdx': '---\ntitle: End\n---\n\nThe last one <\n',
845
+ });
846
+
847
+ const { issues } = await check(dir, {});
848
+
849
+ expect(issues.map(({ file, line, column, message }) => [file, line, column, message])).toEqual([
850
+ [
851
+ 'end.mdx',
852
+ 5,
853
+ 15,
854
+ "Unexpected end of file before name, expected a character that can start a name, such as a letter, `$`, or `_`; if this < isn't a tag, escape it as \\< (other checks were skipped for this file)",
855
+ ],
856
+ [
857
+ 'index.mdx',
858
+ 5,
859
+ 13,
860
+ "Unexpected character `5` (U+0035) before name, expected a character that can start a name, such as a letter, `$`, or `_`; if this < isn't a tag, escape it as \\< (other checks were skipped for this file)",
861
+ ],
862
+ ]);
863
+ });
864
+
865
+ it('does not suggest escaping < for other parse errors', async () => {
866
+ write(dir, {
867
+ 'index.mdx': '---\ntitle: Home\n---\n\nReturns a List<String>.\n',
868
+ 'math.mdx': '---\ntitle: Math\n---\n\nTotal {1 <}\n',
869
+ });
870
+
871
+ const { issues } = await check(dir, {});
872
+
873
+ expect(issues.map(({ rule }) => rule)).toEqual(['parse-error', 'parse-error']);
874
+ for (const { message } of issues) expect(message).not.toContain('escape it');
875
+ });
876
+
841
877
  it('still runs parse-error when --rules picks only other rules', async () => {
842
878
  write(dir, { 'index.mdx': '---\ntitle: Home\n---\n\n<Note>\nunclosed\n' });
843
879
 
@@ -0,0 +1,141 @@
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 { applyEdits, modify, parse } from 'jsonc-parser';
11
+ import { randomUUID } from 'node:crypto';
12
+ import fs from 'node:fs/promises';
13
+ import path from 'node:path';
14
+ import { parse as parseToml } from 'smol-toml';
15
+ export const ADMIN_MCP_NAME = 'mintlify-admin';
16
+ export const ADMIN_MCP_URL = 'https://mcp.mintlify.com';
17
+ function isRecord(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ }
20
+ function readConfig(file) {
21
+ return __awaiter(this, void 0, void 0, function* () {
22
+ try {
23
+ return yield fs.readFile(file, 'utf8');
24
+ }
25
+ catch (error) {
26
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
27
+ return undefined;
28
+ throw error;
29
+ }
30
+ });
31
+ }
32
+ function configPath(agent, options) {
33
+ var _a;
34
+ const env = (_a = options.env) !== null && _a !== void 0 ? _a : process.env;
35
+ if (agent === 'codex') {
36
+ let directory = path.join(options.cwd, '.codex');
37
+ if (options.global)
38
+ directory = env.CODEX_HOME || path.join(options.homeDir, '.codex');
39
+ return path.join(directory, 'config.toml');
40
+ }
41
+ if (agent === 'cursor') {
42
+ const directory = options.global ? options.homeDir : options.cwd;
43
+ return path.join(directory, '.cursor', 'mcp.json');
44
+ }
45
+ if (options.global) {
46
+ if (env.CLAUDE_CONFIG_DIR) {
47
+ throw new Error('CLAUDE_CONFIG_DIR is set. Configure Claude Code with: claude mcp add --scope user --transport http mintlify-admin https://mcp.mintlify.com');
48
+ }
49
+ return path.join(options.homeDir, '.claude.json');
50
+ }
51
+ return path.join(options.cwd, '.mcp.json');
52
+ }
53
+ export function prepareMcpConfig(agent, options) {
54
+ return __awaiter(this, void 0, void 0, function* () {
55
+ const requestedPath = configPath(agent, options);
56
+ const file = yield fs.realpath(requestedPath).catch((error) => {
57
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
58
+ return requestedPath;
59
+ throw error;
60
+ });
61
+ const original = yield readConfig(file);
62
+ const source = original !== null && original !== void 0 ? original : '';
63
+ const key = agent === 'codex' ? 'mcp_servers' : 'mcpServers';
64
+ let config;
65
+ try {
66
+ if (agent === 'codex') {
67
+ config = parseToml(source);
68
+ }
69
+ else {
70
+ const errors = [];
71
+ config = parse(source || '{}', errors, { allowTrailingComma: true });
72
+ if (errors.length > 0)
73
+ throw new Error('Invalid JSON');
74
+ }
75
+ }
76
+ catch (_a) {
77
+ throw new Error(`Cannot parse ${file}. Fix its configuration before running setup.`);
78
+ }
79
+ if (!isRecord(config))
80
+ throw new Error(`Expected an object in ${file}.`);
81
+ const servers = config[key];
82
+ if (servers !== undefined && !isRecord(servers)) {
83
+ throw new Error(`Expected ${key} to be an object in ${file}.`);
84
+ }
85
+ const existing = servers === null || servers === void 0 ? void 0 : servers[ADMIN_MCP_NAME];
86
+ if (existing !== undefined) {
87
+ if (!isRecord(existing) || existing.url !== ADMIN_MCP_URL || existing.command !== undefined) {
88
+ throw new Error(`${ADMIN_MCP_NAME} already has a different configuration in ${file}. Rename or remove that entry before running setup.`);
89
+ }
90
+ if (agent === 'claude-code' && existing.type !== 'http') {
91
+ throw new Error(`Set type to "http" for ${ADMIN_MCP_NAME} in ${file} before running setup.`);
92
+ }
93
+ return { path: file, original, contents: undefined };
94
+ }
95
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
96
+ let contents;
97
+ if (agent === 'codex') {
98
+ contents = `${source}${newline}[mcp_servers.${ADMIN_MCP_NAME}]${newline}url = "${ADMIN_MCP_URL}"${newline}`;
99
+ try {
100
+ parseToml(contents);
101
+ }
102
+ catch (_b) {
103
+ throw new Error(`Cannot safely extend ${file}. Add [mcp_servers.${ADMIN_MCP_NAME}] with url = "${ADMIN_MCP_URL}" manually.`);
104
+ }
105
+ }
106
+ else {
107
+ const server = { url: ADMIN_MCP_URL };
108
+ if (agent === 'claude-code')
109
+ server.type = 'http';
110
+ contents = applyEdits(source || '{}', modify(source || '{}', [key, ADMIN_MCP_NAME], server, {
111
+ formattingOptions: { insertSpaces: true, tabSize: 2, eol: newline },
112
+ }));
113
+ if (!contents.endsWith(newline))
114
+ contents += newline;
115
+ }
116
+ return { path: file, original, contents };
117
+ });
118
+ }
119
+ export function writeMcpConfig(plan) {
120
+ return __awaiter(this, void 0, void 0, function* () {
121
+ if ((yield readConfig(plan.path)) !== plan.original) {
122
+ throw new Error(`${plan.path} changed during setup. Run setup again to preserve those changes.`);
123
+ }
124
+ if (plan.contents === undefined)
125
+ return;
126
+ yield fs.mkdir(path.dirname(plan.path), { recursive: true, mode: 0o700 });
127
+ let mode = 0o600;
128
+ if (plan.original !== undefined)
129
+ mode = (yield fs.stat(plan.path)).mode;
130
+ const temporaryPath = `${plan.path}.${randomUUID()}.tmp`;
131
+ try {
132
+ yield fs.writeFile(temporaryPath, plan.contents, { mode, flag: 'wx' });
133
+ if (plan.original !== undefined)
134
+ yield fs.chmod(temporaryPath, mode);
135
+ yield fs.rename(temporaryPath, plan.path);
136
+ }
137
+ finally {
138
+ yield fs.rm(temporaryPath, { force: true });
139
+ }
140
+ });
141
+ }
@@ -0,0 +1,146 @@
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 { checkbox } from '@inquirer/prompts';
11
+ import { spawn } from 'node:child_process';
12
+ import { existsSync } from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { stripVTControlCharacters } from 'node:util';
16
+ import { ADMIN_MCP_NAME, ADMIN_MCP_URL, prepareMcpConfig, writeMcpConfig, } from './agent-setup-config.js';
17
+ import { detectAgent } from './agent.js';
18
+ export const SETUP_AGENTS = ['claude-code', 'codex', 'cursor'];
19
+ const AGENT_LABELS = {
20
+ 'claude-code': 'Claude Code',
21
+ codex: 'Codex',
22
+ cursor: 'Cursor',
23
+ };
24
+ function isSetupAgent(value) {
25
+ return SETUP_AGENTS.some((agent) => agent === value);
26
+ }
27
+ function detectedAgents() {
28
+ const homeDir = os.homedir();
29
+ const current = detectAgent();
30
+ return SETUP_AGENTS.filter((agent) => {
31
+ if (agent === 'codex') {
32
+ return (current === 'codex_cli' ||
33
+ existsSync(process.env.CODEX_HOME || path.join(homeDir, '.codex')));
34
+ }
35
+ if (agent === 'claude-code') {
36
+ return (current === 'claude_code' ||
37
+ existsSync(process.env.CLAUDE_CONFIG_DIR || path.join(homeDir, '.claude')));
38
+ }
39
+ return (current === 'cursor' || current === 'cursor-cli' || existsSync(path.join(homeDir, '.cursor')));
40
+ });
41
+ }
42
+ export function installSkills(agents, global) {
43
+ const args = [
44
+ '--yes',
45
+ 'skills@1.7.4',
46
+ 'add',
47
+ 'https://mintlify.com/docs',
48
+ '--agent',
49
+ ...agents,
50
+ '--yes',
51
+ ];
52
+ if (global)
53
+ args.push('--global');
54
+ return new Promise((resolve, reject) => {
55
+ const child = spawn('npx', args, {
56
+ stdio: ['ignore', 'pipe', 'pipe'],
57
+ shell: process.platform === 'win32',
58
+ });
59
+ let output = '';
60
+ const recordOutput = (chunk) => {
61
+ output = (output + chunk.toString()).slice(-65536);
62
+ };
63
+ child.stdout.on('data', (chunk) => {
64
+ recordOutput(chunk);
65
+ process.stdout.write(chunk.toString());
66
+ });
67
+ child.stderr.on('data', (chunk) => {
68
+ recordOutput(chunk);
69
+ process.stderr.write(chunk.toString());
70
+ });
71
+ child.on('error', () => reject(new Error('Could not run npx. Install npm and run setup again.')));
72
+ child.on('close', (code, signal) => {
73
+ if (code !== 0 || /Failed to install\s+\d+/.test(stripVTControlCharacters(output))) {
74
+ reject(new Error(`Skills installation failed (${signal !== null && signal !== void 0 ? signal : code}). Review the installer output and run setup again.`));
75
+ }
76
+ else {
77
+ resolve();
78
+ }
79
+ });
80
+ });
81
+ }
82
+ export function setupAgent(options) {
83
+ return __awaiter(this, void 0, void 0, function* () {
84
+ var _a;
85
+ const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
86
+ if (major < 22 || (major === 22 && minor < 20)) {
87
+ throw new Error('mint agent setup requires Node.js 22.20 or later to install skills. Upgrade Node.js and try again.');
88
+ }
89
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
90
+ let agents = [];
91
+ for (const agent of (_a = options.agents) !== null && _a !== void 0 ? _a : []) {
92
+ if (!isSetupAgent(agent))
93
+ throw new Error(`Unsupported agent: ${agent}`);
94
+ if (!agents.includes(agent))
95
+ agents.push(agent);
96
+ }
97
+ if (agents.length === 0) {
98
+ const detected = detectedAgents();
99
+ if (!interactive) {
100
+ agents = detected;
101
+ }
102
+ else {
103
+ agents = yield checkbox({
104
+ message: 'Which agents should use Mintlify?',
105
+ choices: SETUP_AGENTS.map((agent) => ({
106
+ name: AGENT_LABELS[agent],
107
+ value: agent,
108
+ checked: detected.includes(agent),
109
+ })),
110
+ required: true,
111
+ });
112
+ }
113
+ }
114
+ if (agents.length === 0) {
115
+ throw new Error('No supported agents detected. Pass --agent claude-code, codex, or cursor.');
116
+ }
117
+ const location = { cwd: process.cwd(), homeDir: os.homedir(), global: options.global };
118
+ const plans = yield Promise.all(agents.map((agent) => prepareMcpConfig(agent, location)));
119
+ const scope = options.global ? 'your user account' : 'this project';
120
+ process.stdout.write(`Setting up Mintlify skills and Admin MCP for ${scope}.\n`);
121
+ yield installSkills(agents, options.global);
122
+ for (const plan of plans) {
123
+ yield writeMcpConfig(plan);
124
+ const action = plan.contents === undefined ? 'Already configured' : 'Configured';
125
+ process.stdout.write(`${action}: ${plan.path}\n`);
126
+ }
127
+ process.stdout.write('\nAdmin MCP is configured. Complete OAuth sign-in to grant account and project access:\n');
128
+ for (const agent of agents) {
129
+ if (agent === 'codex') {
130
+ let command = `codex mcp login ${ADMIN_MCP_NAME}`;
131
+ if (!options.global) {
132
+ const config = `mcp_servers.${ADMIN_MCP_NAME}.url='${ADMIN_MCP_URL}'`;
133
+ command = `codex -c "${config}" mcp login ${ADMIN_MCP_NAME}`;
134
+ }
135
+ process.stdout.write(` Codex: run ${command}. Trust the project before using its MCP configuration.\n`);
136
+ }
137
+ else if (agent === 'claude-code') {
138
+ process.stdout.write(` Claude Code: open /mcp and authenticate ${ADMIN_MCP_NAME}. Approve the project server if prompted.\n`);
139
+ }
140
+ else {
141
+ process.stdout.write(` Cursor: open MCP settings and connect ${ADMIN_MCP_NAME}.\n`);
142
+ }
143
+ }
144
+ process.stdout.write('Setup: https://mintlify.com/docs/ai/mintlify-mcp\n');
145
+ });
146
+ }
package/bin/cli.js CHANGED
@@ -16,6 +16,7 @@ import { hideBin } from 'yargs/helpers';
16
16
  import { accessibilityCheck } from './accessibilityCheck.js';
17
17
  import { addDomain } from './addDomain.js';
18
18
  import { claimDeployment, createAgentClaimLink } from './agent-claim.js';
19
+ import { SETUP_AGENTS, setupAgent } from './agent-setup.js';
19
20
  import { analyticsBuilder } from './analytics/index.js';
20
21
  import { brokenLinksHandler } from './brokenLinks.js';
21
22
  import { comingSoon } from './comingSoon.js';
@@ -34,6 +35,7 @@ import { login } from './login.js';
34
35
  import { logout } from './logout.js';
35
36
  import { mdxLinter } from './mdxLinter.js';
36
37
  import { createTelemetryMiddleware } from './middlewares/telemetryMiddleware.js';
38
+ import { agents, listMigrationDocs, migrateWithAgent, migrationInstructions, } from './migrate.js';
37
39
  import { ogCommand } from './og.js';
38
40
  import { buildOverlayRegistryForCheck, checkOpenApiFile } from './openApiCheck.js';
39
41
  import { scoreHandler } from './score/index.js';
@@ -100,6 +102,35 @@ export const cli = ({ packageName = 'mint' }) => {
100
102
  .middleware(checkNodeVersion)
101
103
  .middleware(suppressConsoleWarnings)
102
104
  .middleware(telemetryMiddleware)
105
+ .command('agent', 'Set up AI coding agents for Mintlify', (yargs) => yargs
106
+ .command('setup', 'Install Mintlify skills and configure Admin MCP', (yargs) => yargs
107
+ .exitProcess(false)
108
+ .option('agent', {
109
+ alias: 'a',
110
+ type: 'string',
111
+ array: true,
112
+ choices: SETUP_AGENTS,
113
+ description: 'Agents to configure (defaults to interactive selection)',
114
+ })
115
+ .option('global', {
116
+ alias: 'g',
117
+ type: 'boolean',
118
+ default: false,
119
+ description: 'Set up agents for your user account instead of this project',
120
+ })
121
+ .example('$0 agent setup', 'Choose agents and set up the current project')
122
+ .example('$0 agent setup --agent codex --global', 'Set up Codex for your user account')
123
+ .epilog('Requires Node.js 22.20 or later and npm. Admin MCP requires a separate OAuth sign-in in each agent.'), (argv) => __awaiter(void 0, void 0, void 0, function* () {
124
+ try {
125
+ yield setupAgent({ agents: argv.agent, global: argv.global });
126
+ yield terminate(0);
127
+ }
128
+ catch (error) {
129
+ process.stderr.write(`${error instanceof Error ? error.message : 'Agent setup failed.'}\n`);
130
+ yield terminate(1);
131
+ }
132
+ }))
133
+ .demandCommand(1, 'Run mint agent setup to configure your coding agents.'))
103
134
  .command('claim', 'Claim a site created with mint deploy', (yargs) => yargs
104
135
  .option('email', { type: 'string', description: 'User email for a claim link' })
105
136
  .option('code', {
@@ -653,6 +684,40 @@ export const cli = ({ packageName = 'mint' }) => {
653
684
  .command('format', 'Format MDX files in the current directory', (yargs) => yargs
654
685
  .usage('usage: mint format')
655
686
  .example('mint format', 'format all MDX files in the current directory'), formatHandler)
687
+ .command('migrate', 'Print instructions for migrating a docs site to Mintlify', (yargs) => yargs
688
+ .option('from', {
689
+ type: 'string',
690
+ choices: listMigrationDocs('sources'),
691
+ description: 'Include instructions for a source platform',
692
+ })
693
+ .option('topic', {
694
+ type: 'string',
695
+ choices: listMigrationDocs('topics'),
696
+ description: 'Print a single topic',
697
+ })
698
+ .option('agent', {
699
+ type: 'string',
700
+ choices: Object.keys(agents),
701
+ description: 'Run the migration with a coding agent',
702
+ })
703
+ .option('claude', {
704
+ type: 'boolean',
705
+ description: 'Alias for --agent claude',
706
+ }), (argv) => __awaiter(void 0, void 0, void 0, function* () {
707
+ var _a;
708
+ const agent = (_a = argv.agent) !== null && _a !== void 0 ? _a : (argv.claude ? 'claude' : undefined);
709
+ if (agent) {
710
+ try {
711
+ return yield terminate(yield migrateWithAgent(agent, argv.from));
712
+ }
713
+ catch (error) {
714
+ addLog(_jsx(ErrorLog, { message: error instanceof Error ? error.message : String(error) }));
715
+ return yield terminate(1);
716
+ }
717
+ }
718
+ yield new Promise((resolve) => process.stdout.write(migrationInstructions(argv), () => resolve()));
719
+ yield terminate(0);
720
+ }))
656
721
  // Coming soon commands — visible in help, tracked via telemetry to gauge interest.
657
722
  .command('ai', '[Coming soon] AI-powered documentation (run mint ai to vote)', () => undefined, comingSoon('ai', packageName))
658
723
  .command('mcp', '[Coming soon] MCP server for documentation (run mint mcp to vote)', () => undefined, comingSoon('mcp', packageName))
package/bin/migrate.js ADDED
@@ -0,0 +1,54 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readdirSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { windowsCompatibleSpawnOptions } from './helpers.js';
6
+ const root = fileURLToPath(new URL('../migrate', import.meta.url));
7
+ const read = (...segments) => readFileSync(path.join(root, ...segments), 'utf8');
8
+ export const listMigrationDocs = (section) => readdirSync(path.join(root, section))
9
+ .filter((file) => file.endsWith('.md'))
10
+ .map((file) => file.slice(0, -3))
11
+ .sort();
12
+ const overview = () => [
13
+ read('index.md').trimEnd(),
14
+ `Topics: ${listMigrationDocs('topics').join(', ')}. Run \`mint migrate --topic <name>\`.`,
15
+ `Sources: ${listMigrationDocs('sources').join(', ')}. Run \`mint migrate --from <name>\`.`,
16
+ ].join('\n\n');
17
+ export const migrationInstructions = ({ from, topic }) => {
18
+ if (topic)
19
+ return read('topics', `${topic}.md`);
20
+ if (from)
21
+ return `${overview()}\n\n${read('sources', `${from}.md`)}`;
22
+ return `${overview()}\n`;
23
+ };
24
+ const prompt = (from) => `Migrate a documentation site to Mintlify in the current directory. Run \`mint migrate${from ? ` --from ${from}` : ''}\` and follow the instructions it prints.`;
25
+ export const agents = {
26
+ claude: {
27
+ bin: 'claude',
28
+ args: (prompt) => [prompt],
29
+ install: 'npm install -g @anthropic-ai/claude-code',
30
+ },
31
+ codex: {
32
+ bin: 'codex',
33
+ args: (prompt) => [prompt],
34
+ install: 'npm install -g @openai/codex',
35
+ },
36
+ cursor: {
37
+ bin: 'cursor-agent',
38
+ args: (prompt) => [prompt],
39
+ install: 'curl https://cursor.com/install -fsS | bash',
40
+ },
41
+ gemini: {
42
+ bin: 'gemini',
43
+ args: (prompt) => ['-i', prompt],
44
+ install: 'npm install -g @google/gemini-cli',
45
+ },
46
+ };
47
+ export const agentArgs = (name, from) => agents[name].args(prompt(from));
48
+ export const migrateWithAgent = (name, from) => new Promise((resolve, reject) => {
49
+ const { bin, install } = agents[name];
50
+ const args = agentArgs(name, from).map((arg) => windowsCompatibleSpawnOptions.shell ? `"${arg}"` : arg);
51
+ const child = spawn(bin, args, Object.assign({ stdio: 'inherit' }, windowsCompatibleSpawnOptions));
52
+ child.on('error', (error) => reject(error.code === 'ENOENT' ? new Error(`${bin} not found. Install it with: ${install}`) : error));
53
+ child.on('close', (code) => resolve(code !== null && code !== void 0 ? code : 1));
54
+ });