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
package/dist/index.js
CHANGED
|
@@ -1,350 +1,643 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
migrateDocs,
|
|
4
|
+
parseGitHubUrl
|
|
5
|
+
} from "./chunk-HHVOU4Q2.js";
|
|
6
|
+
import {
|
|
7
|
+
logo,
|
|
8
|
+
scaffold,
|
|
9
|
+
slugify,
|
|
10
|
+
success
|
|
11
|
+
} from "./chunk-UCHJJQVK.js";
|
|
12
|
+
|
|
13
|
+
// src/index.ts
|
|
14
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
|
|
15
|
+
import { resolve as resolve2 } from "path";
|
|
16
|
+
|
|
17
|
+
// src/prompts.ts
|
|
18
|
+
import { input, select } from "@inquirer/prompts";
|
|
19
|
+
import { basename } from "path";
|
|
20
|
+
import { resolve } from "path";
|
|
21
|
+
async function gatherAnswers(dirArg, useDefaults) {
|
|
22
|
+
let projectDir;
|
|
23
|
+
if (dirArg) {
|
|
24
|
+
projectDir = resolve(dirArg);
|
|
25
|
+
} else if (useDefaults) {
|
|
26
|
+
projectDir = resolve("my-docs");
|
|
27
|
+
} else {
|
|
28
|
+
const dirName = await input({
|
|
29
|
+
message: " Project directory:",
|
|
30
|
+
default: "my-docs"
|
|
31
|
+
});
|
|
32
|
+
projectDir = resolve(dirName);
|
|
33
|
+
}
|
|
34
|
+
const defaultName = basename(projectDir).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
35
|
+
const projectName = useDefaults ? defaultName : await input({
|
|
36
|
+
message: " Project name:",
|
|
37
|
+
default: defaultName
|
|
38
|
+
});
|
|
39
|
+
const defaultDesc = `Documentation for ${projectName}.`;
|
|
40
|
+
const description = useDefaults ? defaultDesc : await input({
|
|
41
|
+
message: " Description:",
|
|
42
|
+
default: defaultDesc
|
|
43
|
+
});
|
|
44
|
+
const brandPreset = useDefaults ? "primary" : await select({
|
|
45
|
+
message: " Brand preset:",
|
|
46
|
+
choices: [
|
|
47
|
+
{ name: "primary", value: "primary" },
|
|
48
|
+
{ name: "secondary", value: "secondary" }
|
|
49
|
+
],
|
|
50
|
+
default: "primary"
|
|
51
|
+
});
|
|
52
|
+
const repoUrl = useDefaults ? "" : await input({
|
|
53
|
+
message: " GitHub repo URL (optional):",
|
|
54
|
+
default: ""
|
|
55
|
+
});
|
|
56
|
+
let doInstall = true;
|
|
57
|
+
if (!useDefaults) {
|
|
58
|
+
const shouldInstall = await input({
|
|
59
|
+
message: " Install dependencies? (Y/n):",
|
|
60
|
+
default: "Y"
|
|
61
|
+
});
|
|
62
|
+
doInstall = shouldInstall.toLowerCase() !== "n";
|
|
63
|
+
}
|
|
64
|
+
let i18nLocales;
|
|
65
|
+
if (!useDefaults) {
|
|
66
|
+
const enableI18n = await input({
|
|
67
|
+
message: " Enable multi-language support? (y/N):",
|
|
68
|
+
default: "N"
|
|
69
|
+
});
|
|
70
|
+
if (enableI18n.toLowerCase() === "y") {
|
|
71
|
+
const localesInput = await input({
|
|
72
|
+
message: " Which locales? (comma-separated codes, e.g. es,fr,de):",
|
|
73
|
+
default: "es"
|
|
74
|
+
});
|
|
75
|
+
const LOCALE_LABELS = {
|
|
76
|
+
en: "English",
|
|
77
|
+
es: "Espa\xF1ol",
|
|
78
|
+
fr: "Fran\xE7ais",
|
|
79
|
+
de: "Deutsch",
|
|
80
|
+
it: "Italiano",
|
|
81
|
+
pt: "Portugu\xEAs",
|
|
82
|
+
ja: "\u65E5\u672C\u8A9E",
|
|
83
|
+
ko: "\uD55C\uAD6D\uC5B4",
|
|
84
|
+
zh: "\u4E2D\u6587",
|
|
85
|
+
ru: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439",
|
|
86
|
+
ar: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629",
|
|
87
|
+
nl: "Nederlands"
|
|
88
|
+
};
|
|
89
|
+
const codes = localesInput.split(",").map((c) => c.trim()).filter(Boolean);
|
|
90
|
+
i18nLocales = codes.map((code) => ({
|
|
91
|
+
code,
|
|
92
|
+
label: LOCALE_LABELS[code] ?? code.toUpperCase()
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { projectDir, projectName, description, brandPreset, repoUrl, doInstall, i18nLocales };
|
|
97
|
+
}
|
|
2
98
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const STARTER_PAGES = {
|
|
21
|
-
'introduction.mdx': `---
|
|
22
|
-
title: Introduction
|
|
23
|
-
description: Welcome to {NAME} documentation.
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Welcome
|
|
27
|
-
|
|
28
|
-
This is the home page of your **{NAME}** documentation site, powered by [Dox](https://github.com/kenny-io/Dox).
|
|
29
|
-
|
|
30
|
-
Get started by editing this file at \`src/content/introduction.mdx\`.
|
|
31
|
-
`,
|
|
32
|
-
'quickstart.mdx': `---
|
|
33
|
-
title: Quickstart
|
|
34
|
-
description: Get up and running with {NAME} in under 5 minutes.
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Installation
|
|
38
|
-
|
|
39
|
-
\`\`\`bash
|
|
40
|
-
npm install {SLUG}
|
|
41
|
-
\`\`\`
|
|
42
|
-
|
|
43
|
-
## Basic usage
|
|
44
|
-
|
|
45
|
-
\`\`\`ts
|
|
46
|
-
import { create } from '{SLUG}'
|
|
47
|
-
|
|
48
|
-
const client = create({ apiKey: 'your-api-key' })
|
|
49
|
-
\`\`\`
|
|
50
|
-
|
|
51
|
-
That's it — you're ready to go!
|
|
52
|
-
`,
|
|
99
|
+
// src/check.ts
|
|
100
|
+
import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
101
|
+
import { join as join2, extname, relative } from "path";
|
|
102
|
+
import matter from "gray-matter";
|
|
103
|
+
|
|
104
|
+
// src/docs-json.ts
|
|
105
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
106
|
+
import { join } from "path";
|
|
107
|
+
function readDocsJson(projectDir) {
|
|
108
|
+
const docsPath = join(projectDir, "docs.json");
|
|
109
|
+
const raw = readFileSync(docsPath, "utf8");
|
|
110
|
+
return JSON.parse(raw);
|
|
111
|
+
}
|
|
112
|
+
function writeDocsJson(projectDir, config) {
|
|
113
|
+
const docsPath = join(projectDir, "docs.json");
|
|
114
|
+
writeFileSync(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
53
115
|
}
|
|
54
116
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
"tab": "Changelog",
|
|
68
|
-
"href": "/changelog"
|
|
117
|
+
// src/check.ts
|
|
118
|
+
function collectNavPageIds(groups, seen, duplicates) {
|
|
119
|
+
for (const page of groups) {
|
|
120
|
+
if (typeof page === "string") {
|
|
121
|
+
if (seen.has(page)) {
|
|
122
|
+
duplicates.add(page);
|
|
123
|
+
} else {
|
|
124
|
+
seen.add(page);
|
|
125
|
+
}
|
|
126
|
+
} else if (page.pages) {
|
|
127
|
+
collectNavPageIds(page.pages, seen, duplicates);
|
|
69
128
|
}
|
|
70
|
-
|
|
129
|
+
}
|
|
71
130
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
131
|
+
function scanMdx(dir, results) {
|
|
132
|
+
let entries;
|
|
133
|
+
try {
|
|
134
|
+
entries = readdirSync(dir);
|
|
135
|
+
} catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const fullPath = join2(dir, entry);
|
|
140
|
+
try {
|
|
141
|
+
const stat = statSync(fullPath);
|
|
142
|
+
if (stat.isDirectory()) {
|
|
143
|
+
scanMdx(fullPath, results);
|
|
144
|
+
} else if (extname(entry).toLowerCase() === ".mdx") {
|
|
145
|
+
results.push(fullPath);
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
85
150
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
151
|
+
function addOrphanToNav(projectDir, pageId) {
|
|
152
|
+
const config = readDocsJson(projectDir);
|
|
153
|
+
const tab = config.tabs.find((t) => !t.href && !t.api && t.groups && t.groups.length > 0);
|
|
154
|
+
if (!tab?.groups) return;
|
|
155
|
+
const lastGroup = tab.groups[tab.groups.length - 1];
|
|
156
|
+
const existing = lastGroup.pages.filter((p) => typeof p === "string");
|
|
157
|
+
if (!existing.includes(pageId)) {
|
|
158
|
+
lastGroup.pages.push(pageId);
|
|
159
|
+
writeDocsJson(projectDir, config);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function runCheck(projectDir, fix) {
|
|
163
|
+
if (!existsSync(join2(projectDir, "docs.json"))) {
|
|
164
|
+
console.error(`
|
|
165
|
+
\u274C Not a Dox project: docs.json not found in ${projectDir}
|
|
166
|
+
`);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
const contentDir = join2(projectDir, "src", "content");
|
|
170
|
+
const issues = [];
|
|
171
|
+
const config = readDocsJson(projectDir);
|
|
172
|
+
const navPageIds = /* @__PURE__ */ new Set();
|
|
173
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
174
|
+
for (const tab of config.tabs) {
|
|
175
|
+
if (tab.href || tab.api) continue;
|
|
176
|
+
if (!tab.groups || tab.groups.length === 0) {
|
|
177
|
+
issues.push({ severity: "error", message: `Tab "${tab.tab}" has no groups and no href \u2014 it will render empty` });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
collectNavPageIds(tab.groups.map((g) => g), navPageIds, duplicates);
|
|
181
|
+
}
|
|
182
|
+
for (const dup of duplicates) {
|
|
183
|
+
issues.push({ severity: "error", message: `[duplicate] "${dup}" appears more than once in docs.json` });
|
|
184
|
+
}
|
|
185
|
+
for (const pageId of navPageIds) {
|
|
186
|
+
const candidates = [
|
|
187
|
+
join2(contentDir, `${pageId}.mdx`),
|
|
188
|
+
join2(contentDir, `${pageId}/index.mdx`)
|
|
189
|
+
];
|
|
190
|
+
if (!candidates.some((c) => existsSync(c))) {
|
|
191
|
+
issues.push({
|
|
192
|
+
severity: "error",
|
|
193
|
+
message: `"${pageId}" is in docs.json but has no MDX file`,
|
|
194
|
+
file: `src/content/${pageId}.mdx`
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const allFiles = [];
|
|
199
|
+
if (existsSync(contentDir)) scanMdx(contentDir, allFiles);
|
|
200
|
+
const fixedOrphans = [];
|
|
201
|
+
for (const filePath of allFiles) {
|
|
202
|
+
const rel = filePath.slice(contentDir.length + 1).replace(/\.mdx$/, "").replace(/\\/g, "/");
|
|
203
|
+
const pageId = rel.endsWith("/index") ? rel.slice(0, -6) : rel;
|
|
204
|
+
if (!navPageIds.has(pageId)) {
|
|
205
|
+
if (fix) {
|
|
206
|
+
addOrphanToNav(projectDir, pageId);
|
|
207
|
+
fixedOrphans.push(pageId);
|
|
96
208
|
} else {
|
|
97
|
-
|
|
209
|
+
issues.push({
|
|
210
|
+
severity: "warning",
|
|
211
|
+
message: `"${pageId}" is not in docs.json nav (orphan)`,
|
|
212
|
+
file: relative(projectDir, filePath)
|
|
213
|
+
});
|
|
98
214
|
}
|
|
99
|
-
}
|
|
100
|
-
|
|
215
|
+
}
|
|
216
|
+
let data = {};
|
|
217
|
+
let content = "";
|
|
218
|
+
try {
|
|
219
|
+
const raw = readFileSync2(filePath, "utf8");
|
|
220
|
+
const parsed = matter(raw);
|
|
221
|
+
data = parsed.data;
|
|
222
|
+
content = parsed.content;
|
|
223
|
+
} catch {
|
|
224
|
+
issues.push({ severity: "error", message: `Could not parse frontmatter`, file: relative(projectDir, filePath) });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const rel2 = relative(projectDir, filePath);
|
|
228
|
+
if (!data.title) {
|
|
229
|
+
issues.push({ severity: "warning", message: `Missing "title" in frontmatter`, file: rel2 });
|
|
230
|
+
}
|
|
231
|
+
if (!data.description) {
|
|
232
|
+
issues.push({ severity: "warning", message: `Missing "description" in frontmatter`, file: rel2 });
|
|
233
|
+
}
|
|
234
|
+
if (content.trim().length < 50) {
|
|
235
|
+
issues.push({ severity: "warning", message: `Very short body (${content.trim().length} chars) \u2014 page may be empty`, file: rel2 });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
239
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
240
|
+
console.log(`
|
|
241
|
+
Linting ${projectDir}...
|
|
242
|
+
`);
|
|
243
|
+
if (errors.length === 0 && warnings.length === 0 && fixedOrphans.length === 0) {
|
|
244
|
+
console.log(" \u2705 No issues found.\n");
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
console.log(` \u274C ${errors.length} error${errors.length !== 1 ? "s" : ""}, \u26A0\uFE0F ${warnings.length} warning${warnings.length !== 1 ? "s" : ""}
|
|
248
|
+
`);
|
|
249
|
+
if (errors.length > 0) {
|
|
250
|
+
console.log(" ERRORS:");
|
|
251
|
+
for (const issue of errors) {
|
|
252
|
+
console.log(` ${issue.message}`);
|
|
253
|
+
if (issue.file) console.log(` \u2192 ${issue.file}`);
|
|
254
|
+
}
|
|
255
|
+
console.log("");
|
|
256
|
+
}
|
|
257
|
+
if (warnings.length > 0) {
|
|
258
|
+
console.log(" WARNINGS:");
|
|
259
|
+
for (const issue of warnings) {
|
|
260
|
+
console.log(` ${issue.message}`);
|
|
261
|
+
if (issue.file) console.log(` \u2192 ${issue.file}`);
|
|
262
|
+
}
|
|
263
|
+
console.log("");
|
|
264
|
+
}
|
|
265
|
+
if (fixedOrphans.length > 0) {
|
|
266
|
+
console.log(` \u2705 Auto-fixed ${fixedOrphans.length} orphan page${fixedOrphans.length > 1 ? "s" : ""} (added to nav):`);
|
|
267
|
+
for (const p of fixedOrphans) console.log(` + ${p}`);
|
|
268
|
+
console.log("");
|
|
269
|
+
}
|
|
270
|
+
if (!fix && warnings.some((w) => w.message.includes("orphan"))) {
|
|
271
|
+
console.log(" Tip: run with --fix to auto-add orphan pages to navigation.\n");
|
|
272
|
+
}
|
|
273
|
+
return errors.length > 0 ? 1 : 0;
|
|
101
274
|
}
|
|
102
275
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
276
|
+
// src/translate.ts
|
|
277
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync } from "fs";
|
|
278
|
+
import { join as join3, dirname } from "path";
|
|
279
|
+
import { input as input2 } from "@inquirer/prompts";
|
|
280
|
+
import matter2 from "gray-matter";
|
|
281
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
282
|
+
import pLimit from "p-limit";
|
|
283
|
+
function readDocsJson2(projectDir) {
|
|
284
|
+
const docsPath = join3(projectDir, "docs.json");
|
|
285
|
+
const raw = readFileSync3(docsPath, "utf8");
|
|
286
|
+
return JSON.parse(raw);
|
|
108
287
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
288
|
+
function collectPageIds(pages) {
|
|
289
|
+
const ids = [];
|
|
290
|
+
for (const page of pages) {
|
|
291
|
+
if (typeof page === "string") {
|
|
292
|
+
ids.push(page);
|
|
293
|
+
} else if (page && typeof page === "object" && "pages" in page) {
|
|
294
|
+
ids.push(...collectPageIds(page.pages));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return ids;
|
|
112
298
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
299
|
+
function getAllPageIds(config) {
|
|
300
|
+
const ids = [];
|
|
301
|
+
const seen = /* @__PURE__ */ new Set();
|
|
302
|
+
const skippedApiTabs = [];
|
|
303
|
+
const hrefOnlyPages = [];
|
|
304
|
+
for (const tab of config.tabs) {
|
|
305
|
+
if (tab.api) {
|
|
306
|
+
skippedApiTabs.push(tab.tab);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (!tab.groups && tab.href) {
|
|
310
|
+
const pageId = tab.href.replace(/^\//, "");
|
|
311
|
+
if (pageId && !seen.has(pageId)) {
|
|
312
|
+
seen.add(pageId);
|
|
313
|
+
ids.push(pageId);
|
|
314
|
+
hrefOnlyPages.push({ tab: tab.tab, pageId });
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (!tab.groups) continue;
|
|
319
|
+
for (const group of tab.groups) {
|
|
320
|
+
for (const id of collectPageIds(group.pages)) {
|
|
321
|
+
if (!seen.has(id)) {
|
|
322
|
+
seen.add(id);
|
|
323
|
+
ids.push(id);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { ids, skippedApiTabs, hrefOnlyPages };
|
|
116
329
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
console.log(' ║ ██║ ██║██║ ██║ ╚███╔╝ ║')
|
|
125
|
-
console.log(' ║ ██║ ██║██║ ██║ ██╔██╗ ║')
|
|
126
|
-
console.log(' ║ ██████╔╝╚██████╔╝██╔╝ ██╗ ║')
|
|
127
|
-
console.log(' ║ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ║')
|
|
128
|
-
console.log(' ║ ║')
|
|
129
|
-
console.log(' ║ Beautiful docs, zero lock-in. ║')
|
|
130
|
-
console.log(' ║ ║')
|
|
131
|
-
console.log(' ╚══════════════════════════════════════╝')
|
|
132
|
-
console.log('')
|
|
330
|
+
function findSourceFile(projectDir, pageId) {
|
|
331
|
+
const contentRoot = join3(projectDir, "src", "content");
|
|
332
|
+
const candidates = [
|
|
333
|
+
join3(contentRoot, `${pageId}.mdx`),
|
|
334
|
+
join3(contentRoot, `${pageId}/index.mdx`)
|
|
335
|
+
];
|
|
336
|
+
return candidates.find((p) => existsSync2(p)) ?? null;
|
|
133
337
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
338
|
+
var TRANSLATION_SYSTEM_PROMPT = `You are a professional documentation translator. You will receive an MDX documentation file and translate it into the target language.
|
|
339
|
+
|
|
340
|
+
CRITICAL RULES \u2014 follow exactly:
|
|
341
|
+
1. Translate ALL prose text, headings, and paragraphs.
|
|
342
|
+
2. Translate frontmatter fields: title, description, and keywords values.
|
|
343
|
+
3. DO NOT translate or modify MDX component names (e.g. <Note>, <Warning>, <Steps>, <Step>, <CodeGroup>, <Tabs>, <Tab>, <Card>, <Accordion>, <Columns>).
|
|
344
|
+
4. DO NOT translate component prop names or prop values that are identifiers.
|
|
345
|
+
5. DO NOT translate content inside code blocks (\`\`\` ... \`\`\`).
|
|
346
|
+
6. DO NOT translate inline code spans (\`...\`).
|
|
347
|
+
7. DO NOT translate URLs, file paths, or import statements.
|
|
348
|
+
8. Preserve ALL whitespace, blank lines, and indentation exactly as in the original.
|
|
349
|
+
9. Preserve ALL frontmatter YAML structure exactly \u2014 only translate the string values.
|
|
350
|
+
10. Output ONLY the translated MDX file content \u2014 no preamble, no explanation, no markdown fences.
|
|
351
|
+
|
|
352
|
+
Example (translating to Spanish):
|
|
353
|
+
Input frontmatter:
|
|
354
|
+
title: Getting Started
|
|
355
|
+
description: Learn how to use the SDK.
|
|
356
|
+
Output frontmatter:
|
|
357
|
+
title: Comenzando
|
|
358
|
+
description: Aprende a usar el SDK.
|
|
359
|
+
|
|
360
|
+
Input MDX body:
|
|
361
|
+
## Installation
|
|
362
|
+
Run the following command:
|
|
363
|
+
\`\`\`bash
|
|
364
|
+
npm install my-sdk
|
|
365
|
+
\`\`\`
|
|
366
|
+
<Note>This is important.</Note>
|
|
367
|
+
Output MDX body:
|
|
368
|
+
## Instalaci\xF3n
|
|
369
|
+
Ejecuta el siguiente comando:
|
|
370
|
+
\`\`\`bash
|
|
371
|
+
npm install my-sdk
|
|
372
|
+
\`\`\`
|
|
373
|
+
<Note>Esto es importante.</Note>`;
|
|
374
|
+
async function translatePage(sourceContent, targetLocaleLabel, targetLocaleCode, model, client) {
|
|
375
|
+
const message = await client.messages.create({
|
|
376
|
+
model,
|
|
377
|
+
max_tokens: 8192,
|
|
378
|
+
system: TRANSLATION_SYSTEM_PROMPT,
|
|
379
|
+
messages: [
|
|
380
|
+
{
|
|
381
|
+
role: "user",
|
|
382
|
+
content: `Translate the following MDX documentation file to ${targetLocaleLabel} (locale code: ${targetLocaleCode}). Output ONLY the translated MDX content.
|
|
383
|
+
|
|
384
|
+
${sourceContent}`
|
|
385
|
+
}
|
|
386
|
+
]
|
|
387
|
+
});
|
|
388
|
+
const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
389
|
+
return text.trim();
|
|
156
390
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
391
|
+
async function runTranslateCommand(locale, pages, force, apiKey, model, yes, projectDir) {
|
|
392
|
+
const config = readDocsJson2(projectDir);
|
|
393
|
+
if (!config.i18n) {
|
|
394
|
+
console.error("\n \u274C No i18n config found in docs.json.");
|
|
395
|
+
console.error(' Add an "i18n" block to docs.json first:');
|
|
396
|
+
console.error(" {");
|
|
397
|
+
console.error(' "i18n": {');
|
|
398
|
+
console.error(' "defaultLocale": "en",');
|
|
399
|
+
console.error(' "locales": [{"code":"en","label":"English"},{"code":"es","label":"Espa\xF1ol"}]');
|
|
400
|
+
console.error(" }");
|
|
401
|
+
console.error(" }");
|
|
402
|
+
process.exit(1);
|
|
169
403
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
404
|
+
const targetLocale = config.i18n.locales.find((l) => l.code === locale);
|
|
405
|
+
if (!targetLocale) {
|
|
406
|
+
const available = config.i18n.locales.map((l) => l.code).join(", ");
|
|
407
|
+
console.error(`
|
|
408
|
+
\u274C Locale "${locale}" not found in docs.json i18n config.`);
|
|
409
|
+
console.error(` Available locales: ${available}`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
if (locale === config.i18n.defaultLocale) {
|
|
413
|
+
console.error(`
|
|
414
|
+
\u274C Cannot translate to the default locale "${locale}".`);
|
|
415
|
+
process.exit(1);
|
|
175
416
|
}
|
|
417
|
+
if (!apiKey) {
|
|
418
|
+
console.error("\n \u274C Anthropic API key required. Set ANTHROPIC_API_KEY or pass --api-key.");
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
const { ids: allPageIds, skippedApiTabs, hrefOnlyPages } = getAllPageIds(config);
|
|
422
|
+
if (skippedApiTabs.length > 0) {
|
|
423
|
+
console.log(` \u2139 Skipping API reference tab(s): ${skippedApiTabs.join(", ")}`);
|
|
424
|
+
console.log(" API reference pages are auto-generated from your OpenAPI spec and cannot be translated as MDX files.");
|
|
425
|
+
console.log("");
|
|
426
|
+
}
|
|
427
|
+
if (hrefOnlyPages.length > 0) {
|
|
428
|
+
const labels = hrefOnlyPages.map(({ tab, pageId }) => `${tab} (${pageId}.mdx)`).join(", ");
|
|
429
|
+
console.log(` \u2139 Including standalone tab page(s): ${labels}`);
|
|
430
|
+
console.log("");
|
|
431
|
+
}
|
|
432
|
+
const targetPageIds = pages ?? allPageIds;
|
|
433
|
+
const contentRoot = join3(projectDir, "src", "content");
|
|
434
|
+
const toTranslate = [];
|
|
435
|
+
for (const pageId of targetPageIds) {
|
|
436
|
+
const sourceFile = findSourceFile(projectDir, pageId);
|
|
437
|
+
if (!sourceFile) {
|
|
438
|
+
console.warn(` \u26A0 Page "${pageId}" not found in src/content \u2014 skipping.`);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
|
|
442
|
+
const targetFile = join3(contentRoot, locale, relativeFromContent);
|
|
443
|
+
if (existsSync2(targetFile) && !force) {
|
|
444
|
+
console.log(` \u23ED ${pageId} (already translated, use --force to overwrite)`);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
toTranslate.push({ pageId, sourceFile, targetFile });
|
|
448
|
+
}
|
|
449
|
+
if (toTranslate.length === 0) {
|
|
450
|
+
console.log("\n \u2705 Nothing to translate.");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
console.log(`
|
|
454
|
+
\u{1F4CB} ${toTranslate.length} page(s) to translate to ${targetLocale.label} (${locale}):`);
|
|
455
|
+
for (const { pageId } of toTranslate) {
|
|
456
|
+
console.log(` \u2022 ${pageId}`);
|
|
457
|
+
}
|
|
458
|
+
console.log("");
|
|
459
|
+
if (!yes) {
|
|
460
|
+
const confirm = await input2({
|
|
461
|
+
message: " Proceed? (Y/n):",
|
|
462
|
+
default: "Y"
|
|
463
|
+
});
|
|
464
|
+
if (confirm.toLowerCase() === "n") {
|
|
465
|
+
console.log("\n Aborted.");
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const client = new Anthropic({ apiKey });
|
|
470
|
+
const limit = pLimit(3);
|
|
471
|
+
let doneCount = 0;
|
|
472
|
+
const total = toTranslate.length;
|
|
473
|
+
await Promise.all(
|
|
474
|
+
toTranslate.map(
|
|
475
|
+
({ pageId, sourceFile, targetFile }) => limit(async () => {
|
|
476
|
+
try {
|
|
477
|
+
const sourceContent = readFileSync3(sourceFile, "utf8");
|
|
478
|
+
const parsed = matter2(sourceContent);
|
|
479
|
+
if (!parsed.data.title) {
|
|
480
|
+
console.warn(` \u26A0 ${pageId}: missing title in frontmatter \u2014 translating anyway`);
|
|
481
|
+
}
|
|
482
|
+
const translated = await translatePage(
|
|
483
|
+
sourceContent,
|
|
484
|
+
targetLocale.label,
|
|
485
|
+
locale,
|
|
486
|
+
model,
|
|
487
|
+
client
|
|
488
|
+
);
|
|
489
|
+
mkdirSync(dirname(targetFile), { recursive: true });
|
|
490
|
+
writeFileSync2(targetFile, translated + "\n", "utf8");
|
|
491
|
+
doneCount++;
|
|
492
|
+
console.log(` \u2713 [${doneCount}/${total}] ${pageId}`);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
doneCount++;
|
|
495
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
496
|
+
console.error(` \u2717 [${doneCount}/${total}] ${pageId}: ${msg}`);
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
)
|
|
500
|
+
);
|
|
501
|
+
console.log("");
|
|
502
|
+
console.log(` \u2705 Translation complete! ${doneCount}/${total} pages translated.`);
|
|
503
|
+
console.log(` Files written to: src/content/${locale}/`);
|
|
176
504
|
}
|
|
177
505
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
execSync(`rm -rf "${fullPath}"`)
|
|
506
|
+
// src/index.ts
|
|
507
|
+
var args = process.argv.slice(2);
|
|
508
|
+
var flags = args.filter((a) => a.startsWith("-"));
|
|
509
|
+
var positional = [];
|
|
510
|
+
for (let i = 0; i < args.length; i++) {
|
|
511
|
+
if (args[i].startsWith("-")) {
|
|
512
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
513
|
+
i++;
|
|
187
514
|
}
|
|
188
515
|
} else {
|
|
189
|
-
|
|
516
|
+
positional.push(args[i]);
|
|
190
517
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
.replace(/\{SLUG\}/g, slug)
|
|
197
|
-
writeFileSync(join(contentDir, filename), content, 'utf8')
|
|
518
|
+
}
|
|
519
|
+
function getFlagValue(flag) {
|
|
520
|
+
const idx = args.indexOf(flag);
|
|
521
|
+
if (idx !== -1 && idx + 1 < args.length && !args[idx + 1].startsWith("-")) {
|
|
522
|
+
return args[idx + 1];
|
|
198
523
|
}
|
|
199
|
-
|
|
200
|
-
// Write docs.json
|
|
201
|
-
writeFileSync(join(targetDir, 'docs.json'), STARTER_DOCS_JSON, 'utf8')
|
|
524
|
+
return void 0;
|
|
202
525
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
console.
|
|
208
|
-
|
|
526
|
+
async function runMigrateCommand() {
|
|
527
|
+
const sourceUrl = positional[1];
|
|
528
|
+
if (!sourceUrl) {
|
|
529
|
+
console.error("\n \u274C Source URL is required.");
|
|
530
|
+
console.error(" Usage: create-dox migrate <github-url> [output-dir] [options]");
|
|
531
|
+
console.error(" Example: create-dox migrate https://github.com/mintlify/docs my-docs");
|
|
532
|
+
process.exit(1);
|
|
209
533
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
// Replace description (handles multiline template string)
|
|
220
|
-
source = source.replace(
|
|
221
|
-
/description:[\s\S]*?'([^']*)'/,
|
|
222
|
-
`description:\n '${description.replace(/'/g, "\\'")}'`,
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
// Replace brand preset
|
|
226
|
-
source = source.replace(
|
|
227
|
-
/const brandPreset:\s*BrandPresetKey\s*=\s*'[^']*'/,
|
|
228
|
-
`const brandPreset: BrandPresetKey = '${brandPreset}'`,
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
// Replace repo URL
|
|
232
|
-
if (repoUrl) {
|
|
233
|
-
source = source.replace(
|
|
234
|
-
/repoUrl:\s*'[^']*'/,
|
|
235
|
-
`repoUrl: '${repoUrl}'`,
|
|
236
|
-
)
|
|
237
|
-
// Also update GitHub link
|
|
238
|
-
source = source.replace(
|
|
239
|
-
/\{\s*label:\s*'GitHub',\s*href:\s*'[^']*'\s*\}/,
|
|
240
|
-
`{ label: 'GitHub', href: '${repoUrl}' }`,
|
|
241
|
-
)
|
|
242
|
-
// Update support link
|
|
243
|
-
source = source.replace(
|
|
244
|
-
/\{\s*label:\s*'Support',\s*href:\s*'[^']*'\s*\}/,
|
|
245
|
-
`{ label: 'Support', href: '${repoUrl}/issues/new' }`,
|
|
246
|
-
)
|
|
534
|
+
let parsedSource;
|
|
535
|
+
try {
|
|
536
|
+
parsedSource = parseGitHubUrl(sourceUrl);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
console.error(`
|
|
539
|
+
\u274C ${err instanceof Error ? err.message : err}`);
|
|
540
|
+
process.exit(1);
|
|
247
541
|
}
|
|
248
|
-
|
|
249
|
-
|
|
542
|
+
const apiKey = getFlagValue("--api-key") ?? process.env.ANTHROPIC_API_KEY;
|
|
543
|
+
const intoDir = getFlagValue("--into");
|
|
544
|
+
const isInto = Boolean(intoDir);
|
|
545
|
+
let projectDir;
|
|
546
|
+
if (intoDir) {
|
|
547
|
+
projectDir = resolve2(intoDir);
|
|
548
|
+
} else if (positional[2]) {
|
|
549
|
+
projectDir = resolve2(positional[2]);
|
|
550
|
+
} else {
|
|
551
|
+
projectDir = resolve2(`${slugify(parsedSource.repo)}-docs`);
|
|
552
|
+
}
|
|
553
|
+
const branch = getFlagValue("--branch");
|
|
554
|
+
const docsDir = getFlagValue("--docs-dir");
|
|
555
|
+
const yes = flags.includes("--yes") || flags.includes("-y");
|
|
556
|
+
logo();
|
|
557
|
+
console.log(" \u{1F680} Dox Migrate");
|
|
558
|
+
console.log("");
|
|
559
|
+
console.log(` Source: ${sourceUrl}`);
|
|
560
|
+
console.log(` Target: ${projectDir}`);
|
|
561
|
+
if (branch) console.log(` Branch: ${branch}`);
|
|
562
|
+
if (docsDir) console.log(` Docs dir: ${docsDir}`);
|
|
563
|
+
console.log("");
|
|
564
|
+
if (!apiKey) {
|
|
565
|
+
console.warn(" \u26A0 No API key provided. Non-Markdown files will be skipped.");
|
|
566
|
+
console.warn(" Set ANTHROPIC_API_KEY=... or pass --api-key <key> to convert them.");
|
|
567
|
+
console.warn("");
|
|
568
|
+
}
|
|
569
|
+
await migrateDocs({
|
|
570
|
+
sourceUrl,
|
|
571
|
+
projectDir,
|
|
572
|
+
into: isInto,
|
|
573
|
+
apiKey,
|
|
574
|
+
branch,
|
|
575
|
+
docsDir,
|
|
576
|
+
yes
|
|
577
|
+
});
|
|
250
578
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
579
|
+
async function runScaffoldCommand() {
|
|
580
|
+
const useDefaults = flags.includes("--yes") || flags.includes("-y");
|
|
581
|
+
const dirArg = positional[0];
|
|
582
|
+
if (dirArg) {
|
|
583
|
+
const resolved = resolve2(dirArg);
|
|
584
|
+
if (existsSync3(resolved) && readdirSync2(resolved).length > 0) {
|
|
585
|
+
console.error(`
|
|
586
|
+
\u274C Directory "${resolved}" already exists and is not empty.`);
|
|
587
|
+
process.exit(1);
|
|
259
588
|
}
|
|
260
589
|
}
|
|
590
|
+
const answers = await gatherAnswers(dirArg, useDefaults);
|
|
591
|
+
const result = await scaffold({
|
|
592
|
+
projectDir: answers.projectDir,
|
|
593
|
+
projectName: answers.projectName,
|
|
594
|
+
description: answers.description,
|
|
595
|
+
brandPreset: answers.brandPreset,
|
|
596
|
+
repoUrl: answers.repoUrl,
|
|
597
|
+
doInstall: answers.doInstall,
|
|
598
|
+
i18nLocales: answers.i18nLocales
|
|
599
|
+
});
|
|
600
|
+
success(result.projectDir, answers.projectName);
|
|
261
601
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
run('git commit -m "Initial commit from create-dox"', targetDir)
|
|
268
|
-
} catch {
|
|
269
|
-
// Git might not be configured — that's fine
|
|
270
|
-
console.log(' ⚠️ Could not initialize git (you can do this manually).')
|
|
271
|
-
}
|
|
602
|
+
async function runCheckCommand() {
|
|
603
|
+
const projectDir = resolve2(positional[1] ?? ".");
|
|
604
|
+
const fix = flags.includes("--fix");
|
|
605
|
+
const exitCode = await runCheck(projectDir, fix);
|
|
606
|
+
process.exit(exitCode);
|
|
272
607
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
608
|
+
async function runTranslateSubcommand() {
|
|
609
|
+
const locale = getFlagValue("--locale");
|
|
610
|
+
if (!locale) {
|
|
611
|
+
console.error("\n \u274C --locale is required.");
|
|
612
|
+
console.error(" Usage: create-dox translate --locale es [--pages page1,page2] [--force] [--api-key key]");
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
const pagesArg = getFlagValue("--pages");
|
|
616
|
+
const pages = pagesArg ? pagesArg.split(",").map((p) => p.trim()).filter(Boolean) : void 0;
|
|
617
|
+
const force = flags.includes("--force");
|
|
618
|
+
const apiKey = getFlagValue("--api-key") ?? process.env.ANTHROPIC_API_KEY;
|
|
619
|
+
const model = getFlagValue("--model") ?? "claude-sonnet-4-6";
|
|
620
|
+
const yes = flags.includes("--yes") || flags.includes("-y");
|
|
621
|
+
const projectDir = resolve2(positional[1] ?? ".");
|
|
622
|
+
logo();
|
|
623
|
+
console.log(" \u{1F310} Dox Translate");
|
|
624
|
+
console.log("");
|
|
625
|
+
await runTranslateCommand(locale, pages, force, apiKey, model, yes, projectDir);
|
|
279
626
|
}
|
|
280
|
-
|
|
281
|
-
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
282
|
-
|
|
283
627
|
async function main() {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
} else if (useDefaults) {
|
|
292
|
-
projectDir = resolve('my-docs')
|
|
628
|
+
const subcommand = positional[0];
|
|
629
|
+
if (subcommand === "migrate") {
|
|
630
|
+
await runMigrateCommand();
|
|
631
|
+
} else if (subcommand === "check") {
|
|
632
|
+
await runCheckCommand();
|
|
633
|
+
} else if (subcommand === "translate") {
|
|
634
|
+
await runTranslateSubcommand();
|
|
293
635
|
} else {
|
|
294
|
-
|
|
295
|
-
|
|
636
|
+
logo();
|
|
637
|
+
await runScaffoldCommand();
|
|
296
638
|
}
|
|
297
|
-
|
|
298
|
-
if (existsSync(projectDir) && readdirSync(projectDir).length > 0) {
|
|
299
|
-
console.log(`\n ❌ Directory "${projectDir}" already exists and is not empty.`)
|
|
300
|
-
rl.close()
|
|
301
|
-
process.exit(1)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// 2. Project name
|
|
305
|
-
const defaultName = basename(projectDir)
|
|
306
|
-
.replace(/[-_]/g, ' ')
|
|
307
|
-
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
308
|
-
const projectName = useDefaults ? defaultName : await ask(' Project name', defaultName)
|
|
309
|
-
|
|
310
|
-
// 3. Description
|
|
311
|
-
const defaultDesc = `Documentation for ${projectName}.`
|
|
312
|
-
const description = useDefaults ? defaultDesc : await ask(' Description', defaultDesc)
|
|
313
|
-
|
|
314
|
-
// 4. Brand preset
|
|
315
|
-
const brandPreset = useDefaults ? 'primary' : await choose('\n Brand preset:', BRAND_PRESETS, 'primary')
|
|
316
|
-
|
|
317
|
-
// 5. GitHub repo (optional)
|
|
318
|
-
const repoUrl = useDefaults ? '' : await ask(' GitHub repo URL (optional)', '')
|
|
319
|
-
|
|
320
|
-
// 6. Install deps?
|
|
321
|
-
let doInstall = true
|
|
322
|
-
if (!useDefaults) {
|
|
323
|
-
const shouldInstall = await ask(' Install dependencies? (Y/n)', 'Y')
|
|
324
|
-
doInstall = shouldInstall.toLowerCase() !== 'n'
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const slug = slugify(projectName)
|
|
328
|
-
|
|
329
|
-
// ── Execute ──────────────────────────────────────────────────────────────
|
|
330
|
-
|
|
331
|
-
cloneTemplate(projectDir)
|
|
332
|
-
writeStarterContent(projectDir, projectName, slug)
|
|
333
|
-
updateSiteConfig(projectDir, projectName, description, brandPreset, repoUrl)
|
|
334
|
-
updateEnvExample(projectDir)
|
|
335
|
-
|
|
336
|
-
if (doInstall) {
|
|
337
|
-
installDeps(projectDir)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
initGit(projectDir)
|
|
341
|
-
success(projectDir, projectName)
|
|
342
|
-
|
|
343
|
-
rl.close()
|
|
344
639
|
}
|
|
345
|
-
|
|
346
640
|
main().catch((err) => {
|
|
347
|
-
console.error(
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
})
|
|
641
|
+
console.error("\n \u274C Error:", err.message);
|
|
642
|
+
process.exit(1);
|
|
643
|
+
});
|