@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.
- package/README.md +18 -0
- package/__test__/agent-setup-config.test.ts +314 -0
- package/__test__/agent-setup.test.ts +207 -0
- package/__test__/migrate.test.ts +186 -0
- package/__test__/validateRules.test.ts +36 -0
- package/bin/agent-setup-config.js +141 -0
- package/bin/agent-setup.js +146 -0
- package/bin/cli.js +65 -0
- package/bin/migrate.js +54 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/bin/validate/files.js +16 -4
- package/bin/validate/mdx.js +1 -1
- package/bin/welcome.js +1 -0
- package/migrate/index.md +56 -0
- package/migrate/sources/confluence.md +120 -0
- package/migrate/sources/docsify.md +128 -0
- package/migrate/sources/document360.md +117 -0
- package/migrate/sources/docus.md +136 -0
- package/migrate/sources/docusaurus.md +143 -0
- package/migrate/sources/fern.md +130 -0
- package/migrate/sources/fumadocs.md +141 -0
- package/migrate/sources/gitbook.md +120 -0
- package/migrate/sources/github-wiki.md +90 -0
- package/migrate/sources/jekyll.md +104 -0
- package/migrate/sources/mdbook.md +98 -0
- package/migrate/sources/mkdocs.md +113 -0
- package/migrate/sources/nextra.md +144 -0
- package/migrate/sources/notion.md +99 -0
- package/migrate/sources/nuxt.md +113 -0
- package/migrate/sources/pdf.md +99 -0
- package/migrate/sources/readme.md +118 -0
- package/migrate/sources/redocly.md +114 -0
- package/migrate/sources/starlight.md +136 -0
- package/migrate/sources/vitepress.md +143 -0
- package/migrate/sources/vuepress.md +153 -0
- package/migrate/topics/api.md +76 -0
- package/migrate/topics/changelog.md +47 -0
- package/migrate/topics/conversion.md +194 -0
- package/migrate/topics/discovery.md +93 -0
- package/migrate/topics/navigation.md +82 -0
- package/migrate/topics/verify.md +58 -0
- package/package.json +12 -10
- package/src/agent-setup-config.ts +144 -0
- package/src/agent-setup.ts +165 -0
- package/src/cli.tsx +91 -0
- package/src/migrate.ts +76 -0
- package/src/validate/files.ts +23 -3
- package/src/validate/mdx.ts +1 -1
- package/src/welcome.ts +1 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { applyEdits, modify, parse, type ParseError } from 'jsonc-parser';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parse as parseToml } from 'smol-toml';
|
|
6
|
+
|
|
7
|
+
export type SetupAgent = 'claude-code' | 'codex' | 'cursor';
|
|
8
|
+
|
|
9
|
+
export const ADMIN_MCP_NAME = 'mintlify-admin';
|
|
10
|
+
export const ADMIN_MCP_URL = 'https://mcp.mintlify.com';
|
|
11
|
+
|
|
12
|
+
type ConfigLocation = {
|
|
13
|
+
cwd: string;
|
|
14
|
+
homeDir: string;
|
|
15
|
+
global: boolean;
|
|
16
|
+
env?: NodeJS.ProcessEnv;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type McpConfigPlan = {
|
|
20
|
+
path: string;
|
|
21
|
+
original: string | undefined;
|
|
22
|
+
contents: string | undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readConfig(file: string): Promise<string | undefined> {
|
|
30
|
+
try {
|
|
31
|
+
return await fs.readFile(file, 'utf8');
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return undefined;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function configPath(agent: SetupAgent, options: ConfigLocation): string {
|
|
39
|
+
const env = options.env ?? process.env;
|
|
40
|
+
if (agent === 'codex') {
|
|
41
|
+
let directory = path.join(options.cwd, '.codex');
|
|
42
|
+
if (options.global) directory = env.CODEX_HOME || path.join(options.homeDir, '.codex');
|
|
43
|
+
return path.join(directory, 'config.toml');
|
|
44
|
+
}
|
|
45
|
+
if (agent === 'cursor') {
|
|
46
|
+
const directory = options.global ? options.homeDir : options.cwd;
|
|
47
|
+
return path.join(directory, '.cursor', 'mcp.json');
|
|
48
|
+
}
|
|
49
|
+
if (options.global) {
|
|
50
|
+
if (env.CLAUDE_CONFIG_DIR) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'CLAUDE_CONFIG_DIR is set. Configure Claude Code with: claude mcp add --scope user --transport http mintlify-admin https://mcp.mintlify.com'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return path.join(options.homeDir, '.claude.json');
|
|
56
|
+
}
|
|
57
|
+
return path.join(options.cwd, '.mcp.json');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function prepareMcpConfig(
|
|
61
|
+
agent: SetupAgent,
|
|
62
|
+
options: ConfigLocation
|
|
63
|
+
): Promise<McpConfigPlan> {
|
|
64
|
+
const requestedPath = configPath(agent, options);
|
|
65
|
+
const file = await fs.realpath(requestedPath).catch((error: unknown) => {
|
|
66
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return requestedPath;
|
|
67
|
+
throw error;
|
|
68
|
+
});
|
|
69
|
+
const original = await readConfig(file);
|
|
70
|
+
const source = original ?? '';
|
|
71
|
+
const key = agent === 'codex' ? 'mcp_servers' : 'mcpServers';
|
|
72
|
+
let config: unknown;
|
|
73
|
+
try {
|
|
74
|
+
if (agent === 'codex') {
|
|
75
|
+
config = parseToml(source);
|
|
76
|
+
} else {
|
|
77
|
+
const errors: ParseError[] = [];
|
|
78
|
+
config = parse(source || '{}', errors, { allowTrailingComma: true });
|
|
79
|
+
if (errors.length > 0) throw new Error('Invalid JSON');
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error(`Cannot parse ${file}. Fix its configuration before running setup.`);
|
|
83
|
+
}
|
|
84
|
+
if (!isRecord(config)) throw new Error(`Expected an object in ${file}.`);
|
|
85
|
+
const servers = config[key];
|
|
86
|
+
if (servers !== undefined && !isRecord(servers)) {
|
|
87
|
+
throw new Error(`Expected ${key} to be an object in ${file}.`);
|
|
88
|
+
}
|
|
89
|
+
const existing = servers?.[ADMIN_MCP_NAME];
|
|
90
|
+
if (existing !== undefined) {
|
|
91
|
+
if (!isRecord(existing) || existing.url !== ADMIN_MCP_URL || existing.command !== undefined) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`${ADMIN_MCP_NAME} already has a different configuration in ${file}. Rename or remove that entry before running setup.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (agent === 'claude-code' && existing.type !== 'http') {
|
|
97
|
+
throw new Error(`Set type to "http" for ${ADMIN_MCP_NAME} in ${file} before running setup.`);
|
|
98
|
+
}
|
|
99
|
+
return { path: file, original, contents: undefined };
|
|
100
|
+
}
|
|
101
|
+
const newline = source.includes('\r\n') ? '\r\n' : '\n';
|
|
102
|
+
let contents: string;
|
|
103
|
+
if (agent === 'codex') {
|
|
104
|
+
contents = `${source}${newline}[mcp_servers.${ADMIN_MCP_NAME}]${newline}url = "${ADMIN_MCP_URL}"${newline}`;
|
|
105
|
+
try {
|
|
106
|
+
parseToml(contents);
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Cannot safely extend ${file}. Add [mcp_servers.${ADMIN_MCP_NAME}] with url = "${ADMIN_MCP_URL}" manually.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
const server: Record<string, string> = { url: ADMIN_MCP_URL };
|
|
114
|
+
if (agent === 'claude-code') server.type = 'http';
|
|
115
|
+
contents = applyEdits(
|
|
116
|
+
source || '{}',
|
|
117
|
+
modify(source || '{}', [key, ADMIN_MCP_NAME], server, {
|
|
118
|
+
formattingOptions: { insertSpaces: true, tabSize: 2, eol: newline },
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
if (!contents.endsWith(newline)) contents += newline;
|
|
122
|
+
}
|
|
123
|
+
return { path: file, original, contents };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function writeMcpConfig(plan: McpConfigPlan): Promise<void> {
|
|
127
|
+
if ((await readConfig(plan.path)) !== plan.original) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`${plan.path} changed during setup. Run setup again to preserve those changes.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (plan.contents === undefined) return;
|
|
133
|
+
await fs.mkdir(path.dirname(plan.path), { recursive: true, mode: 0o700 });
|
|
134
|
+
let mode = 0o600;
|
|
135
|
+
if (plan.original !== undefined) mode = (await fs.stat(plan.path)).mode;
|
|
136
|
+
const temporaryPath = `${plan.path}.${randomUUID()}.tmp`;
|
|
137
|
+
try {
|
|
138
|
+
await fs.writeFile(temporaryPath, plan.contents, { mode, flag: 'wx' });
|
|
139
|
+
if (plan.original !== undefined) await fs.chmod(temporaryPath, mode);
|
|
140
|
+
await fs.rename(temporaryPath, plan.path);
|
|
141
|
+
} finally {
|
|
142
|
+
await fs.rm(temporaryPath, { force: true });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { checkbox } from '@inquirer/prompts';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ADMIN_MCP_NAME,
|
|
10
|
+
ADMIN_MCP_URL,
|
|
11
|
+
prepareMcpConfig,
|
|
12
|
+
writeMcpConfig,
|
|
13
|
+
type SetupAgent,
|
|
14
|
+
} from './agent-setup-config.js';
|
|
15
|
+
import { detectAgent } from './agent.js';
|
|
16
|
+
|
|
17
|
+
export const SETUP_AGENTS: SetupAgent[] = ['claude-code', 'codex', 'cursor'];
|
|
18
|
+
|
|
19
|
+
const AGENT_LABELS: Record<SetupAgent, string> = {
|
|
20
|
+
'claude-code': 'Claude Code',
|
|
21
|
+
codex: 'Codex',
|
|
22
|
+
cursor: 'Cursor',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type SetupOptions = {
|
|
26
|
+
agents?: string[];
|
|
27
|
+
global: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function isSetupAgent(value: string): value is SetupAgent {
|
|
31
|
+
return SETUP_AGENTS.some((agent) => agent === value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function detectedAgents(): SetupAgent[] {
|
|
35
|
+
const homeDir = os.homedir();
|
|
36
|
+
const current = detectAgent();
|
|
37
|
+
return SETUP_AGENTS.filter((agent) => {
|
|
38
|
+
if (agent === 'codex') {
|
|
39
|
+
return (
|
|
40
|
+
current === 'codex_cli' ||
|
|
41
|
+
existsSync(process.env.CODEX_HOME || path.join(homeDir, '.codex'))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (agent === 'claude-code') {
|
|
45
|
+
return (
|
|
46
|
+
current === 'claude_code' ||
|
|
47
|
+
existsSync(process.env.CLAUDE_CONFIG_DIR || path.join(homeDir, '.claude'))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return (
|
|
51
|
+
current === 'cursor' || current === 'cursor-cli' || existsSync(path.join(homeDir, '.cursor'))
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function installSkills(agents: SetupAgent[], global: boolean): Promise<void> {
|
|
57
|
+
const args = [
|
|
58
|
+
'--yes',
|
|
59
|
+
'skills@1.7.4',
|
|
60
|
+
'add',
|
|
61
|
+
'https://mintlify.com/docs',
|
|
62
|
+
'--agent',
|
|
63
|
+
...agents,
|
|
64
|
+
'--yes',
|
|
65
|
+
];
|
|
66
|
+
if (global) args.push('--global');
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const child = spawn('npx', args, {
|
|
69
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
70
|
+
shell: process.platform === 'win32',
|
|
71
|
+
});
|
|
72
|
+
let output = '';
|
|
73
|
+
const recordOutput = (chunk: Buffer) => {
|
|
74
|
+
output = (output + chunk.toString()).slice(-65_536);
|
|
75
|
+
};
|
|
76
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
77
|
+
recordOutput(chunk);
|
|
78
|
+
process.stdout.write(chunk.toString());
|
|
79
|
+
});
|
|
80
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
81
|
+
recordOutput(chunk);
|
|
82
|
+
process.stderr.write(chunk.toString());
|
|
83
|
+
});
|
|
84
|
+
child.on('error', () =>
|
|
85
|
+
reject(new Error('Could not run npx. Install npm and run setup again.'))
|
|
86
|
+
);
|
|
87
|
+
child.on('close', (code, signal) => {
|
|
88
|
+
if (code !== 0 || /Failed to install\s+\d+/.test(stripVTControlCharacters(output))) {
|
|
89
|
+
reject(
|
|
90
|
+
new Error(
|
|
91
|
+
`Skills installation failed (${signal ?? code}). Review the installer output and run setup again.`
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
resolve();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function setupAgent(options: SetupOptions): Promise<void> {
|
|
102
|
+
const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
|
|
103
|
+
if (major < 22 || (major === 22 && minor < 20)) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
'mint agent setup requires Node.js 22.20 or later to install skills. Upgrade Node.js and try again.'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
109
|
+
let agents: SetupAgent[] = [];
|
|
110
|
+
for (const agent of options.agents ?? []) {
|
|
111
|
+
if (!isSetupAgent(agent)) throw new Error(`Unsupported agent: ${agent}`);
|
|
112
|
+
if (!agents.includes(agent)) agents.push(agent);
|
|
113
|
+
}
|
|
114
|
+
if (agents.length === 0) {
|
|
115
|
+
const detected = detectedAgents();
|
|
116
|
+
if (!interactive) {
|
|
117
|
+
agents = detected;
|
|
118
|
+
} else {
|
|
119
|
+
agents = await checkbox({
|
|
120
|
+
message: 'Which agents should use Mintlify?',
|
|
121
|
+
choices: SETUP_AGENTS.map((agent) => ({
|
|
122
|
+
name: AGENT_LABELS[agent],
|
|
123
|
+
value: agent,
|
|
124
|
+
checked: detected.includes(agent),
|
|
125
|
+
})),
|
|
126
|
+
required: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (agents.length === 0) {
|
|
131
|
+
throw new Error('No supported agents detected. Pass --agent claude-code, codex, or cursor.');
|
|
132
|
+
}
|
|
133
|
+
const location = { cwd: process.cwd(), homeDir: os.homedir(), global: options.global };
|
|
134
|
+
const plans = await Promise.all(agents.map((agent) => prepareMcpConfig(agent, location)));
|
|
135
|
+
const scope = options.global ? 'your user account' : 'this project';
|
|
136
|
+
process.stdout.write(`Setting up Mintlify skills and Admin MCP for ${scope}.\n`);
|
|
137
|
+
await installSkills(agents, options.global);
|
|
138
|
+
for (const plan of plans) {
|
|
139
|
+
await writeMcpConfig(plan);
|
|
140
|
+
const action = plan.contents === undefined ? 'Already configured' : 'Configured';
|
|
141
|
+
process.stdout.write(`${action}: ${plan.path}\n`);
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write(
|
|
144
|
+
'\nAdmin MCP is configured. Complete OAuth sign-in to grant account and project access:\n'
|
|
145
|
+
);
|
|
146
|
+
for (const agent of agents) {
|
|
147
|
+
if (agent === 'codex') {
|
|
148
|
+
let command = `codex mcp login ${ADMIN_MCP_NAME}`;
|
|
149
|
+
if (!options.global) {
|
|
150
|
+
const config = `mcp_servers.${ADMIN_MCP_NAME}.url='${ADMIN_MCP_URL}'`;
|
|
151
|
+
command = `codex -c "${config}" mcp login ${ADMIN_MCP_NAME}`;
|
|
152
|
+
}
|
|
153
|
+
process.stdout.write(
|
|
154
|
+
` Codex: run ${command}. Trust the project before using its MCP configuration.\n`
|
|
155
|
+
);
|
|
156
|
+
} else if (agent === 'claude-code') {
|
|
157
|
+
process.stdout.write(
|
|
158
|
+
` Claude Code: open /mcp and authenticate ${ADMIN_MCP_NAME}. Approve the project server if prompted.\n`
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
process.stdout.write(` Cursor: open MCP settings and connect ${ADMIN_MCP_NAME}.\n`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
process.stdout.write('Setup: https://mintlify.com/docs/ai/mintlify-mcp\n');
|
|
165
|
+
}
|
package/src/cli.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { hideBin } from 'yargs/helpers';
|
|
|
15
15
|
import { accessibilityCheck } from './accessibilityCheck.js';
|
|
16
16
|
import { addDomain } from './addDomain.js';
|
|
17
17
|
import { claimDeployment, createAgentClaimLink } from './agent-claim.js';
|
|
18
|
+
import { SETUP_AGENTS, setupAgent } from './agent-setup.js';
|
|
18
19
|
import { analyticsBuilder } from './analytics/index.js';
|
|
19
20
|
import { brokenLinksHandler } from './brokenLinks.js';
|
|
20
21
|
import { comingSoon } from './comingSoon.js';
|
|
@@ -41,6 +42,13 @@ import { login } from './login.js';
|
|
|
41
42
|
import { logout } from './logout.js';
|
|
42
43
|
import { mdxLinter } from './mdxLinter.js';
|
|
43
44
|
import { createTelemetryMiddleware } from './middlewares/telemetryMiddleware.js';
|
|
45
|
+
import {
|
|
46
|
+
agents,
|
|
47
|
+
listMigrationDocs,
|
|
48
|
+
migrateWithAgent,
|
|
49
|
+
migrationInstructions,
|
|
50
|
+
type AgentName,
|
|
51
|
+
} from './migrate.js';
|
|
44
52
|
import { ogCommand } from './og.js';
|
|
45
53
|
import { buildOverlayRegistryForCheck, checkOpenApiFile } from './openApiCheck.js';
|
|
46
54
|
import { scoreHandler } from './score/index.js';
|
|
@@ -113,6 +121,49 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
|
|
|
113
121
|
.middleware(checkNodeVersion)
|
|
114
122
|
.middleware(suppressConsoleWarnings)
|
|
115
123
|
.middleware(telemetryMiddleware)
|
|
124
|
+
.command('agent', 'Set up AI coding agents for Mintlify', (yargs) =>
|
|
125
|
+
yargs
|
|
126
|
+
.command(
|
|
127
|
+
'setup',
|
|
128
|
+
'Install Mintlify skills and configure Admin MCP',
|
|
129
|
+
(yargs) =>
|
|
130
|
+
yargs
|
|
131
|
+
.exitProcess(false)
|
|
132
|
+
.option('agent', {
|
|
133
|
+
alias: 'a',
|
|
134
|
+
type: 'string',
|
|
135
|
+
array: true,
|
|
136
|
+
choices: SETUP_AGENTS,
|
|
137
|
+
description: 'Agents to configure (defaults to interactive selection)',
|
|
138
|
+
})
|
|
139
|
+
.option('global', {
|
|
140
|
+
alias: 'g',
|
|
141
|
+
type: 'boolean',
|
|
142
|
+
default: false,
|
|
143
|
+
description: 'Set up agents for your user account instead of this project',
|
|
144
|
+
})
|
|
145
|
+
.example('$0 agent setup', 'Choose agents and set up the current project')
|
|
146
|
+
.example(
|
|
147
|
+
'$0 agent setup --agent codex --global',
|
|
148
|
+
'Set up Codex for your user account'
|
|
149
|
+
)
|
|
150
|
+
.epilog(
|
|
151
|
+
'Requires Node.js 22.20 or later and npm. Admin MCP requires a separate OAuth sign-in in each agent.'
|
|
152
|
+
),
|
|
153
|
+
async (argv) => {
|
|
154
|
+
try {
|
|
155
|
+
await setupAgent({ agents: argv.agent, global: argv.global });
|
|
156
|
+
await terminate(0);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
process.stderr.write(
|
|
159
|
+
`${error instanceof Error ? error.message : 'Agent setup failed.'}\n`
|
|
160
|
+
);
|
|
161
|
+
await terminate(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
.demandCommand(1, 'Run mint agent setup to configure your coding agents.')
|
|
166
|
+
)
|
|
116
167
|
.command(
|
|
117
168
|
'claim',
|
|
118
169
|
'Claim a site created with mint deploy',
|
|
@@ -886,6 +937,46 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
|
|
|
886
937
|
.example('mint format', 'format all MDX files in the current directory'),
|
|
887
938
|
formatHandler
|
|
888
939
|
)
|
|
940
|
+
.command(
|
|
941
|
+
'migrate',
|
|
942
|
+
'Print instructions for migrating a docs site to Mintlify',
|
|
943
|
+
(yargs: Argv) =>
|
|
944
|
+
yargs
|
|
945
|
+
.option('from', {
|
|
946
|
+
type: 'string',
|
|
947
|
+
choices: listMigrationDocs('sources'),
|
|
948
|
+
description: 'Include instructions for a source platform',
|
|
949
|
+
})
|
|
950
|
+
.option('topic', {
|
|
951
|
+
type: 'string',
|
|
952
|
+
choices: listMigrationDocs('topics'),
|
|
953
|
+
description: 'Print a single topic',
|
|
954
|
+
})
|
|
955
|
+
.option('agent', {
|
|
956
|
+
type: 'string',
|
|
957
|
+
choices: Object.keys(agents) as AgentName[],
|
|
958
|
+
description: 'Run the migration with a coding agent',
|
|
959
|
+
})
|
|
960
|
+
.option('claude', {
|
|
961
|
+
type: 'boolean',
|
|
962
|
+
description: 'Alias for --agent claude',
|
|
963
|
+
}),
|
|
964
|
+
async (argv) => {
|
|
965
|
+
const agent = argv.agent ?? (argv.claude ? 'claude' : undefined);
|
|
966
|
+
if (agent) {
|
|
967
|
+
try {
|
|
968
|
+
return await terminate(await migrateWithAgent(agent, argv.from));
|
|
969
|
+
} catch (error) {
|
|
970
|
+
addLog(<ErrorLog message={error instanceof Error ? error.message : String(error)} />);
|
|
971
|
+
return await terminate(1);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
await new Promise<void>((resolve) =>
|
|
975
|
+
process.stdout.write(migrationInstructions(argv), () => resolve())
|
|
976
|
+
);
|
|
977
|
+
await terminate(0);
|
|
978
|
+
}
|
|
979
|
+
)
|
|
889
980
|
// Coming soon commands — visible in help, tracked via telemetry to gauge interest.
|
|
890
981
|
.command(
|
|
891
982
|
'ai',
|
package/src/migrate.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
|
|
6
|
+
import { windowsCompatibleSpawnOptions } from './helpers.js';
|
|
7
|
+
|
|
8
|
+
type Section = 'topics' | 'sources';
|
|
9
|
+
|
|
10
|
+
const root = fileURLToPath(new URL('../migrate', import.meta.url));
|
|
11
|
+
|
|
12
|
+
const read = (...segments: string[]) => readFileSync(path.join(root, ...segments), 'utf8');
|
|
13
|
+
|
|
14
|
+
export const listMigrationDocs = (section: Section) =>
|
|
15
|
+
readdirSync(path.join(root, section))
|
|
16
|
+
.filter((file) => file.endsWith('.md'))
|
|
17
|
+
.map((file) => file.slice(0, -3))
|
|
18
|
+
.sort();
|
|
19
|
+
|
|
20
|
+
const overview = () =>
|
|
21
|
+
[
|
|
22
|
+
read('index.md').trimEnd(),
|
|
23
|
+
`Topics: ${listMigrationDocs('topics').join(', ')}. Run \`mint migrate --topic <name>\`.`,
|
|
24
|
+
`Sources: ${listMigrationDocs('sources').join(', ')}. Run \`mint migrate --from <name>\`.`,
|
|
25
|
+
].join('\n\n');
|
|
26
|
+
|
|
27
|
+
export const migrationInstructions = ({ from, topic }: { from?: string; topic?: string }) => {
|
|
28
|
+
if (topic) return read('topics', `${topic}.md`);
|
|
29
|
+
if (from) return `${overview()}\n\n${read('sources', `${from}.md`)}`;
|
|
30
|
+
return `${overview()}\n`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const prompt = (from?: string) =>
|
|
34
|
+
`Migrate a documentation site to Mintlify in the current directory. Run \`mint migrate${from ? ` --from ${from}` : ''}\` and follow the instructions it prints.`;
|
|
35
|
+
|
|
36
|
+
export const agents = {
|
|
37
|
+
claude: {
|
|
38
|
+
bin: 'claude',
|
|
39
|
+
args: (prompt: string) => [prompt],
|
|
40
|
+
install: 'npm install -g @anthropic-ai/claude-code',
|
|
41
|
+
},
|
|
42
|
+
codex: {
|
|
43
|
+
bin: 'codex',
|
|
44
|
+
args: (prompt: string) => [prompt],
|
|
45
|
+
install: 'npm install -g @openai/codex',
|
|
46
|
+
},
|
|
47
|
+
cursor: {
|
|
48
|
+
bin: 'cursor-agent',
|
|
49
|
+
args: (prompt: string) => [prompt],
|
|
50
|
+
install: 'curl https://cursor.com/install -fsS | bash',
|
|
51
|
+
},
|
|
52
|
+
gemini: {
|
|
53
|
+
bin: 'gemini',
|
|
54
|
+
args: (prompt: string) => ['-i', prompt],
|
|
55
|
+
install: 'npm install -g @google/gemini-cli',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type AgentName = keyof typeof agents;
|
|
60
|
+
|
|
61
|
+
export const agentArgs = (name: AgentName, from?: string) => agents[name].args(prompt(from));
|
|
62
|
+
|
|
63
|
+
export const migrateWithAgent = (name: AgentName, from?: string): Promise<number> =>
|
|
64
|
+
new Promise((resolve, reject) => {
|
|
65
|
+
const { bin, install } = agents[name];
|
|
66
|
+
const args = agentArgs(name, from).map((arg) =>
|
|
67
|
+
windowsCompatibleSpawnOptions.shell ? `"${arg}"` : arg
|
|
68
|
+
);
|
|
69
|
+
const child = spawn(bin, args, { stdio: 'inherit', ...windowsCompatibleSpawnOptions });
|
|
70
|
+
child.on('error', (error: NodeJS.ErrnoException) =>
|
|
71
|
+
reject(
|
|
72
|
+
error.code === 'ENOENT' ? new Error(`${bin} not found. Install it with: ${install}`) : error
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
76
|
+
});
|
package/src/validate/files.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
parseContent,
|
|
14
14
|
relocate,
|
|
15
15
|
selectEdits,
|
|
16
|
+
toOffset,
|
|
16
17
|
unmapEdit,
|
|
17
18
|
} from './mdx.js';
|
|
18
19
|
import { PARSE_ERROR } from './rules/parseError.js';
|
|
@@ -56,16 +57,35 @@ const readFrontmatter = (content: string): Record<string, unknown> => {
|
|
|
56
57
|
type ParseError = { line?: number; column?: number; message: string };
|
|
57
58
|
|
|
58
59
|
const REASON_POSITION = /\s*\(\d+:\d+(?:-\d+:\d+)?\)$/;
|
|
60
|
+
const TAG_NAME_RULES = new Set(['unexpected-character', 'unexpected-eof']);
|
|
61
|
+
const ESCAPE_HINT = "; if this < isn't a tag, escape it as \\<";
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
const isStrayLessThan = (
|
|
64
|
+
error: unknown,
|
|
65
|
+
content: string,
|
|
66
|
+
position: { line?: number; column?: number } | null
|
|
67
|
+
) => {
|
|
68
|
+
const { source, ruleId } = (error ?? {}) as { source?: unknown; ruleId?: unknown };
|
|
69
|
+
return (
|
|
70
|
+
source === 'micromark-extension-mdx-jsx' &&
|
|
71
|
+
typeof ruleId === 'string' &&
|
|
72
|
+
TAG_NAME_RULES.has(ruleId) &&
|
|
73
|
+
position?.line != null &&
|
|
74
|
+
position.column != null &&
|
|
75
|
+
content[toOffset(content, position.line, position.column) - 1] === '<'
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const toParseError = (error: unknown, content = ''): ParseError => {
|
|
61
80
|
const reason = (error as { reason?: unknown } | null)?.reason;
|
|
62
81
|
const fallback = error instanceof Error ? error.message : 'could not parse this file';
|
|
63
82
|
const text = typeof reason === 'string' ? reason : fallback;
|
|
64
83
|
const position = error && typeof error === 'object' ? getErrorPosition(error) : null;
|
|
84
|
+
const hint = isStrayLessThan(error, content, position) ? ESCAPE_HINT : '';
|
|
65
85
|
return {
|
|
66
86
|
...(position?.line != null && { line: position.line }),
|
|
67
87
|
...(position?.column != null && { column: position.column }),
|
|
68
|
-
message: text.replace(REASON_POSITION, ''),
|
|
88
|
+
message: text.replace(REASON_POSITION, '') + hint,
|
|
69
89
|
};
|
|
70
90
|
};
|
|
71
91
|
|
|
@@ -102,7 +122,7 @@ export const parseFile = (
|
|
|
102
122
|
try {
|
|
103
123
|
return { context: { ...context, tree: getAST(content, file) }, approximate: true };
|
|
104
124
|
} catch (error) {
|
|
105
|
-
return { error: toParseError(error) };
|
|
125
|
+
return { error: toParseError(error, content) };
|
|
106
126
|
}
|
|
107
127
|
};
|
|
108
128
|
|
package/src/validate/mdx.ts
CHANGED
|
@@ -186,7 +186,7 @@ export const applyEdits = (content: string, edits: Edit[]): string =>
|
|
|
186
186
|
content
|
|
187
187
|
);
|
|
188
188
|
|
|
189
|
-
const toOffset = (content: string, line: number, column = 1) => {
|
|
189
|
+
export const toOffset = (content: string, line: number, column = 1) => {
|
|
190
190
|
let offset = 0;
|
|
191
191
|
for (let current = 1; current < line; current++) {
|
|
192
192
|
const next = content.indexOf('\n', offset);
|
package/src/welcome.ts
CHANGED
|
@@ -57,6 +57,7 @@ const GROUPS: CommandGroup[] = [
|
|
|
57
57
|
{
|
|
58
58
|
title: 'CLI',
|
|
59
59
|
commands: [
|
|
60
|
+
{ name: 'agent setup', description: 'Install Mintlify skills and configure Admin MCP' },
|
|
60
61
|
{ name: 'config', description: 'Manage CLI configuration' },
|
|
61
62
|
{ name: 'update', description: 'Update to the latest version' },
|
|
62
63
|
],
|