create-dox 0.1.0 → 0.3.0
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/dist/chunk-HHVOU4Q2.js +966 -0
- package/dist/chunk-UCHJJQVK.js +335 -0
- package/dist/index.js +605 -312
- package/dist/migrate/index.js +8 -0
- package/dist/scaffold.js +7 -0
- package/package.json +24 -17
- package/README.md +0 -79
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
initGit,
|
|
4
|
+
installDeps,
|
|
5
|
+
scaffold
|
|
6
|
+
} from "./chunk-UCHJJQVK.js";
|
|
7
|
+
|
|
8
|
+
// src/migrate/index.ts
|
|
9
|
+
import { mkdirSync as mkdirSync2, copyFileSync as copyFileSync2, readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdtempSync, rmSync } from "fs";
|
|
10
|
+
import { join as join3, dirname as dirname2, resolve } from "path";
|
|
11
|
+
import { tmpdir } from "os";
|
|
12
|
+
import pLimit from "p-limit";
|
|
13
|
+
|
|
14
|
+
// src/migrate/github.ts
|
|
15
|
+
import { execSync } from "child_process";
|
|
16
|
+
import { existsSync, readdirSync, statSync, copyFileSync, mkdirSync } from "fs";
|
|
17
|
+
import { join, relative, extname, basename, dirname } from "path";
|
|
18
|
+
var OPENAPI_FILENAMES = [
|
|
19
|
+
"openapi.json",
|
|
20
|
+
"openapi.yaml",
|
|
21
|
+
"openapi.yml",
|
|
22
|
+
"swagger.json",
|
|
23
|
+
"swagger.yaml",
|
|
24
|
+
"swagger.yml"
|
|
25
|
+
];
|
|
26
|
+
function detectOpenApiSpec(cloneDir) {
|
|
27
|
+
return findOpenApiSpec(cloneDir, 0);
|
|
28
|
+
}
|
|
29
|
+
function findOpenApiSpec(dir, depth) {
|
|
30
|
+
if (depth > 3) return null;
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = readdirSync(dir);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
for (const filename of OPENAPI_FILENAMES) {
|
|
38
|
+
if (entries.includes(filename)) {
|
|
39
|
+
return { absPath: join(dir, filename), filename };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
44
|
+
const fullPath = join(dir, entry);
|
|
45
|
+
try {
|
|
46
|
+
if (statSync(fullPath).isDirectory()) {
|
|
47
|
+
const found = findOpenApiSpec(fullPath, depth + 1);
|
|
48
|
+
if (found) return found;
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function parseGitHubUrl(rawUrl) {
|
|
56
|
+
let url;
|
|
57
|
+
try {
|
|
58
|
+
url = new URL(rawUrl);
|
|
59
|
+
} catch {
|
|
60
|
+
throw new Error(`Invalid URL: ${rawUrl}`);
|
|
61
|
+
}
|
|
62
|
+
if (url.hostname !== "github.com") {
|
|
63
|
+
throw new Error(`URL must be a github.com URL, got: ${url.hostname}`);
|
|
64
|
+
}
|
|
65
|
+
const parts = url.pathname.replace(/^\//, "").split("/");
|
|
66
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
67
|
+
throw new Error(`GitHub URL must include owner and repo: ${rawUrl}`);
|
|
68
|
+
}
|
|
69
|
+
const owner = parts[0];
|
|
70
|
+
const repo = parts[1];
|
|
71
|
+
let branch = "HEAD";
|
|
72
|
+
let docsDir = "";
|
|
73
|
+
if (parts.length >= 4 && parts[2] === "tree") {
|
|
74
|
+
branch = parts[3];
|
|
75
|
+
if (parts.length > 4) {
|
|
76
|
+
docsDir = parts.slice(4).join("/");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const cloneUrl = `https://github.com/${owner}/${repo}.git`;
|
|
80
|
+
return { owner, repo, branch, docsDir, cloneUrl };
|
|
81
|
+
}
|
|
82
|
+
async function cloneRepo(source, targetDir) {
|
|
83
|
+
const parts = ["git", "clone", "--depth", "1"];
|
|
84
|
+
if (source.branch !== "HEAD") {
|
|
85
|
+
parts.push("--branch", source.branch);
|
|
86
|
+
}
|
|
87
|
+
parts.push(source.cloneUrl, targetDir);
|
|
88
|
+
const cmd = parts.join(" ");
|
|
89
|
+
try {
|
|
90
|
+
execSync(cmd, { stdio: "pipe" });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const stderr = err.stderr?.toString().trim() ?? "";
|
|
93
|
+
const msg = stderr || (err instanceof Error ? err.message : String(err));
|
|
94
|
+
throw new Error(`Failed to clone ${source.cloneUrl}: ${msg}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
var MD_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx"]);
|
|
98
|
+
var ALL_DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".rst", ".txt"]);
|
|
99
|
+
function hasMdFiles(dir) {
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = readdirSync(dir);
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const fullPath = join(dir, entry);
|
|
108
|
+
try {
|
|
109
|
+
const stat = statSync(fullPath);
|
|
110
|
+
if (stat.isDirectory()) {
|
|
111
|
+
if (hasMdFiles(fullPath)) return true;
|
|
112
|
+
} else if (MD_EXTENSIONS.has(extname(entry).toLowerCase())) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
function detectDocsDir(cloneDir) {
|
|
121
|
+
const candidates = [
|
|
122
|
+
"docs",
|
|
123
|
+
"documentation",
|
|
124
|
+
"content",
|
|
125
|
+
"pages",
|
|
126
|
+
"src/content",
|
|
127
|
+
"src/pages",
|
|
128
|
+
"guide",
|
|
129
|
+
"guides",
|
|
130
|
+
""
|
|
131
|
+
];
|
|
132
|
+
for (const candidate of candidates) {
|
|
133
|
+
const fullPath = candidate ? join(cloneDir, candidate) : cloneDir;
|
|
134
|
+
try {
|
|
135
|
+
const stat = statSync(fullPath);
|
|
136
|
+
if (stat.isDirectory() && hasMdFiles(fullPath)) {
|
|
137
|
+
return candidate;
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return "";
|
|
143
|
+
}
|
|
144
|
+
function slugifySegment(seg) {
|
|
145
|
+
return seg.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
146
|
+
}
|
|
147
|
+
function derivePageId(relPath) {
|
|
148
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
149
|
+
const parts = normalized.split("/");
|
|
150
|
+
const filename = parts[parts.length - 1];
|
|
151
|
+
const dirs = parts.slice(0, -1);
|
|
152
|
+
const base = basename(filename, extname(filename));
|
|
153
|
+
if (dirs.length === 0 && base.toLowerCase() === "readme") {
|
|
154
|
+
return "introduction";
|
|
155
|
+
}
|
|
156
|
+
if (base.toLowerCase() === "index") {
|
|
157
|
+
if (dirs.length === 0) return "introduction";
|
|
158
|
+
return dirs.map(slugifySegment).join("/");
|
|
159
|
+
}
|
|
160
|
+
return [...dirs, base].map(slugifySegment).join("/");
|
|
161
|
+
}
|
|
162
|
+
var I18N_DIR_PREFIXES = /* @__PURE__ */ new Set(["fr", "es", "de", "ja", "ko", "zh", "pt", "it", "ru", "ar", "nl", "pl", "tr", "vi", "th", "id", "hi", "uk", "cs", "sv", "da", "fi", "no", "he", "ro", "hu", "el", "bg", "sk", "sl", "hr", "lt", "lv", "et", "ms", "fil", "bn", "ta", "te", "mr", "gu", "kn", "ml", "pa", "ur", "fa", "sw"]);
|
|
163
|
+
var ASSET_DIRS = /* @__PURE__ */ new Set(["images", "img", "assets", "static", "public", "media"]);
|
|
164
|
+
function scanDir(dir, baseDir, primaryOnly, results, skipDirs) {
|
|
165
|
+
let entries;
|
|
166
|
+
try {
|
|
167
|
+
entries = readdirSync(dir);
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (entry.startsWith("_") || entry.startsWith(".") || entry === "node_modules") continue;
|
|
173
|
+
if (ASSET_DIRS.has(entry.toLowerCase())) continue;
|
|
174
|
+
if (skipDirs && skipDirs.has(entry.toLowerCase())) continue;
|
|
175
|
+
const fullPath = join(dir, entry);
|
|
176
|
+
let stat;
|
|
177
|
+
try {
|
|
178
|
+
stat = statSync(fullPath);
|
|
179
|
+
} catch {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (stat.isDirectory()) {
|
|
183
|
+
scanDir(fullPath, baseDir, primaryOnly, results, skipDirs);
|
|
184
|
+
} else {
|
|
185
|
+
const ext = extname(entry).toLowerCase();
|
|
186
|
+
const validExt = primaryOnly ? MD_EXTENSIONS.has(ext) : ALL_DOC_EXTENSIONS.has(ext);
|
|
187
|
+
if (!validExt) continue;
|
|
188
|
+
const relPath = relative(baseDir, fullPath);
|
|
189
|
+
const pageId = derivePageId(relPath);
|
|
190
|
+
results.push({ absPath: fullPath, relPath, pageId, ext });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function findDocFiles(cloneDir, docsDir, skipI18n = false) {
|
|
195
|
+
const baseDir = docsDir ? join(cloneDir, docsDir) : cloneDir;
|
|
196
|
+
const skipDirs = skipI18n ? I18N_DIR_PREFIXES : void 0;
|
|
197
|
+
const primaryResults = [];
|
|
198
|
+
scanDir(baseDir, baseDir, true, primaryResults, skipDirs);
|
|
199
|
+
if (primaryResults.length > 0) return primaryResults;
|
|
200
|
+
const allResults = [];
|
|
201
|
+
scanDir(baseDir, baseDir, false, allResults, skipDirs);
|
|
202
|
+
return allResults;
|
|
203
|
+
}
|
|
204
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico", ".bmp", ".avif"]);
|
|
205
|
+
var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([...IMAGE_EXTENSIONS, ".mp4", ".webm", ".mp3", ".pdf"]);
|
|
206
|
+
function scanAssets(dir, baseDir, results) {
|
|
207
|
+
let entries;
|
|
208
|
+
try {
|
|
209
|
+
entries = readdirSync(dir);
|
|
210
|
+
} catch {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
215
|
+
const fullPath = join(dir, entry);
|
|
216
|
+
let stat;
|
|
217
|
+
try {
|
|
218
|
+
stat = statSync(fullPath);
|
|
219
|
+
} catch {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (stat.isDirectory()) {
|
|
223
|
+
scanAssets(fullPath, baseDir, results);
|
|
224
|
+
} else {
|
|
225
|
+
const ext = extname(entry).toLowerCase();
|
|
226
|
+
if (!ASSET_EXTENSIONS.has(ext)) continue;
|
|
227
|
+
results.push({ absPath: fullPath, relPath: relative(baseDir, fullPath) });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function copyStaticAssets(cloneDir, docsDir, targetPublicDir) {
|
|
232
|
+
const baseDir = docsDir ? join(cloneDir, docsDir) : cloneDir;
|
|
233
|
+
const assetRoots = [];
|
|
234
|
+
for (const name of ASSET_DIRS) {
|
|
235
|
+
const candidate = join(baseDir, name);
|
|
236
|
+
if (existsSync(candidate)) assetRoots.push(candidate);
|
|
237
|
+
}
|
|
238
|
+
if (assetRoots.length === 0) return 0;
|
|
239
|
+
const assets = [];
|
|
240
|
+
for (const root of assetRoots) {
|
|
241
|
+
scanAssets(root, baseDir, assets);
|
|
242
|
+
}
|
|
243
|
+
for (const asset of assets) {
|
|
244
|
+
const dest = join(targetPublicDir, asset.relPath);
|
|
245
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
246
|
+
copyFileSync(asset.absPath, dest);
|
|
247
|
+
}
|
|
248
|
+
return assets.length;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/migrate/importer.ts
|
|
252
|
+
import { readFileSync } from "fs";
|
|
253
|
+
import { basename as basename2, extname as extname2 } from "path";
|
|
254
|
+
import matter from "gray-matter";
|
|
255
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
256
|
+
function titleFromFilename(relPath) {
|
|
257
|
+
const filename = relPath.replace(/\\/g, "/").split("/").pop() ?? relPath;
|
|
258
|
+
const base = basename2(filename, extname2(filename));
|
|
259
|
+
return base.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
260
|
+
}
|
|
261
|
+
function extractFirstParagraph(content) {
|
|
262
|
+
for (const line of content.split("\n")) {
|
|
263
|
+
const trimmed = line.trim();
|
|
264
|
+
if (!trimmed) continue;
|
|
265
|
+
if (trimmed.startsWith("#")) continue;
|
|
266
|
+
if (trimmed.startsWith("```") || trimmed.startsWith(":::") || trimmed.startsWith("<")) continue;
|
|
267
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("export ")) continue;
|
|
268
|
+
return trimmed.slice(0, 200);
|
|
269
|
+
}
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
function normalizeComponents(body) {
|
|
273
|
+
let result = body;
|
|
274
|
+
const importedComponents = /* @__PURE__ */ new Set();
|
|
275
|
+
result = result.replace(
|
|
276
|
+
/^import\s+(\w+|\{[^}]+\})\s+from\s+['"][^'"]+['"]\s*;?\s*$/gm,
|
|
277
|
+
(_, imported) => {
|
|
278
|
+
const name = imported.trim();
|
|
279
|
+
if (/^[A-Z]\w*$/.test(name)) importedComponents.add(name);
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
for (const name of importedComponents) {
|
|
284
|
+
result = result.replace(
|
|
285
|
+
new RegExp(`<${name}(?:\\s[^>]*)?\\/>`, "gm"),
|
|
286
|
+
`{/* <${name} /> \u2014 imported snippet component */}`
|
|
287
|
+
);
|
|
288
|
+
result = result.replace(
|
|
289
|
+
new RegExp(`<${name}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${name}>`, "g"),
|
|
290
|
+
`{/* <${name}> \u2014 imported snippet component */}`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
result = result.replace(/<!--([\s\S]*?)-->/g, (_, inner) => `{/*${inner}*/}`);
|
|
294
|
+
result = result.replace(/<Tip>([\s\S]*?)<\/Tip>/g, (_, c) => `<Note>${c}</Note>`);
|
|
295
|
+
result = result.replace(/<Check>([\s\S]*?)<\/Check>/g, (_, c) => `<Note>${c}</Note>`);
|
|
296
|
+
result = result.replace(/<Danger>([\s\S]*?)<\/Danger>/g, (_, c) => `<Error>${c}</Error>`);
|
|
297
|
+
result = result.replace(/<Callout(?:\s[^>]*)?>([\s\S]*?)<\/Callout>/g, (_, c) => `<Note>${c}</Note>`);
|
|
298
|
+
result = result.replace(/:::(\w+)(?:\s+[^\n]*)?\n([\s\S]*?):::/g, (_, type, content) => {
|
|
299
|
+
const tag = mapAdmonitionToDoxTag(type.toLowerCase());
|
|
300
|
+
return `<${tag}>
|
|
301
|
+
${content.trim()}
|
|
302
|
+
</${tag}>`;
|
|
303
|
+
});
|
|
304
|
+
result = result.replace(
|
|
305
|
+
/\{%\s*hint\s+style="(\w+)"\s*%\}([\s\S]*?)\{%\s*endhint\s*%\}/g,
|
|
306
|
+
(_, style, content) => {
|
|
307
|
+
const tag = mapGitBookStyleToDoxTag(style.toLowerCase());
|
|
308
|
+
return `<${tag}>
|
|
309
|
+
${content.trim()}
|
|
310
|
+
</${tag}>`;
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
result = result.replace(/<AccordionGroup[^>]*>\n?([\s\S]*?)\n?<\/AccordionGroup>/g, (_, inner) => inner.trim());
|
|
314
|
+
result = result.replace(/<Expandable(\s[^>]*)?>/g, (_, attrs = "") => {
|
|
315
|
+
const title = attrs.match(/title="([^"]*)"/)?.[1] ?? "Details";
|
|
316
|
+
return `<Accordion title="${title}">`;
|
|
317
|
+
});
|
|
318
|
+
result = result.replace(/<\/Expandable>/g, "</Accordion>");
|
|
319
|
+
result = result.replace(/<Latex>([\s\S]*?)<\/Latex>/g, (_, inner) => `\`${inner.trim()}\``);
|
|
320
|
+
result = result.replace(/<(?:ResponseField|ParamField)([^>]*)>/g, (_, attrs) => {
|
|
321
|
+
const name = attrs.match(/name="([^"]*)"/)?.[1] ?? "";
|
|
322
|
+
const type = attrs.match(/type="([^"]*)"/)?.[1] ?? "";
|
|
323
|
+
const required = /\brequired\b/.test(attrs);
|
|
324
|
+
const def = attrs.match(/default="([^"]*)"/)?.[1];
|
|
325
|
+
const deprecated = /\bdeprecated\b/.test(attrs);
|
|
326
|
+
const meta = [
|
|
327
|
+
type && `\`${type}\``,
|
|
328
|
+
required && "*(required)*",
|
|
329
|
+
deprecated && "*(deprecated)*",
|
|
330
|
+
def !== void 0 && `*(default: \`${def}\`)*`
|
|
331
|
+
].filter(Boolean).join(" ");
|
|
332
|
+
return `
|
|
333
|
+
**\`${name}\`** ${meta}
|
|
334
|
+
|
|
335
|
+
`;
|
|
336
|
+
});
|
|
337
|
+
result = result.replace(/<\/(?:ResponseField|ParamField)>/g, "\n");
|
|
338
|
+
result = result.replace(/<RequestExample[^>]*>/g, "<CodeGroup>");
|
|
339
|
+
result = result.replace(/<\/RequestExample>/g, "</CodeGroup>");
|
|
340
|
+
result = result.replace(/<ResponseExample[^>]*>/g, "<CodeGroup>");
|
|
341
|
+
result = result.replace(/<\/ResponseExample>/g, "</CodeGroup>");
|
|
342
|
+
result = result.replace(/<Panel[^>]*>([\s\S]*?)<\/Panel>/g, (_, inner) => inner.trim());
|
|
343
|
+
result = result.replace(/<Badge[^>]*>([\s\S]*?)<\/Badge>/g, (_, inner) => `**${inner.trim()}**`);
|
|
344
|
+
result = result.replace(/<Tile(\s[^>]*)?>/g, (_, attrs = "") => `<Card${attrs}>`);
|
|
345
|
+
result = result.replace(/<\/Tile>/g, "</Card>");
|
|
346
|
+
result = result.replace(/<View(\s[^>]*)?>/g, (_, attrs = "") => {
|
|
347
|
+
const title = attrs.match(/title="([^"]*)"/)?.[1] ?? "View";
|
|
348
|
+
return `<Tab title="${title}">`;
|
|
349
|
+
});
|
|
350
|
+
result = result.replace(/<\/View>/g, "</Tab>");
|
|
351
|
+
result = result.replace(/<Update(\s[^>]*)?>/g, (_, attrs = "") => {
|
|
352
|
+
const label = attrs.match(/label="([^"]*)"/)?.[1] ?? "";
|
|
353
|
+
const desc = attrs.match(/description="([^"]*)"/)?.[1] ?? "";
|
|
354
|
+
return `## ${label}${desc ? `
|
|
355
|
+
|
|
356
|
+
*${desc}*` : ""}
|
|
357
|
+
|
|
358
|
+
`;
|
|
359
|
+
});
|
|
360
|
+
result = result.replace(/<\/Update>/g, "\n");
|
|
361
|
+
result = result.replace(/<Prompt[^>]*>([\s\S]*?)<\/Prompt>/g, (_, inner) => {
|
|
362
|
+
return `\`\`\`text
|
|
363
|
+
${inner.trim()}
|
|
364
|
+
\`\`\``;
|
|
365
|
+
});
|
|
366
|
+
result = result.replace(/<Tree[^>]*>/g, "```\n");
|
|
367
|
+
result = result.replace(/<\/Tree>/g, "\n```");
|
|
368
|
+
result = result.replace(/<Tree\.Folder[^>]*name="([^"]*)"[^>]*>/g, (_, name) => `\u{1F4C1} ${name}/
|
|
369
|
+
`);
|
|
370
|
+
result = result.replace(/<\/Tree\.Folder>/g, "");
|
|
371
|
+
result = result.replace(new RegExp('<Tree\\.File[^>]*name="([^"]*)"[^>]*/>', "g"), (_, name) => ` ${name}
|
|
372
|
+
`);
|
|
373
|
+
result = result.replace(/<Color[^>]*>/g, "| Name | Value |\n|---|---|\n");
|
|
374
|
+
result = result.replace(/<\/Color>/g, "");
|
|
375
|
+
result = result.replace(/<Color\.Row[^>]*title="([^"]*)"[^>]*>/g, (_, title) => `**${title}**
|
|
376
|
+
`);
|
|
377
|
+
result = result.replace(/<\/Color\.Row>/g, "");
|
|
378
|
+
result = result.replace(
|
|
379
|
+
/<Color\.Item[^>]*name="([^"]*)"[^>]*value="([^"]*)"[^>]*\/>/g,
|
|
380
|
+
(_, name, value) => `| ${name} | \`${value}\` |
|
|
381
|
+
`
|
|
382
|
+
);
|
|
383
|
+
result = result.replace(/<Banner[^>]*>([\s\S]*?)<\/Banner>/g, "");
|
|
384
|
+
result = result.replace(/<Banner[^>]*\/>/g, "");
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
function mapAdmonitionToDoxTag(type) {
|
|
388
|
+
if (type === "warning" || type === "caution") return "Warning";
|
|
389
|
+
if (type === "danger") return "Error";
|
|
390
|
+
if (type === "info") return "Info";
|
|
391
|
+
return "Note";
|
|
392
|
+
}
|
|
393
|
+
function mapGitBookStyleToDoxTag(style) {
|
|
394
|
+
if (style === "warning") return "Warning";
|
|
395
|
+
if (style === "danger") return "Error";
|
|
396
|
+
if (style === "success") return "Note";
|
|
397
|
+
return "Info";
|
|
398
|
+
}
|
|
399
|
+
var RST_SYSTEM_PROMPT = `You are a documentation converter. Convert the given file content to clean MDX.
|
|
400
|
+
Respond with ONLY valid JSON \u2014 no prose, no markdown fences:
|
|
401
|
+
{
|
|
402
|
+
"frontmatter": { "title": "string", "description": "string", "keywords": ["..."] },
|
|
403
|
+
"body": "string \u2014 full MDX body"
|
|
404
|
+
}
|
|
405
|
+
Rules: preserve code blocks with language hints; convert tables to Markdown; convert callout
|
|
406
|
+
boxes to <Note> or <Warning>; preserve heading hierarchy; do not include page title as a heading.`;
|
|
407
|
+
function parseClaudeResponse(text) {
|
|
408
|
+
try {
|
|
409
|
+
return JSON.parse(text);
|
|
410
|
+
} catch {
|
|
411
|
+
const stripped = text.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
|
|
412
|
+
return JSON.parse(stripped);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function importFile(file, apiKey) {
|
|
416
|
+
const ext = file.ext.toLowerCase();
|
|
417
|
+
if (ext === ".md" || ext === ".mdx") {
|
|
418
|
+
const raw = readFileSync(file.absPath, "utf8");
|
|
419
|
+
const parsed = matter(raw);
|
|
420
|
+
const fmTitle = parsed.data.title ?? "";
|
|
421
|
+
const fmDesc = parsed.data.description ?? "";
|
|
422
|
+
const fmKeywords = parsed.data.keywords;
|
|
423
|
+
const title = fmTitle || titleFromFilename(file.relPath);
|
|
424
|
+
const description = fmDesc || extractFirstParagraph(parsed.content);
|
|
425
|
+
const keywords = Array.isArray(fmKeywords) ? fmKeywords : [];
|
|
426
|
+
const openapi = parsed.data.openapi;
|
|
427
|
+
const body = normalizeComponents(parsed.content);
|
|
428
|
+
if (openapi && !body.trim()) return null;
|
|
429
|
+
return { pageId: file.pageId, frontmatter: { title, description, keywords }, body };
|
|
430
|
+
}
|
|
431
|
+
if (!apiKey) {
|
|
432
|
+
throw new Error(`Skipping non-Markdown file (no API key): ${file.relPath}`);
|
|
433
|
+
}
|
|
434
|
+
const content = readFileSync(file.absPath, "utf8");
|
|
435
|
+
const client = new Anthropic({ apiKey });
|
|
436
|
+
const message = await client.messages.create({
|
|
437
|
+
model: "claude-sonnet-4-6",
|
|
438
|
+
max_tokens: 4096,
|
|
439
|
+
system: RST_SYSTEM_PROMPT,
|
|
440
|
+
messages: [
|
|
441
|
+
{
|
|
442
|
+
role: "user",
|
|
443
|
+
content: `Convert this documentation file to MDX.
|
|
444
|
+
|
|
445
|
+
File: ${file.relPath}
|
|
446
|
+
|
|
447
|
+
Content:
|
|
448
|
+
${content.slice(0, 8e4)}`
|
|
449
|
+
}
|
|
450
|
+
]
|
|
451
|
+
});
|
|
452
|
+
const responseText = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
453
|
+
const claudeResult = parseClaudeResponse(responseText);
|
|
454
|
+
return {
|
|
455
|
+
pageId: file.pageId,
|
|
456
|
+
frontmatter: claudeResult.frontmatter,
|
|
457
|
+
body: claudeResult.body
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/migrate/nav-builder.ts
|
|
462
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
463
|
+
import { join as join2 } from "path";
|
|
464
|
+
function titleCase(str) {
|
|
465
|
+
return str.split("-").map((word) => {
|
|
466
|
+
if (word.toLowerCase() === "api") return "API";
|
|
467
|
+
if (word.toLowerCase() === "sdk") return "SDK";
|
|
468
|
+
if (word.toLowerCase() === "cli") return "CLI";
|
|
469
|
+
if (word.toLowerCase() === "ui") return "UI";
|
|
470
|
+
if (word.toLowerCase() === "faq") return "FAQ";
|
|
471
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
472
|
+
}).join(" ");
|
|
473
|
+
}
|
|
474
|
+
function buildNavStructure(pages) {
|
|
475
|
+
const seen = /* @__PURE__ */ new Set();
|
|
476
|
+
const ordered = [];
|
|
477
|
+
for (const p of pages) {
|
|
478
|
+
if (!seen.has(p.pageId)) {
|
|
479
|
+
seen.add(p.pageId);
|
|
480
|
+
ordered.push(p.pageId);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const depth1Segments = /* @__PURE__ */ new Set();
|
|
484
|
+
for (const id of ordered) {
|
|
485
|
+
const parts = id.split("/");
|
|
486
|
+
if (parts.length > 1) {
|
|
487
|
+
depth1Segments.add(parts[0]);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const rootOnlyPages = ordered.filter((id) => !id.includes("/"));
|
|
491
|
+
const useSingleTab = depth1Segments.size === 0 || depth1Segments.size === 1 && rootOnlyPages.length === 0;
|
|
492
|
+
let tabs;
|
|
493
|
+
if (useSingleTab) {
|
|
494
|
+
const groups = buildGroups(ordered, null);
|
|
495
|
+
tabs = [{ tab: "Overview", groups }];
|
|
496
|
+
} else {
|
|
497
|
+
tabs = [];
|
|
498
|
+
if (rootOnlyPages.length > 0) {
|
|
499
|
+
const groups = buildGroups(rootOnlyPages, null);
|
|
500
|
+
tabs.push({ tab: "Overview", groups });
|
|
501
|
+
}
|
|
502
|
+
for (const seg of depth1Segments) {
|
|
503
|
+
const tabPages = ordered.filter((id) => id.startsWith(seg + "/") || id === seg);
|
|
504
|
+
const groups = buildGroups(tabPages, seg);
|
|
505
|
+
tabs.push({ tab: titleCase(seg), groups });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
tabs.push({ tab: "Changelog", href: "/changelog" });
|
|
509
|
+
return { tabs };
|
|
510
|
+
}
|
|
511
|
+
function buildGroups(pageIds, tabSegment) {
|
|
512
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
513
|
+
for (const id of pageIds) {
|
|
514
|
+
let groupName;
|
|
515
|
+
if (tabSegment === null) {
|
|
516
|
+
groupName = "Overview";
|
|
517
|
+
} else {
|
|
518
|
+
const rel = id.startsWith(tabSegment + "/") ? id.slice(tabSegment.length + 1) : id;
|
|
519
|
+
const relParts = rel.split("/");
|
|
520
|
+
groupName = relParts.length === 1 ? titleCase(tabSegment) : titleCase(relParts[0]);
|
|
521
|
+
}
|
|
522
|
+
if (!groupMap.has(groupName)) groupMap.set(groupName, []);
|
|
523
|
+
groupMap.get(groupName).push(id);
|
|
524
|
+
}
|
|
525
|
+
const groups = [];
|
|
526
|
+
for (const [groupName, groupPages] of groupMap) {
|
|
527
|
+
const sorted = [...groupPages];
|
|
528
|
+
const introIdx = sorted.indexOf("introduction");
|
|
529
|
+
if (introIdx > 0) {
|
|
530
|
+
sorted.splice(introIdx, 1);
|
|
531
|
+
sorted.unshift("introduction");
|
|
532
|
+
}
|
|
533
|
+
groups.push({ group: groupName, pages: sorted });
|
|
534
|
+
}
|
|
535
|
+
return groups;
|
|
536
|
+
}
|
|
537
|
+
function detectPlatform(cloneDir) {
|
|
538
|
+
if (existsSync2(join2(cloneDir, "mint.json"))) return "mintlify";
|
|
539
|
+
if (existsSync2(join2(cloneDir, "docs.json"))) {
|
|
540
|
+
try {
|
|
541
|
+
const parsed = JSON.parse(readFileSync2(join2(cloneDir, "docs.json"), "utf8"));
|
|
542
|
+
if (Array.isArray(parsed.tabs)) return "dox";
|
|
543
|
+
const schema = parsed.$schema;
|
|
544
|
+
if (schema?.includes("mintlify") || "navigation" in parsed) return "mintlify";
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (existsSync2(join2(cloneDir, "docusaurus.config.js")) || existsSync2(join2(cloneDir, "docusaurus.config.ts")) || existsSync2(join2(cloneDir, "docusaurus.config.mjs"))) return "docusaurus";
|
|
549
|
+
if (existsSync2(join2(cloneDir, "SUMMARY.md"))) return "gitbook";
|
|
550
|
+
if (existsSync2(join2(cloneDir, ".vitepress"))) return "vitepress";
|
|
551
|
+
if (existsSync2(join2(cloneDir, "astro.config.mjs")) || existsSync2(join2(cloneDir, "astro.config.ts"))) return "starlight";
|
|
552
|
+
if (existsSync2(join2(cloneDir, "_meta.json")) || existsSync2(join2(cloneDir, "pages", "_meta.json"))) return "nextra";
|
|
553
|
+
return "unknown";
|
|
554
|
+
}
|
|
555
|
+
function slugify(s) {
|
|
556
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
557
|
+
}
|
|
558
|
+
function normalizePageRef(ref, docsDir) {
|
|
559
|
+
let r = ref;
|
|
560
|
+
if (docsDir && r.startsWith(docsDir + "/")) r = r.slice(docsDir.length + 1);
|
|
561
|
+
r = r.replace(/\.(mdx?|rst|txt)$/, "");
|
|
562
|
+
const parts = r.split("/");
|
|
563
|
+
const last = parts[parts.length - 1].toLowerCase();
|
|
564
|
+
if (last === "index" || last === "readme") {
|
|
565
|
+
return parts.length === 1 ? "introduction" : parts.slice(0, -1).map(slugify).join("/");
|
|
566
|
+
}
|
|
567
|
+
return parts.map(slugify).join("/");
|
|
568
|
+
}
|
|
569
|
+
function convertMintTabs(tabs, docsDir) {
|
|
570
|
+
if (tabs.length === 0) return null;
|
|
571
|
+
function convertPageRef(page) {
|
|
572
|
+
if (typeof page === "string") return normalizePageRef(page, docsDir);
|
|
573
|
+
if (page !== null && typeof page === "object" && "group" in page && "pages" in page) {
|
|
574
|
+
const p = page;
|
|
575
|
+
const pages = [];
|
|
576
|
+
if (typeof p.root === "string") {
|
|
577
|
+
pages.push(normalizePageRef(p.root, docsDir));
|
|
578
|
+
}
|
|
579
|
+
pages.push(...(p.pages ?? []).map(convertPageRef));
|
|
580
|
+
const group = { group: String(p.group), pages };
|
|
581
|
+
if (typeof p.icon === "string") group.icon = p.icon;
|
|
582
|
+
return group;
|
|
583
|
+
}
|
|
584
|
+
return String(page);
|
|
585
|
+
}
|
|
586
|
+
const resultTabs = tabs.map((item) => {
|
|
587
|
+
const tabName = String(item.tab);
|
|
588
|
+
if (item.href) return { tab: tabName, href: String(item.href) };
|
|
589
|
+
if (tabName.toLowerCase() === "changelog") return { tab: "Changelog", href: "/changelog" };
|
|
590
|
+
const groups = (item.groups ?? []).map((g) => {
|
|
591
|
+
const group = {
|
|
592
|
+
group: String(g.group),
|
|
593
|
+
pages: (g.pages ?? []).map(convertPageRef)
|
|
594
|
+
};
|
|
595
|
+
if (typeof g.icon === "string") group.icon = g.icon;
|
|
596
|
+
return group;
|
|
597
|
+
});
|
|
598
|
+
return { tab: tabName, groups };
|
|
599
|
+
});
|
|
600
|
+
if (!resultTabs.some((t) => t.tab === "Changelog")) {
|
|
601
|
+
resultTabs.push({ tab: "Changelog", href: "/changelog" });
|
|
602
|
+
}
|
|
603
|
+
return { tabs: resultTabs };
|
|
604
|
+
}
|
|
605
|
+
function parseMintConfig(config, docsDir) {
|
|
606
|
+
const nav = config.navigation;
|
|
607
|
+
if (nav && typeof nav === "object" && !Array.isArray(nav)) {
|
|
608
|
+
const navObj = nav;
|
|
609
|
+
if (Array.isArray(navObj.languages) && navObj.languages.length > 0) {
|
|
610
|
+
const langs = navObj.languages;
|
|
611
|
+
const enLang = langs.find((l) => l.language === "en") ?? langs[0];
|
|
612
|
+
const langTabs = enLang.tabs;
|
|
613
|
+
if (Array.isArray(langTabs) && langTabs.length > 0) return convertMintTabs(langTabs, docsDir);
|
|
614
|
+
}
|
|
615
|
+
const v3Tabs = navObj.tabs;
|
|
616
|
+
if (Array.isArray(v3Tabs) && v3Tabs.length > 0) return convertMintTabs(v3Tabs, docsDir);
|
|
617
|
+
}
|
|
618
|
+
if (!Array.isArray(nav) || nav.length === 0) return null;
|
|
619
|
+
if ("tab" in nav[0]) return convertMintTabs(nav, docsDir);
|
|
620
|
+
function convertPageRef(page) {
|
|
621
|
+
if (typeof page === "string") return normalizePageRef(page, docsDir);
|
|
622
|
+
if (page !== null && typeof page === "object" && "group" in page && "pages" in page) {
|
|
623
|
+
const p = page;
|
|
624
|
+
return { group: String(p.group), pages: (p.pages ?? []).map(convertPageRef) };
|
|
625
|
+
}
|
|
626
|
+
return String(page);
|
|
627
|
+
}
|
|
628
|
+
const groups = nav.map((item) => ({
|
|
629
|
+
group: String(item.group ?? ""),
|
|
630
|
+
pages: (item.pages ?? []).map(convertPageRef)
|
|
631
|
+
}));
|
|
632
|
+
return { tabs: [{ tab: "Docs", groups }, { tab: "Changelog", href: "/changelog" }] };
|
|
633
|
+
}
|
|
634
|
+
function parseGitBookSummary(cloneDir, docsDir) {
|
|
635
|
+
const candidates = [join2(cloneDir, "SUMMARY.md")];
|
|
636
|
+
if (docsDir) candidates.push(join2(cloneDir, docsDir, "SUMMARY.md"));
|
|
637
|
+
let raw = "";
|
|
638
|
+
for (const p of candidates) {
|
|
639
|
+
if (existsSync2(p)) {
|
|
640
|
+
raw = readFileSync2(p, "utf8");
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (!raw) return null;
|
|
645
|
+
const groups = [];
|
|
646
|
+
let currentGroupName = "Overview";
|
|
647
|
+
let currentPages = [];
|
|
648
|
+
for (const line of raw.split("\n")) {
|
|
649
|
+
const groupMatch = line.match(/^##\s+(.+)/);
|
|
650
|
+
if (groupMatch) {
|
|
651
|
+
if (currentPages.length > 0) groups.push({ group: currentGroupName, pages: currentPages });
|
|
652
|
+
currentGroupName = groupMatch[1].trim();
|
|
653
|
+
currentPages = [];
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const pageMatch = line.match(/^\*\s+\[.+?\]\((.+?)\)/);
|
|
657
|
+
if (pageMatch) {
|
|
658
|
+
const ref = pageMatch[1].trim();
|
|
659
|
+
if (ref.startsWith("http")) continue;
|
|
660
|
+
currentPages.push(normalizePageRef(ref, docsDir));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (currentPages.length > 0) groups.push({ group: currentGroupName, pages: currentPages });
|
|
664
|
+
if (groups.length === 0) return null;
|
|
665
|
+
return { tabs: [{ tab: "Docs", groups }, { tab: "Changelog", href: "/changelog" }] };
|
|
666
|
+
}
|
|
667
|
+
function parseNextraMeta(cloneDir, docsDir) {
|
|
668
|
+
const baseDir = docsDir ? join2(cloneDir, docsDir) : cloneDir;
|
|
669
|
+
const metaPath = join2(baseDir, "_meta.json");
|
|
670
|
+
if (!existsSync2(metaPath)) return null;
|
|
671
|
+
try {
|
|
672
|
+
const meta = JSON.parse(readFileSync2(metaPath, "utf8"));
|
|
673
|
+
const pages = [];
|
|
674
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
675
|
+
if (typeof value === "object" && value !== null) {
|
|
676
|
+
const v = value;
|
|
677
|
+
if (v.type === "separator" || v.type === "menu") continue;
|
|
678
|
+
}
|
|
679
|
+
pages.push(key === "index" ? "introduction" : slugify(key));
|
|
680
|
+
}
|
|
681
|
+
if (pages.length === 0) return null;
|
|
682
|
+
return {
|
|
683
|
+
tabs: [
|
|
684
|
+
{ tab: "Docs", groups: [{ group: "Overview", pages }] },
|
|
685
|
+
{ tab: "Changelog", href: "/changelog" }
|
|
686
|
+
]
|
|
687
|
+
};
|
|
688
|
+
} catch {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
var PLATFORM_LABELS = {
|
|
693
|
+
mintlify: "Mintlify",
|
|
694
|
+
docusaurus: "Docusaurus",
|
|
695
|
+
gitbook: "GitBook",
|
|
696
|
+
nextra: "Nextra",
|
|
697
|
+
vitepress: "VitePress",
|
|
698
|
+
starlight: "Starlight (Astro)",
|
|
699
|
+
dox: "Dox",
|
|
700
|
+
unknown: "unknown"
|
|
701
|
+
};
|
|
702
|
+
function detectNavFromConfig(cloneDir, docsDir, platform) {
|
|
703
|
+
const detected = platform ?? detectPlatform(cloneDir);
|
|
704
|
+
const label = PLATFORM_LABELS[detected];
|
|
705
|
+
switch (detected) {
|
|
706
|
+
case "dox": {
|
|
707
|
+
try {
|
|
708
|
+
const parsed = JSON.parse(
|
|
709
|
+
readFileSync2(join2(cloneDir, "docs.json"), "utf8")
|
|
710
|
+
);
|
|
711
|
+
console.log(` \u{1F4CB} Detected ${label} \u2014 using docs.json navigation as-is`);
|
|
712
|
+
return parsed;
|
|
713
|
+
} catch {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
case "mintlify": {
|
|
718
|
+
for (const file of ["docs.json", "mint.json"]) {
|
|
719
|
+
const p = join2(cloneDir, file);
|
|
720
|
+
if (!existsSync2(p)) continue;
|
|
721
|
+
try {
|
|
722
|
+
const config = JSON.parse(readFileSync2(p, "utf8"));
|
|
723
|
+
const nav = parseMintConfig(config, docsDir);
|
|
724
|
+
if (nav) {
|
|
725
|
+
console.log(` \u{1F4CB} Detected ${label} (${file}) \u2014 converting navigation`);
|
|
726
|
+
return nav;
|
|
727
|
+
}
|
|
728
|
+
} catch {
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
case "gitbook": {
|
|
734
|
+
const nav = parseGitBookSummary(cloneDir, docsDir);
|
|
735
|
+
if (nav) console.log(` \u{1F4CB} Detected ${label} (SUMMARY.md) \u2014 converting navigation`);
|
|
736
|
+
return nav;
|
|
737
|
+
}
|
|
738
|
+
case "nextra": {
|
|
739
|
+
const nav = parseNextraMeta(cloneDir, docsDir);
|
|
740
|
+
if (nav) console.log(` \u{1F4CB} Detected ${label} (_meta.json) \u2014 converting navigation`);
|
|
741
|
+
return nav;
|
|
742
|
+
}
|
|
743
|
+
case "docusaurus":
|
|
744
|
+
case "vitepress":
|
|
745
|
+
case "starlight":
|
|
746
|
+
console.log(` \u{1F4CB} Detected ${label} \u2014 nav config is JavaScript, using directory structure`);
|
|
747
|
+
return null;
|
|
748
|
+
default:
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/migrate/index.ts
|
|
754
|
+
function readDocsJson(projectDir) {
|
|
755
|
+
const docsPath = join3(projectDir, "docs.json");
|
|
756
|
+
const raw = readFileSync3(docsPath, "utf8");
|
|
757
|
+
return JSON.parse(raw);
|
|
758
|
+
}
|
|
759
|
+
function writeDocsJson(projectDir, config) {
|
|
760
|
+
const docsPath = join3(projectDir, "docs.json");
|
|
761
|
+
writeFileSync(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
762
|
+
}
|
|
763
|
+
function mergeDocsJson(existing, incoming) {
|
|
764
|
+
const existingTabNames = new Set(existing.tabs.map((t) => t.tab));
|
|
765
|
+
const merged = { tabs: [...existing.tabs.filter((t) => t.tab !== "Changelog")] };
|
|
766
|
+
for (const tab of incoming.tabs) {
|
|
767
|
+
if (tab.tab === "Changelog") continue;
|
|
768
|
+
if (existingTabNames.has(tab.tab)) {
|
|
769
|
+
const existingTab = merged.tabs.find((t) => t.tab === tab.tab);
|
|
770
|
+
if (existingTab.groups && tab.groups) {
|
|
771
|
+
const existingGroupNames = new Set(existingTab.groups.map((g) => g.group));
|
|
772
|
+
for (const group of tab.groups) {
|
|
773
|
+
if (existingGroupNames.has(group.group)) {
|
|
774
|
+
const eg = existingTab.groups.find((g) => g.group === group.group);
|
|
775
|
+
const existingPageSet = new Set(eg.pages.map((p) => typeof p === "string" ? p : p.group));
|
|
776
|
+
for (const page of group.pages) {
|
|
777
|
+
const key = typeof page === "string" ? page : page.group;
|
|
778
|
+
if (!existingPageSet.has(key)) eg.pages.push(page);
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
existingTab.groups.push(group);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
} else if (tab.groups) {
|
|
785
|
+
existingTab.groups = tab.groups;
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
merged.tabs.push(tab);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
merged.tabs.push({ tab: "Changelog", href: "/changelog" });
|
|
792
|
+
return merged;
|
|
793
|
+
}
|
|
794
|
+
function injectApiTab(config, specFilename) {
|
|
795
|
+
const tabs = [...config.tabs];
|
|
796
|
+
const existingApiIdx = tabs.findIndex((t) => t.tab.toLowerCase().includes("api"));
|
|
797
|
+
if (existingApiIdx >= 0) {
|
|
798
|
+
const existing = tabs[existingApiIdx];
|
|
799
|
+
if (existing.groups && existing.groups.length > 0) {
|
|
800
|
+
tabs[existingApiIdx] = {
|
|
801
|
+
...existing,
|
|
802
|
+
api: { source: `/${specFilename}` }
|
|
803
|
+
};
|
|
804
|
+
} else {
|
|
805
|
+
tabs[existingApiIdx] = { tab: existing.tab, api: { source: `/${specFilename}` } };
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
const apiTab = { tab: "API Reference", api: { source: `/${specFilename}` } };
|
|
809
|
+
const changelogIdx = tabs.findIndex((t) => t.tab === "Changelog");
|
|
810
|
+
if (changelogIdx >= 0) {
|
|
811
|
+
tabs.splice(changelogIdx, 0, apiTab);
|
|
812
|
+
} else {
|
|
813
|
+
tabs.push(apiTab);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return { ...config, tabs };
|
|
817
|
+
}
|
|
818
|
+
async function migrateDocs(opts) {
|
|
819
|
+
const { sourceUrl, projectDir: rawProjectDir, into, apiKey, projectName } = opts;
|
|
820
|
+
const projectDir = resolve(rawProjectDir);
|
|
821
|
+
const source = parseGitHubUrl(sourceUrl);
|
|
822
|
+
if (opts.branch) source.branch = opts.branch;
|
|
823
|
+
if (!into) {
|
|
824
|
+
console.log(`
|
|
825
|
+
\u{1F3D7} Scaffolding new project at ${projectDir}...`);
|
|
826
|
+
await scaffold({
|
|
827
|
+
projectDir,
|
|
828
|
+
projectName: projectName ?? "My Docs",
|
|
829
|
+
description: `Documentation migrated from ${source.owner}/${source.repo}`,
|
|
830
|
+
brandPreset: "primary",
|
|
831
|
+
repoUrl: `https://github.com/${source.owner}/${source.repo}`,
|
|
832
|
+
doInstall: false
|
|
833
|
+
});
|
|
834
|
+
} else {
|
|
835
|
+
if (!existsSync3(projectDir)) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
`Project directory "${projectDir}" does not exist. Use without --into to scaffold a new one.`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const tmpBase = mkdtempSync(join3(tmpdir(), "dox-migrate-"));
|
|
842
|
+
const cloneDir = join3(tmpBase, "repo");
|
|
843
|
+
console.log(`
|
|
844
|
+
\u{1F4E6} Cloning ${source.owner}/${source.repo}...`);
|
|
845
|
+
try {
|
|
846
|
+
await cloneRepo(source, cloneDir);
|
|
847
|
+
const platform = detectPlatform(cloneDir);
|
|
848
|
+
let docsDir;
|
|
849
|
+
if (opts.docsDir) {
|
|
850
|
+
docsDir = opts.docsDir;
|
|
851
|
+
} else if (source.docsDir) {
|
|
852
|
+
docsDir = source.docsDir;
|
|
853
|
+
} else if (platform === "mintlify") {
|
|
854
|
+
docsDir = "";
|
|
855
|
+
} else {
|
|
856
|
+
docsDir = detectDocsDir(cloneDir);
|
|
857
|
+
}
|
|
858
|
+
const hasI18n = platform === "mintlify";
|
|
859
|
+
const docFiles = findDocFiles(cloneDir, docsDir, hasI18n);
|
|
860
|
+
const docsDirLabel = docsDir ? `${docsDir}/` : "repo root";
|
|
861
|
+
console.log(` \u{1F4C4} Found ${docFiles.length} files in ${docsDirLabel}`);
|
|
862
|
+
if (docFiles.length === 0) {
|
|
863
|
+
console.warn(" \u26A0 No doc files found. Check the URL and try again.");
|
|
864
|
+
return { pagesWritten: 0, projectDir };
|
|
865
|
+
}
|
|
866
|
+
const detectedNav = detectNavFromConfig(cloneDir, docsDir, platform);
|
|
867
|
+
const openApiSpec = detectOpenApiSpec(cloneDir);
|
|
868
|
+
if (openApiSpec) {
|
|
869
|
+
console.log(` \u{1F50C} Found OpenAPI spec: ${openApiSpec.filename}`);
|
|
870
|
+
}
|
|
871
|
+
const limit = pLimit(5);
|
|
872
|
+
let doneCount = 0;
|
|
873
|
+
const imported = (await Promise.all(
|
|
874
|
+
docFiles.map(
|
|
875
|
+
(file) => limit(async () => {
|
|
876
|
+
try {
|
|
877
|
+
const result = await importFile(file, apiKey);
|
|
878
|
+
doneCount++;
|
|
879
|
+
if (result) {
|
|
880
|
+
console.log(` [${doneCount}/${docFiles.length}] ${result.pageId}`);
|
|
881
|
+
} else {
|
|
882
|
+
console.log(` [${doneCount}/${docFiles.length}] ${file.pageId} (openapi \u2014 wired via spec)`);
|
|
883
|
+
}
|
|
884
|
+
return result;
|
|
885
|
+
} catch (err) {
|
|
886
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
887
|
+
if (msg.includes("no API key")) {
|
|
888
|
+
console.warn(` \u26A0 ${msg}`);
|
|
889
|
+
} else {
|
|
890
|
+
console.warn(` \u26A0 Skipping ${file.relPath}: ${msg}`);
|
|
891
|
+
}
|
|
892
|
+
doneCount++;
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
})
|
|
896
|
+
)
|
|
897
|
+
)).filter(Boolean);
|
|
898
|
+
const pageIdSeen = /* @__PURE__ */ new Set();
|
|
899
|
+
const deduped = imported.filter((p) => {
|
|
900
|
+
if (pageIdSeen.has(p.pageId)) return false;
|
|
901
|
+
pageIdSeen.add(p.pageId);
|
|
902
|
+
return true;
|
|
903
|
+
});
|
|
904
|
+
const contentDir = join3(projectDir, "src", "content");
|
|
905
|
+
let pagesWritten = 0;
|
|
906
|
+
for (const page of deduped) {
|
|
907
|
+
const filePath = join3(contentDir, `${page.pageId}.mdx`);
|
|
908
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
909
|
+
const mdx = [
|
|
910
|
+
"---",
|
|
911
|
+
`title: "${page.frontmatter.title.replace(/"/g, '\\"')}"`,
|
|
912
|
+
`description: "${page.frontmatter.description.replace(/"/g, '\\"')}"`,
|
|
913
|
+
page.frontmatter.keywords.length > 0 ? `keywords: [${page.frontmatter.keywords.map((k) => `"${k.replace(/"/g, '\\"')}"`).join(", ")}]` : null,
|
|
914
|
+
"---",
|
|
915
|
+
"",
|
|
916
|
+
page.body
|
|
917
|
+
].filter((line) => line !== null).join("\n");
|
|
918
|
+
writeFileSync(filePath, mdx, "utf8");
|
|
919
|
+
pagesWritten++;
|
|
920
|
+
}
|
|
921
|
+
const publicDir = join3(projectDir, "public");
|
|
922
|
+
mkdirSync2(publicDir, { recursive: true });
|
|
923
|
+
const assetCount = copyStaticAssets(cloneDir, docsDir, publicDir);
|
|
924
|
+
if (assetCount > 0) {
|
|
925
|
+
console.log(` \u{1F5BC} Copied ${assetCount} static assets \u2192 public/`);
|
|
926
|
+
}
|
|
927
|
+
let finalNav = detectedNav ?? buildNavStructure(deduped);
|
|
928
|
+
if (openApiSpec) {
|
|
929
|
+
copyFileSync2(openApiSpec.absPath, join3(publicDir, openApiSpec.filename));
|
|
930
|
+
console.log(` \u{1F4CB} Copied ${openApiSpec.filename} \u2192 public/${openApiSpec.filename}`);
|
|
931
|
+
finalNav = injectApiTab(finalNav, openApiSpec.filename);
|
|
932
|
+
}
|
|
933
|
+
if (into && existsSync3(join3(projectDir, "docs.json"))) {
|
|
934
|
+
const existing = readDocsJson(projectDir);
|
|
935
|
+
const merged = mergeDocsJson(existing, finalNav);
|
|
936
|
+
writeDocsJson(projectDir, merged);
|
|
937
|
+
} else {
|
|
938
|
+
writeDocsJson(projectDir, finalNav);
|
|
939
|
+
}
|
|
940
|
+
if (!into) {
|
|
941
|
+
installDeps(projectDir);
|
|
942
|
+
initGit(projectDir);
|
|
943
|
+
}
|
|
944
|
+
console.log("");
|
|
945
|
+
console.log(" \u2705 Migration complete!");
|
|
946
|
+
console.log("");
|
|
947
|
+
console.log(` \u{1F4C2} ${projectDir}`);
|
|
948
|
+
console.log(` \u{1F4C4} ${pagesWritten} pages written to src/content/`);
|
|
949
|
+
console.log("");
|
|
950
|
+
if (!into) {
|
|
951
|
+
console.log(" Next steps:");
|
|
952
|
+
console.log("");
|
|
953
|
+
console.log(` cd ${rawProjectDir}`);
|
|
954
|
+
console.log(" npm run dev");
|
|
955
|
+
console.log("");
|
|
956
|
+
}
|
|
957
|
+
return { pagesWritten, projectDir };
|
|
958
|
+
} finally {
|
|
959
|
+
rmSync(tmpBase, { recursive: true, force: true });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
export {
|
|
964
|
+
parseGitHubUrl,
|
|
965
|
+
migrateDocs
|
|
966
|
+
};
|