import { addLog, dev, exportSite, validateBuild, ErrorLog, SuccessLog, Logs, WarningLog, } from '@mintlify/previewing'; import { thumbnailOverrideSchema } from '@mintlify/validation'; import { render, Text } from 'ink'; import fs from 'node:fs/promises'; import path from 'path'; import yargs, { type Argv } from 'yargs'; import { hideBin } from 'yargs/helpers'; import { accessibilityCheck } from './accessibilityCheck.js'; import { addDomain } from './addDomain.js'; import { claimDeployment, createAgentClaimLink } from './agent-claim.js'; import { analyticsBuilder } from './analytics/index.js'; import { brokenLinksHandler } from './brokenLinks.js'; import { comingSoon } from './comingSoon.js'; import { setTelemetryEnabled } from './config.js'; import { getConfigValue, setConfigValue, clearConfigValue } from './config.js'; import { API_URL } from './constants.js'; import { DEPLOY_HELP, deploymentInstructions } from './deploy-instructions.js'; import { formatDeployment, prepareAgentDeployment } from './deploy.js'; import { formatHandler } from './format.js'; import { CMD_EXEC_PATH, checkPort, checkNodeVersion, autoUpgradeIfNeeded, getVersions, isAI, suppressConsoleWarnings, terminate, } from './helpers.js'; import { init } from './init.js'; import { getAccessToken } from './keyring.js'; import { login } from './login.js'; import { logout } from './logout.js'; import { mdxLinter } from './mdxLinter.js'; import { createTelemetryMiddleware } from './middlewares/telemetryMiddleware.js'; import { ogCommand } from './og.js'; import { buildOverlayRegistryForCheck, checkOpenApiFile, getOpenApiFilenamesFromDocsConfig, } from './openApiCheck.js'; import { scoreHandler } from './score/index.js'; import { sendPublishPrompt, signup } from './signup.js'; import { status, getCliSubdomains } from './status.js'; import { trackTelemetryPreferenceChange } from './telemetry/track.js'; import { update } from './update.js'; import { renderWelcome } from './welcome.js'; import { workflowsBuilder } from './workflows/index.js'; // The branded welcome screen replaces yargs' default help, but only for an // interactive human at a TTY. Piped, scripted, AI, and test callers keep the // plain yargs help so their output stays parseable and stable. const shouldShowWelcome = (args: string[]): boolean => { const interactive = Boolean(process.stdout.isTTY) && !isAI() && process.env.CLI_TEST_MODE !== 'true'; if (!interactive) return false; if (args.length === 0) return true; const positionals = args.filter((arg) => !arg.startsWith('-')); const command = positionals[0]; // `help` followed by a subcommand (e.g. `mint help dev`) is a subcommand // lookup — let yargs handle it rather than intercepting with the welcome screen. const isTopLevel = command === undefined || (command === 'help' && positionals.length === 1); const helpRequested = command === 'help' || args.includes('--help') || args.includes('-h'); return isTopLevel && helpRequested; }; const showWelcome = async (packageName: string): Promise => { const { cli: version } = getVersions(packageName); const subdomain = getConfigValue('subdomain'); let signedIn = false; try { signedIn = Boolean(await getAccessToken()); } catch {} process.stdout.write(renderWelcome({ packageName, version, signedIn, subdomain })); await terminate(0); }; export const cli = ({ packageName = 'mint' }: { packageName?: string }) => { const commandArguments = hideBin(process.argv); if (shouldShowWelcome(commandArguments)) { return showWelcome(packageName); } const telemetryMiddleware = createTelemetryMiddleware(); render(); return ( yargs(commandArguments) .scriptName(packageName) .option('telemetry', { type: 'boolean', alias: 't', description: 'Enable or disable usage telemetry', }) .middleware(async (argv) => { if (argv.telemetry !== undefined && argv._.length === 0) { await setTelemetryEnabled(argv.telemetry); await trackTelemetryPreferenceChange({ enabled: argv.telemetry }); addLog(); await terminate(0); } }, true) .middleware(checkNodeVersion) .middleware(suppressConsoleWarnings) .middleware(telemetryMiddleware) .command( 'claim', 'Claim a site created with mint deploy', (yargs) => yargs .option('email', { type: 'string', description: 'User email for a claim link' }) .option('code', { type: 'string', description: 'Code returned by the user from the claim page', }) .option('directory', { type: 'string', default: '.', description: 'Original documentation directory used for mint deploy', }) .conflicts('code', 'email') .example('$0 claim --email you@example.com', 'Get a claim link for the current site') .example('$0 claim --code ABCDEFGH', 'Finish the claim with the code from the user') .epilog( 'Run from the same directory as mint deploy, or pass --directory. Ask the user to open the claim link and return the code. The CLI reads saved credentials internally; do not read or print keychain secrets. Retry the same command if Mintlify signup fails.' ), async (argv) => { try { const message = await claimDeployment( { apiUrl: API_URL, directory: argv.directory, code: argv.code, email: argv.email }, (message) => process.stderr.write(`Warning: ${message}\n`), (message) => process.stderr.write(`${message}\n`) ); process.stdout.write(`${message}\n`); await terminate(0); } catch (error) { process.stderr.write(`${error instanceof Error ? error.message : 'Claim failed.'}\n`); await terminate(1); } } ) .command( 'deploy', 'Provision an anonymous docs site and get Git push instructions', (yargs) => yargs .option('subdomain', { type: 'string', description: 'Requested site name; required for the first deployment or --new', }) .option('org-name', { type: 'string', description: 'Organization display name' }) .option('directory', { type: 'string', default: '.', description: 'Documentation directory associated with this site', }) .option('new', { type: 'boolean', default: false, description: 'Create a new registration and site', }) .option('email', { type: 'string', description: 'User email for an optional claim link', }) .example('$0 deploy --subdomain my-docs', 'Provision a site without signing in') .example( '$0 deploy --email you@example.com', 'Get repository access and a claim link for the current site' ) .example('$0 deploy --new --subdomain another-site', 'Create another anonymous site') .epilog(DEPLOY_HELP), async (argv) => { try { const result = await prepareAgentDeployment( { apiUrl: API_URL, directory: argv.directory, subdomain: argv.subdomain, orgName: argv.orgName, newSite: argv.new, }, (message) => { process.stderr.write(`Warning: ${message}\n`); }, (message) => process.stderr.write(`${message}\n`) ); process.stdout.write(`${formatDeployment(result, API_URL)}\n`); let claimUrl: string | undefined; if (argv.email && result.status === 'ready') { try { process.stderr.write('Creating the claim link...\n'); claimUrl = await createAgentClaimLink(API_URL, result.credentials, argv.email); } catch { process.stderr.write( 'Warning: Could not create the claim link. Your site is provisioned; retry with --email later.\n' ); } } process.stdout.write(`${deploymentInstructions(result, claimUrl)}\n`); await terminate(0); } catch (error) { process.stderr.write( `${error instanceof Error ? error.message : 'Deployment failed.'}\n` ); await terminate(1); } } ) .command( 'dev', 'Initialize a local preview environment', (yargs) => yargs .option('open', { type: 'boolean', default: true, description: 'open a local preview in the browser', }) .option('local-schema', { type: 'boolean', default: false, hidden: true, description: 'use a locally hosted schema file (note: only https protocol is supported in production)', }) .option('client-version', { type: 'string', hidden: true, description: 'the version of the client to use for cli testing', }) .option('local-client-version', { type: 'string', hidden: true, description: 'path to local client repo to symlink for development', }) .option('groups', { type: 'array', description: 'Mock user groups for local development and testing', example: '--groups admin user', }) .option('signed-in', { type: 'boolean', default: false, description: 'Mock a signed-in user with no groups for local development and testing', }) .option('disable-openapi', { type: 'boolean', default: false, description: 'Disable OpenAPI file generation', }) .option('disable-prefetch', { type: 'boolean', default: false, description: 'Disable navigation prefetching in the local preview', }) .option('host', { type: 'boolean', default: false, description: 'Expose the local preview on your network', }) .usage('usage: mintlify dev [options]') .example('mintlify dev', 'run with default settings (opens in browser)') .example('mintlify dev --no-open', 'run without opening in browser'), async (argv) => { await autoUpgradeIfNeeded(); const port = await checkPort(argv); const { cli: cliVersion } = getVersions(packageName); let accessToken: string | undefined; let subdomain: string | undefined; try { accessToken = (await getAccessToken()) ?? undefined; const configuredSubdomain = getConfigValue('subdomain'); subdomain = configuredSubdomain ?? (accessToken ? (await getCliSubdomains(accessToken))[0] : undefined); } catch {} if (port != undefined) { await dev({ ...argv, groups: argv.groups ?? (argv.signedIn ? [] : undefined), port, packageName, cliVersion, accessToken, subdomain, apiUrl: API_URL, }); } else { addLog(); await terminate(1); } } ) .command( 'validate', 'Validate the documentation build (strict mode, exits on warnings or errors)', (yargs) => yargs .option('local-schema', { type: 'boolean', default: false, hidden: true, description: 'use a locally hosted schema file', }) .option('client-version', { type: 'string', hidden: true, description: 'the version of the client to use for cli testing', }) .option('groups', { type: 'array', description: 'Mock user groups for validation', }) .option('disable-openapi', { type: 'boolean', default: false, description: 'Disable OpenAPI file generation', }) .usage('usage: mintlify validate [options]') .example('mintlify validate', 'validate the build'), async (argv) => { if (!argv['disable-openapi']) { let openApiFilenames: string[] = []; try { const docsJson = await fs.readFile(path.join(CMD_EXEC_PATH, 'docs.json'), 'utf-8'); openApiFilenames = getOpenApiFilenamesFromDocsConfig(JSON.parse(docsJson)); } catch {} let overlayRegistry; try { overlayRegistry = await buildOverlayRegistryForCheck(argv['local-schema']); } catch (error) { addLog(); await terminate(1); return; } const results = await Promise.all( openApiFilenames.map((file) => checkOpenApiFile(file, argv['local-schema'], overlayRegistry) ) ); if (results.some((valid) => !valid)) { await terminate(1); return; } } const { cli: cliVersion } = getVersions(packageName); await validateBuild( { ...argv, packageName, cliVersion, }, sendPublishPrompt ); } ) .command( 'export', 'Export a static site for air-gapped deployment', (yargs) => yargs .option('output', { type: 'string', default: 'export.zip', description: 'output zip file path', }) .option('client-version', { type: 'string', hidden: true, }) .option('local-client-version', { type: 'string', hidden: true, description: 'path to local client repo to symlink for development', }) .option('groups', { type: 'array', description: 'Mock user groups for export', }) .option('base-path', { type: 'string', description: 'path prefix the export will be served under, e.g. /docs', }) .option('disable-openapi', { type: 'boolean', default: false, description: 'Disable OpenAPI file generation', }) .usage('usage: mintlify export [options]') .example('mintlify export', 'export as export.zip') .example('mintlify export --output docs.zip', 'export to custom filename') .example('mintlify export --base-path /docs', 'export for hosting under /docs'), async (argv) => { const { cli: cliVersion } = getVersions(packageName); await exportSite({ ...argv, packageName, cliVersion, }); await terminate(0); } ) .command( 'openapi-check ', false, (yargs) => yargs .positional('filename', { describe: 'the filename of the OpenAPI spec (e.g. ./openapi.yaml) or the URL to the OpenAPI spec (e.g. https://petstore3.swagger.io/api/v3/openapi.json)', type: 'string', demandOption: true, }) .option('local-schema', { type: 'boolean', default: false, description: 'use a locally hosted schema file (note: only https protocol is supported in production)', }), async ({ filename, 'local-schema': localSchema }) => { addLog( ); let overlayRegistry; try { overlayRegistry = await buildOverlayRegistryForCheck(localSchema); } catch (error) { addLog(); await terminate(1); return; } const valid = await checkOpenApiFile(filename, localSchema, overlayRegistry); await terminate(valid ? 0 : 1); } ) .command( 'broken-links', 'Check for broken links', (yargs) => yargs .option('files', { type: 'string', array: true, description: 'Files or globs to check (defaults to the whole site)', }) .option('check-anchors', { type: 'boolean', default: false, description: 'also validate anchor links (e.g. #section) against heading slugs', }) .option('check-external', { type: 'boolean', default: false, description: 'also check external links for broken URLs', }) .option('check-snippets', { type: 'boolean', default: false, description: 'also check links inside components', }) .option('check-redirects', { type: 'boolean', default: false, description: 'also check that docs.json redirect destinations resolve to valid paths', }) .example('mint broken-links --files introduction.mdx', 'Check a specific page') .example('mint broken-links --files "guides/**/*.mdx"', 'Check pages matching a glob'), (argv) => brokenLinksHandler(argv) ) .command( 'status', 'View current authentication status', () => undefined, async () => { await status(); await terminate(0); } ) .command( 'logout', 'Logout of your Mintlify account', () => undefined, async () => { await logout(); await terminate(0); } ) .command( 'login', 'Authenticate your account to Mintlify', () => undefined, async () => { await login(); await terminate(0); } ) .command( 'signup', 'Create a new Mintlify account', (yargs: Argv) => yargs .option('firstName', { type: 'string', description: 'First name' }) .option('lastName', { type: 'string', description: 'Last name' }) .option('company', { type: 'string', description: 'Company name' }) .option('email', { type: 'string', description: 'Email address' }) .epilogue( 'Note: this command does not return until the user clicks the verification link ' + 'sent to their email, which can take several minutes. Non-interactive or scripted ' + 'callers should run it as a background process rather than waiting on it synchronously.' ), async (argv) => { await signup({ firstName: argv.firstName, lastName: argv.lastName, company: argv.company, email: argv.email, }); await terminate(0); } ) .command( 'add-domain ', 'Add a custom domain to your deployment', (yargs: Argv) => yargs .positional('domain', { type: 'string', demandOption: true, description: 'The custom domain to add (e.g. docs.example.com)', }) .option('basePath', { type: 'string', description: 'Serve the docs under a base path (e.g. /docs)', }), async (argv) => { await addDomain(argv.domain, argv.basePath); await terminate(0); } ) .command('config', 'Manage CLI configuration', (yargs) => yargs .command( 'set ', 'Set a configuration value', (yargs) => yargs .positional('key', { type: 'string', demandOption: true, description: 'Config key (e.g. subdomain)', }) .positional('value', { type: 'string', demandOption: true, description: 'Config value', }), async (argv) => { const validKeys = ['subdomain', 'dateFrom', 'dateTo']; if (!validKeys.includes(argv.key)) { addLog( ); await terminate(1); return; } await setConfigValue(argv.key, argv.value); addLog(); await terminate(0); } ) .command( 'get ', 'Get a configuration value', (yargs) => yargs.positional('key', { type: 'string', demandOption: true, description: 'Config key (e.g. subdomain, dateFrom, dateTo)', }), async (argv) => { const validKeys = ['subdomain', 'dateFrom', 'dateTo']; if (!validKeys.includes(argv.key)) { addLog( ); await terminate(1); return; } const val = getConfigValue(argv.key); addLog({val ?? 'not set'}); await terminate(0); } ) .command( 'clear ', 'Remove a configuration value', (yargs) => yargs.positional('key', { type: 'string', demandOption: true, description: 'Config key (e.g. subdomain, dateFrom, dateTo)', }), async (argv) => { const validKeys = ['subdomain', 'dateFrom', 'dateTo']; if (!validKeys.includes(argv.key)) { addLog( ); await terminate(1); return; } await clearConfigValue(argv.key); addLog(); await terminate(0); } ) .demandCommand(1, 'specify a subcommand: set, get, or clear') ) .command( 'update', 'Update the CLI to the latest version', () => undefined, async () => { await update({ packageName }); await terminate(0); } ) .command( ['a11y', 'accessibility-check', 'a11y-check', 'accessibility'], 'Check for accessibility issues in documentation', (yargs) => yargs .option('skip-contrast', { type: 'boolean', default: false, description: 'Skip color contrast checks', }) .option('skip-alt-text', { type: 'boolean', default: false, description: 'Skip alt text checks on images and videos', }) .check((argv) => { if (argv.skipContrast && argv.skipAltText) { throw new Error('Cannot skip both contrast and alt-text checks'); } return true; }) .example('mint a11y', 'Run all accessibility checks') .example('mint a11y --skip-contrast', 'Only check for missing alt text') .example('mint a11y --skip-alt-text', 'Only check color contrast'), async (argv) => { let accessibilityCheckTerminateCode = 0; let mdxLinterTerminateCode = 0; if (!argv.skipContrast) { accessibilityCheckTerminateCode = await accessibilityCheck(); } if (!argv.skipAltText) { mdxLinterTerminateCode = await mdxLinter(); } await terminate(accessibilityCheckTerminateCode || mdxLinterTerminateCode); } ) .command( 'og [page]', 'Render the social preview (OG) image for a page', (yargs) => yargs .positional('page', { type: 'string', description: 'Page path, such as guides/webhooks. Defaults to index', }) .option('preset', { choices: thumbnailOverrideSchema.shape.preset.unwrap().options, description: 'Try a preset without editing docs.json', }) .option('surface', { choices: thumbnailOverrideSchema.shape.surface.unwrap().options, description: 'Try a surface without editing docs.json', }) .option('appearance', { choices: thumbnailOverrideSchema.shape.appearance.unwrap().options, description: 'Try an appearance without editing docs.json', }) .option('output', { type: 'string', description: 'Where to write the PNG. Defaults to a temporary file', }) .option('open', { type: 'boolean', default: true, description: 'Open the image after rendering. Use --no-open in CI', }) .example('mint og guides/webhooks', 'Render one page and open it') .example('mint og --preset headline --surface brand', 'Try settings on the home page') .example('mint og --output og.png --no-open', 'Render to a file in CI'), async (argv) => { await terminate(await ogCommand(argv)); } ) .command( ['version', 'v'], 'Display the current version of the CLI and client', () => undefined, async () => { const { cli, client } = getVersions(packageName); addLog( cli version {' '} {cli} ); addLog( client version {' '} {client} ); } ) .command( 'new [directory]', 'Create a new Mintlify documentation site', (yargs) => yargs .positional('directory', { describe: 'The directory to initialize your documentation', type: 'string', default: '.', }) .option('theme', { type: 'string', description: 'Theme for the documentation site', }) .option('name', { type: 'string', description: 'Name of the documentation project', }) .option('template', { type: 'string', description: 'Use a template as a starting point', }) .option('force', { type: 'boolean', default: false, description: 'Create the documentation in a subdirectory', }), async ({ directory, theme, name, force, template }) => { try { await init(directory, force, theme, name, template); await terminate(0); } catch (error) { addLog( ); await terminate(1); } } ) .command('analytics', 'View analytics for your documentation', analyticsBuilder) .command( ['automations', 'workflow', 'workflows'], 'Create and manage automations', workflowsBuilder ) .command( 'score [url]', 'Run agent readiness checks on a docs site', (yargs: Argv) => yargs .positional('url', { type: 'string', description: 'URL of the docs site to check (defaults to your configured subdomain)', }) .option('format', { type: 'string', choices: ['table', 'plain', 'json'] as const, description: 'Output format', }) .example('mint score', 'Run agent readiness checks on your default subdomain') .example('mint score docs.example.com', 'Run agent readiness checks on a URL'), scoreHandler ) .command( 'format', 'Format MDX files in the current directory', (yargs) => yargs .usage('usage: mint format') .example('mint format', 'format all MDX files in the current directory'), formatHandler ) // Coming soon commands — visible in help, tracked via telemetry to gauge interest. .command( 'ai', '[Coming soon] AI-powered documentation (run mint ai to vote)', () => undefined, comingSoon('ai', packageName) ) .command( 'mcp', '[Coming soon] MCP server for documentation (run mint mcp to vote)', () => undefined, comingSoon('mcp', packageName) ) // Print the help menu when the user enters an invalid command. .strictCommands() .demandCommand(1, 'unknown command. see above for the list of supported commands.') // Alias option flags --help = -h, default --version = -v .alias('h', 'help') .alias('v', 'version') .parseAsync() ); };