@mintlify/cli
Version:
The Mintlify CLI
376 lines (375 loc) • 15.8 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { jsx as _jsx } from "react/jsx-runtime";
import { potentiallyParseOpenApiString, parseFrontmatter } from '@mintlify/common';
import { getConfigObj, getConfigPath } from '@mintlify/prebuild';
import { addLog, ErrorLog, SuccessLog } from '@mintlify/previewing';
import { divisions, validateDocsConfig, } from '@mintlify/validation';
import fs from 'fs';
import { outputFile } from 'fs-extra';
import inquirer from 'inquirer';
import yaml from 'js-yaml';
import path from 'path';
import { CMD_EXEC_PATH } from './constants.js';
const specCache = {};
const candidateSpecCache = {};
const specLocks = new Map();
function withSpecLock(specPath, task) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const key = path.resolve(specPath);
const previous = (_a = specLocks.get(key)) !== null && _a !== void 0 ? _a : Promise.resolve();
let releaseNext;
const next = new Promise((resolve) => {
releaseNext = resolve;
});
specLocks.set(key, next);
yield previous;
try {
yield task();
}
finally {
releaseNext();
}
});
}
let inquirerLockQueue = Promise.resolve();
function withInquirerLock(task) {
return __awaiter(this, void 0, void 0, function* () {
const previous = inquirerLockQueue;
let releaseNext;
const next = new Promise((resolve) => {
releaseNext = resolve;
});
inquirerLockQueue = next;
yield previous;
try {
return yield task();
}
finally {
releaseNext();
}
});
}
export function migrateMdx() {
return __awaiter(this, void 0, void 0, function* () {
const docsConfigPath = yield getConfigPath(CMD_EXEC_PATH, 'docs');
if (!docsConfigPath) {
addLog(_jsx(ErrorLog, { message: "docs.json not found in current directory" }));
return;
}
const docsConfigObj = yield getConfigObj(CMD_EXEC_PATH, 'docs');
const validationResults = yield validateDocsConfig(docsConfigObj);
if (!validationResults.success) {
addLog(_jsx(ErrorLog, { message: "docs.json is invalid" }));
return;
}
const validatedDocsConfig = validationResults.data;
const docsConfig = docsConfigObj;
yield buildCandidateSpecCacheIfNeeded(CMD_EXEC_PATH);
const updatedNavigation = yield processNav(validatedDocsConfig.navigation);
docsConfig.navigation = updatedNavigation;
yield outputFile(docsConfigPath, JSON.stringify(docsConfig, null, 2));
addLog(_jsx(SuccessLog, { message: "docs.json updated" }));
for (const specPath in specCache) {
const specObj = specCache[specPath];
const ext = path.extname(specPath).toLowerCase();
const stringified = ext === '.json' ? JSON.stringify(specObj, null, 2) : yaml.dump(specObj);
yield outputFile(specPath, stringified);
addLog(_jsx(SuccessLog, { message: `updated ${path.relative(CMD_EXEC_PATH, specPath)}` }));
}
addLog(_jsx(SuccessLog, { message: "migration complete" }));
});
}
function processNav(nav) {
return __awaiter(this, void 0, void 0, function* () {
let newNav = Object.assign({}, nav);
if ('pages' in newNav) {
newNav.pages = yield Promise.all(newNav.pages.map((page) => __awaiter(this, void 0, void 0, function* () {
if (typeof page === 'object' && page !== null && 'group' in page) {
return processNav(page);
}
if (typeof page === 'string' && !/\s/.test(page)) {
const mdxCandidatePath = path.join(CMD_EXEC_PATH, `${page}.mdx`);
if (!fs.existsSync(mdxCandidatePath)) {
return page;
}
const fmParsed = parseFrontmatter(yield fs.promises.readFile(mdxCandidatePath, 'utf-8'));
const frontmatter = fmParsed.attributes;
const content = fmParsed.body;
if (!frontmatter.openapi) {
return page;
}
const parsed = potentiallyParseOpenApiString(frontmatter.openapi);
if (!parsed) {
addLog(_jsx(ErrorLog, { message: `invalid openapi frontmatter in ${mdxCandidatePath}: ${frontmatter.openapi}` }));
return page;
}
const { filename, method, endpoint: endpointPath } = parsed;
let specPath = filename;
if (specPath && URL.canParse(specPath)) {
return page;
}
if (!specPath) {
const methodLower = method.toLowerCase();
const matchingSpecs = yield findMatchingOpenApiSpecs({
method: methodLower,
endpointPath,
}, candidateSpecCache);
if (matchingSpecs.length === 0) {
addLog(_jsx(ErrorLog, { message: `no OpenAPI spec found for ${method.toUpperCase()} ${endpointPath} in repository` }));
return page;
}
if (matchingSpecs.length === 1) {
specPath = path.relative(CMD_EXEC_PATH, matchingSpecs[0]);
}
else {
const answer = yield withInquirerLock(() => inquirer.prompt([
{
type: 'list',
name: 'chosen',
message: `multiple OpenAPI specs found for ${method.toUpperCase()} ${endpointPath}. which one should be used for ${path.relative(CMD_EXEC_PATH, mdxCandidatePath)}?`,
choices: matchingSpecs.map((p) => ({
name: path.relative(CMD_EXEC_PATH, p),
value: path.relative(CMD_EXEC_PATH, p),
})),
},
]));
specPath = answer.chosen;
}
}
const href = `/${page}`;
const pageName = specPath ? `${specPath} ${method} ${endpointPath}` : frontmatter.openapi;
delete frontmatter.openapi;
yield withSpecLock(path.resolve(specPath), () => migrateToXMint({
specPath,
method,
endpointPath,
frontmatter,
content,
href,
}));
try {
yield fs.promises.unlink(mdxCandidatePath);
}
catch (err) {
addLog(_jsx(ErrorLog, { message: `failed to delete ${mdxCandidatePath}: ${err.message}` }));
}
return pageName;
}
return page;
})));
}
for (const division of ['groups', ...divisions]) {
if (division in newNav) {
const items = newNav[division];
newNav = Object.assign(Object.assign({}, newNav), { [division]: yield Promise.all(items.map((item) => processNav(item))) });
}
}
return newNav;
});
}
function migrateToXMint(args) {
return __awaiter(this, void 0, void 0, function* () {
const { specPath, method, endpointPath, frontmatter, content, href } = args;
if (!fs.existsSync(specPath)) {
addLog(_jsx(ErrorLog, { message: `spec file not found: ${specPath}` }));
return;
}
let specObj;
if (path.resolve(specPath) in specCache) {
specObj = specCache[path.resolve(specPath)];
}
else {
const pathname = path.join(CMD_EXEC_PATH, specPath);
const file = yield fs.promises.readFile(pathname, 'utf-8');
const ext = path.extname(specPath).toLowerCase();
if (ext === '.json') {
specObj = JSON.parse(file);
}
else if (ext === '.yml' || ext === '.yaml') {
specObj = yaml.load(file);
}
else {
addLog(_jsx(ErrorLog, { message: `unsupported spec file extension: ${specPath}` }));
return;
}
}
const methodLower = method.toLowerCase();
if (!editXMint(specObj, endpointPath, methodLower, {
metadata: Object.keys(frontmatter).length > 0 ? frontmatter : undefined,
content: content.length > 0 ? content : undefined,
href,
})) {
addLog(_jsx(ErrorLog, { message: `operation not found in spec: ${method.toUpperCase()} ${endpointPath} in ${specPath}` }));
return;
}
specCache[path.resolve(specPath)] = specObj;
});
}
function editXMint(document, path, method, newXMint) {
if (method === 'webhook') {
return editWebhookXMint(document, path, newXMint);
}
if (!document.paths || !document.paths[path]) {
return false;
}
const pathItem = document.paths[path];
const normalizedMethod = method.toLowerCase();
if (!pathItem[normalizedMethod]) {
return false;
}
const operation = pathItem[normalizedMethod];
operation['x-mint'] = newXMint;
if ('x-mcp' in operation && !('mcp' in operation['x-mint'])) {
operation['x-mint']['mcp'] = operation['x-mcp'];
delete operation['x-mcp'];
}
return true;
}
function editWebhookXMint(document, path, newXMint) {
var _a;
const webhookObject = (_a = document.webhooks) === null || _a === void 0 ? void 0 : _a[path];
if (!webhookObject || typeof webhookObject !== 'object') {
return false;
}
if (!webhookObject['post']) {
return false;
}
const operation = webhookObject['post'];
operation['x-mint'] = newXMint;
if ('x-mcp' in operation && !('mcp' in operation['x-mint'])) {
operation['x-mint']['mcp'] = operation['x-mcp'];
delete operation['x-mcp'];
}
return true;
}
function findMatchingOpenApiSpecs(args, docsByPath) {
return __awaiter(this, void 0, void 0, function* () {
const { method, endpointPath } = args;
const docsEntries = docsByPath
? Object.entries(docsByPath)
: (yield collectOpenApiFiles(CMD_EXEC_PATH)).map((absPath) => [absPath, undefined]);
const normalizedMethod = method.toLowerCase();
const endpointVariants = new Set([endpointPath]);
if (!endpointPath.startsWith('/')) {
endpointVariants.add(`/${endpointPath}`);
}
else {
endpointVariants.add(endpointPath.replace(/^\/+/, ''));
}
const matches = [];
for (const [absPath, maybeDoc] of docsEntries) {
try {
const doc = maybeDoc || (yield loadOpenApiDocument(absPath));
if (!doc)
continue;
if (normalizedMethod === 'webhook') {
const webhooks = doc.webhooks;
if (!webhooks)
continue;
for (const key of Object.keys(webhooks)) {
if (endpointVariants.has(key)) {
const pathItem = webhooks[key];
if (pathItem && typeof pathItem === 'object' && 'post' in pathItem && pathItem.post) {
matches.push(absPath);
break;
}
}
}
continue;
}
if (!doc.paths)
continue;
for (const variant of endpointVariants) {
const pathItem = doc.paths[variant];
if (!pathItem)
continue;
const hasOperation = !!pathItem[normalizedMethod];
if (hasOperation) {
matches.push(absPath);
break;
}
}
}
catch (_a) { }
}
return matches.map((abs) => path.resolve(abs)).filter((v, i, a) => a.indexOf(v) === i);
});
}
function collectOpenApiFiles(rootDir) {
return __awaiter(this, void 0, void 0, function* () {
const results = [];
const excludedDirs = new Set([
'node_modules',
'.git',
'dist',
'build',
'.next',
'.vercel',
'out',
'coverage',
'tmp',
'temp',
]);
function walk(currentDir) {
return __awaiter(this, void 0, void 0, function* () {
const entries = yield fs.promises.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const abs = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
if (excludedDirs.has(entry.name))
continue;
yield walk(abs);
}
else if (entry.isFile()) {
if (/\.(ya?ml|json)$/i.test(entry.name)) {
results.push(abs);
}
}
}
});
}
yield walk(rootDir);
return results;
});
}
function loadOpenApiDocument(absPath) {
return __awaiter(this, void 0, void 0, function* () {
try {
const file = yield fs.promises.readFile(absPath, 'utf-8');
const ext = path.extname(absPath).toLowerCase();
let doc;
if (ext === '.json') {
doc = JSON.parse(file);
}
else if (ext === '.yml' || ext === '.yaml') {
doc = yaml.load(file);
}
return doc;
}
catch (_a) {
return undefined;
}
});
}
function buildCandidateSpecCacheIfNeeded(rootDir) {
return __awaiter(this, void 0, void 0, function* () {
if (Object.keys(candidateSpecCache).length > 0)
return;
const files = yield collectOpenApiFiles(rootDir);
yield Promise.all(files.map((abs) => __awaiter(this, void 0, void 0, function* () {
const doc = yield loadOpenApiDocument(abs);
if (doc) {
candidateSpecCache[path.resolve(abs)] = doc;
}
})));
});
}