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/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
- // create-dox — Scaffold a new Dox documentation project.
4
- // Zero dependencies. Requires Node >= 18.
5
-
6
- import { createInterface } from 'node:readline'
7
- import { execSync } from 'node:child_process'
8
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, cpSync } from 'node:fs'
9
- import { resolve, join, basename } from 'node:path'
10
-
11
- // ── Constants ────────────────────────────────────────────────────────────────
12
-
13
- const REPO_URL = 'https://github.com/kenny-io/Dox.git'
14
- const BRAND_PRESETS = ['primary', 'secondary']
15
- const args = process.argv.slice(2)
16
- const flags = args.filter((a) => a.startsWith('-'))
17
- const positional = args.filter((a) => !a.startsWith('-'))
18
- const useDefaults = flags.includes('--yes') || flags.includes('-y')
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
- const STARTER_DOCS_JSON = `{
56
- "tabs": [
57
- {
58
- "tab": "Overview",
59
- "groups": [
60
- {
61
- "group": "Getting Started",
62
- "pages": ["introduction", "quickstart"]
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
- // ── Helpers ──────────────────────────────────────────────────────────────────
75
-
76
- const rl = createInterface({ input: process.stdin, output: process.stdout })
77
-
78
- function ask(question, fallback) {
79
- return new Promise((resolve) => {
80
- const suffix = fallback ? ` (${fallback})` : ''
81
- rl.question(`${question}${suffix}: `, (answer) => {
82
- resolve(answer.trim() || fallback || '')
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
- function choose(question, options, fallback) {
88
- return new Promise((resolve) => {
89
- const optionList = options.map((o, i) => ` ${i + 1}) ${o}`).join('\n')
90
- const defaultIndex = options.indexOf(fallback) + 1
91
- const suffix = defaultIndex ? ` [${defaultIndex}]` : ''
92
- rl.question(`${question}\n${optionList}\n> Choose${suffix}: `, (answer) => {
93
- const num = parseInt(answer.trim(), 10)
94
- if (num >= 1 && num <= options.length) {
95
- resolve(options[num - 1])
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
- resolve(fallback || options[0])
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
- function slugify(name) {
104
- return name
105
- .toLowerCase()
106
- .replace(/[^a-z0-9]+/g, '-')
107
- .replace(/(^-|-$)/g, '')
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
- function run(cmd, cwd) {
111
- execSync(cmd, { cwd, stdio: 'inherit' })
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
- function runSilent(cmd, cwd) {
115
- return execSync(cmd, { cwd, encoding: 'utf8' }).trim()
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
- function logo() {
119
- console.log('')
120
- console.log(' ╔══════════════════════════════════════╗')
121
- console.log(' ║ ║')
122
- console.log(' ║ ██████╗ ██████╗ ██╗ ██╗ ║')
123
- console.log(' ║ ██╔══██╗██╔═══██╗╚██╗██╔╝ ║')
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
- function success(projectDir, projectName) {
136
- console.log('')
137
- console.log(' ✅ Your Dox project is ready!')
138
- console.log('')
139
- console.log(` 📂 ${projectDir}`)
140
- console.log('')
141
- console.log(' Next steps:')
142
- console.log('')
143
- console.log(` cd ${basename(projectDir)}`)
144
- console.log(' npm run dev')
145
- console.log('')
146
- console.log(` Then open http://localhost:3040 to see your ${projectName} docs.`)
147
- console.log('')
148
- console.log(' 📝 Key files to edit:')
149
- console.log(' • src/data/site.ts — name, links, branding')
150
- console.log(' • docs.json — navigation structure')
151
- console.log(' • src/content/*.mdx — your documentation')
152
- console.log(' • openapi.yaml — API spec (optional)')
153
- console.log('')
154
- console.log(' Happy documenting! 🚀')
155
- console.log('')
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
- // ── Scaffold logic ───────────────────────────────────────────────────────────
159
-
160
- function cloneTemplate(targetDir) {
161
- console.log('')
162
- console.log(' ⏳ Cloning Dox template...')
163
- run(`git clone --depth 1 ${REPO_URL} "${targetDir}"`)
164
-
165
- // Remove the template's .git so the user starts fresh
166
- const gitDir = join(targetDir, '.git')
167
- if (existsSync(gitDir)) {
168
- execSync(`rm -rf "${gitDir}"`)
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
- // Remove the CLI folder from the cloned project (they don't need it)
172
- const cliDir = join(targetDir, 'cli')
173
- if (existsSync(cliDir)) {
174
- execSync(`rm -rf "${cliDir}"`)
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
- function writeStarterContent(targetDir, projectName, slug) {
179
- const contentDir = join(targetDir, 'src', 'content')
180
-
181
- // Clear existing example content
182
- if (existsSync(contentDir)) {
183
- const entries = readdirSync(contentDir)
184
- for (const entry of entries) {
185
- const fullPath = join(contentDir, entry)
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
- mkdirSync(contentDir, { recursive: true })
516
+ positional.push(args[i]);
190
517
  }
191
-
192
- // Write starter pages
193
- for (const [filename, template] of Object.entries(STARTER_PAGES)) {
194
- const content = template
195
- .replace(/\{NAME\}/g, projectName)
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
- function updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl) {
205
- const siteFile = join(targetDir, 'src', 'data', 'site.ts')
206
- if (!existsSync(siteFile)) {
207
- console.log(' ⚠️ Could not find src/data/site.ts skipping config update.')
208
- return
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
- let source = readFileSync(siteFile, 'utf8')
212
-
213
- // Replace name
214
- source = source.replace(
215
- /name:\s*'[^']*'/,
216
- `name: '${projectName.replace(/'/g, "\\'")}'`,
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
- writeFileSync(siteFile, source, 'utf8')
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
- function updateEnvExample(targetDir) {
253
- const envFile = join(targetDir, '.env.example')
254
- if (existsSync(envFile)) {
255
- // Copy .env.example to .env.local for immediate use
256
- const envLocal = join(targetDir, '.env.local')
257
- if (!existsSync(envLocal)) {
258
- cpSync(envFile, envLocal)
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
- function initGit(targetDir) {
264
- try {
265
- run('git init', targetDir)
266
- run('git add -A', targetDir)
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
- function installDeps(targetDir) {
275
- console.log('')
276
- console.log(' 📦 Installing dependencies...')
277
- console.log('')
278
- run('npm install', targetDir)
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
- logo()
285
-
286
- // 1. Project directory
287
- const dirArg = positional[0]
288
- let projectDir
289
- if (dirArg) {
290
- projectDir = resolve(dirArg)
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
- const dirName = await ask(' Project directory', 'my-docs')
295
- projectDir = resolve(dirName)
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('\n Error:', err.message)
348
- rl.close()
349
- process.exit(1)
350
- })
641
+ console.error("\n \u274C Error:", err.message);
642
+ process.exit(1);
643
+ });