import { getConfigPath } from '@mintlify/prebuild'; import { MintConfigUpdater } from '@mintlify/prebuild'; import { addLog, ErrorLog, getClientVersion, SuccessLog, InfoLog, SpinnerLog, removeLastLog, LOCAL_LINKED_CLI_VERSION, WarningLog, } from '@mintlify/previewing'; import { upgradeToDocsConfig, validatePathWithinCwd } from '@mintlify/validation'; import detect from 'detect-port'; import fse from 'fs-extra'; import inquirer from 'inquirer'; import yaml from 'js-yaml'; import { execFile, execFileSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import fs from 'node:fs/promises'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; import path from 'path'; import type { ArgumentsCamelCase } from 'yargs'; import { detectAgent } from './agent.js'; import { shutdownPostHog } from './telemetry/client.js'; export const CMD_EXEC_PATH = process.cwd(); export const checkPort = async (argv: ArgumentsCamelCase): Promise => { const initialPort = typeof argv.port === 'number' ? argv.port : 3000; if (initialPort === (await detect(initialPort))) return initialPort; for (let port = initialPort + 1; port < initialPort + 10; port++) { addLog(); if (port === (await detect(port))) return port; } }; export const checkNodeVersion = async () => { let nodeVersionString = process.version; if (nodeVersionString.charAt(0) === 'v') { nodeVersionString = nodeVersionString.slice(1); } const versionArr = nodeVersionString.split('.'); const majorVersion = parseInt(versionArr[0]!, 10); const minorVersion = parseInt(versionArr[1]!, 10); if (majorVersion === 25) { addLog( ); await terminate(1); } if (majorVersion < 20 || (majorVersion === 20 && minorVersion < 17)) { addLog( ); await terminate(1); } }; export const checkForMintJson = async () => { return !!(await getConfigPath(CMD_EXEC_PATH, 'mint')); }; export const findDocsRoot = async (startDir: string): Promise => { const start = path.resolve(startDir); const { root } = path.parse(start); let dir = start; while (dir !== root) { if ((await getConfigPath(dir, 'docs')) || (await getConfigPath(dir, 'mint'))) return dir; dir = path.dirname(dir); } return start; }; export const checkForDocsJson = async () => { const docsJsonPath = path.join(CMD_EXEC_PATH, 'docs.json'); if (!(await fse.pathExists(docsJsonPath))) { addLog(); const promptResult = await inquirer.prompt([ { type: 'list', name: 'action', message: 'would you like to upgrade your mint.json to docs.json?', choices: [ { name: 'upgrade (migrate from mint.json to docs.json)', value: 'upgrade' }, { name: 'continue (use existing mint.json)', value: 'continue' }, ], }, ]); const { action } = promptResult; if (action === 'continue') { addLog(); } if (action === 'upgrade') { addLog(); await upgradeConfig(); } } }; export const autoUpgradeIfNeeded = async () => { const hasMintJson = await checkForMintJson(); if (!hasMintJson) return; const docsJsonPath = path.join(CMD_EXEC_PATH, 'docs.json'); const hasDocsJson = await fse.pathExists(docsJsonPath); if (!hasDocsJson) { addLog(); addLog(); await upgradeConfig(); } }; export const upgradeConfig = async () => { try { const mintJsonPath = path.join(CMD_EXEC_PATH, 'mint.json'); const docsJsonPath = path.join(CMD_EXEC_PATH, 'docs.json'); const mintJsonFileContent = await fs.readFile(mintJsonPath, 'utf8'); const validationResult = await MintConfigUpdater.validateConfigJsonString(mintJsonFileContent); const mintConfig = validationResult.data; const upgradedDocsConfig = upgradeToDocsConfig(mintConfig, { shouldUpgradeTheme: true, }); await fs.writeFile(docsJsonPath, JSON.stringify(upgradedDocsConfig, null, 2)); removeLastLog(); addLog(); } catch (err) { removeLastLog(); addLog(); } }; const require = createRequire(import.meta.url); const isRunningFromLinkedBuild = (): boolean => { try { const thisFile = fileURLToPath(import.meta.url); return !thisFile.split(path.sep).includes('node_modules'); } catch { return false; } }; const readPackageVersion = (packageName: string): string | undefined => { try { const pkgPath = require.resolve(`${packageName}/package.json`); const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version?: string }; return typeof pkg.version === 'string' ? pkg.version : undefined; } catch { return undefined; } }; export const getCliVersion = ( packageName: string = process.env.MINTLIFY_PACKAGE_NAME ?? 'mint' ): string | undefined => { if (process.env.CLI_TEST_MODE === 'true') { return 'test-cli'; } if (isRunningFromLinkedBuild()) { return LOCAL_LINKED_CLI_VERSION; } return readPackageVersion(packageName); }; export const getVersions = ( packageName: string = process.env.MINTLIFY_PACKAGE_NAME ?? 'mint' ): { cli: string | undefined; client: string | undefined; } => { const cli = getCliVersion(packageName); const client = getClientVersion().trim(); return { cli, client }; }; // On Windows, npm/pnpm entrypoints are .cmd shims that Node refuses to spawn // without a shell (EINVAL since the CVE-2024-27980 fix). Everywhere else we // keep the shell disabled so arguments can't be interpreted as shell syntax. export const windowsCompatibleSpawnOptions = { shell: process.platform === 'win32', }; // npm package name grammar (optionally scoped). Rejecting anything else before // it reaches a package-manager spawn keeps the win32 shell path (above) safe // from shell metacharacters. const VALID_PACKAGE_NAME_PATTERN = /^(@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*$/; export const assertValidPackageName = (packageName: string): void => { if (!VALID_PACKAGE_NAME_PATTERN.test(packageName)) { throw new Error(`Invalid package name: ${packageName}`); } }; export const getLatestCliVersion = (packageName: string) => { assertValidPackageName(packageName); return execFileSync('npm', ['view', packageName, 'version', '--silent'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], ...windowsCompatibleSpawnOptions, }).trim(); }; export const suppressConsoleWarnings = (): void => { // Ignore tailwind warnings and punycode deprecation warning const ignoredMessages = [ 'No utility classes were detected', 'https://tailwindcss.com/docs/content-configuration', 'DeprecationWarning', ]; const originalConsoleError = console.error; console.error = (...args) => { const message = args.join(' '); if (ignoredMessages.some((ignoredMessage) => message.includes(ignoredMessage))) { return; } originalConsoleError.apply(console, args); }; const originalConsoleWarn = console.warn; console.warn = (...args) => { const message = args.join(' '); if (ignoredMessages.some((ignoredMessage) => message.includes(ignoredMessage))) { return; } originalConsoleWarn.apply(console, args); }; }; export const readLocalOpenApiFile = async ( filename: string ): Promise | undefined> => { const { resolvedPath } = validatePathWithinCwd(filename, CMD_EXEC_PATH); const file = await fs.readFile(resolvedPath, 'utf-8'); const document = yaml.load(file) as Record | undefined; return document; }; export const terminate = async (code: number) => { // Wait for the logs to be fully rendered before exiting await new Promise((resolve) => setTimeout(resolve, 50)); await shutdownPostHog(); process.exit(code); }; export const execFileAsync = promisify(execFile); export function isAI(): boolean { return !process.stdin.isTTY || detectAgent() !== undefined; } export const detectPackageManager = async ({ packageName }: { packageName: string }) => { assertValidPackageName(packageName); try { const { stdout: packagePath } = await execFileAsync('which', [packageName]); if (packagePath.includes('pnpm')) { return 'pnpm'; } else { return 'npm'; } } catch (error) { return 'npm'; } };