@thallylabs/mcp 0.7.0 → 0.7.2
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/LICENSE +1 -1
- package/dist/index.js +78 -919
- package/dist/tools.js +78 -919
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -19,7 +19,17 @@ import { promisify } from "util";
|
|
|
19
19
|
import tar from "tar";
|
|
20
20
|
var pipelineAsync = promisify(pipeline);
|
|
21
21
|
var TARBALL_URL = "https://codeload.github.com/thallylabs/thally/tar.gz/main";
|
|
22
|
-
var EXCLUDE_PATHS = [
|
|
22
|
+
var EXCLUDE_PATHS = [
|
|
23
|
+
"/cli/",
|
|
24
|
+
"/packages/",
|
|
25
|
+
"/node_modules/",
|
|
26
|
+
"/.git/",
|
|
27
|
+
"/thally-agent.yml",
|
|
28
|
+
"/thally-track.yml",
|
|
29
|
+
"/CODEOWNERS",
|
|
30
|
+
"/CLAUDE.md",
|
|
31
|
+
"/notes/"
|
|
32
|
+
];
|
|
23
33
|
var STARTER_PAGES = {
|
|
24
34
|
"introduction.mdx": `---
|
|
25
35
|
title: Introduction
|
|
@@ -526,867 +536,15 @@ async function handleUpdatePage(input) {
|
|
|
526
536
|
|
|
527
537
|
// src/tools/migrate-docs.ts
|
|
528
538
|
import { z as z6 } from "zod";
|
|
529
|
-
|
|
530
|
-
// src/lib/migrate/index.ts
|
|
531
|
-
import { mkdirSync as mkdirSync3, copyFileSync, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdtempSync, rmSync } from "fs";
|
|
532
|
-
import { join as join7, dirname as dirname2, resolve as resolve2 } from "path";
|
|
533
|
-
import { tmpdir } from "os";
|
|
534
|
-
import { execSync as execSync3 } from "child_process";
|
|
535
|
-
import pLimit from "p-limit";
|
|
536
|
-
|
|
537
|
-
// src/lib/migrate/github.ts
|
|
538
|
-
import { execSync as execSync2 } from "child_process";
|
|
539
|
-
import { readdirSync as readdirSync2, statSync } from "fs";
|
|
540
|
-
import { join as join5, relative, extname, basename } from "path";
|
|
541
|
-
var OPENAPI_FILENAMES = [
|
|
542
|
-
"openapi.json",
|
|
543
|
-
"openapi.yaml",
|
|
544
|
-
"openapi.yml",
|
|
545
|
-
"swagger.json",
|
|
546
|
-
"swagger.yaml",
|
|
547
|
-
"swagger.yml"
|
|
548
|
-
];
|
|
549
|
-
function detectOpenApiSpec(cloneDir) {
|
|
550
|
-
return findOpenApiSpec(cloneDir, 0);
|
|
551
|
-
}
|
|
552
|
-
function findOpenApiSpec(dir, depth) {
|
|
553
|
-
if (depth > 3) return null;
|
|
554
|
-
let entries;
|
|
555
|
-
try {
|
|
556
|
-
entries = readdirSync2(dir);
|
|
557
|
-
} catch {
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
for (const filename of OPENAPI_FILENAMES) {
|
|
561
|
-
if (entries.includes(filename)) {
|
|
562
|
-
return { absPath: join5(dir, filename), filename };
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
for (const entry of entries) {
|
|
566
|
-
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
567
|
-
const fullPath = join5(dir, entry);
|
|
568
|
-
try {
|
|
569
|
-
if (statSync(fullPath).isDirectory()) {
|
|
570
|
-
const found = findOpenApiSpec(fullPath, depth + 1);
|
|
571
|
-
if (found) return found;
|
|
572
|
-
}
|
|
573
|
-
} catch {
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
return null;
|
|
577
|
-
}
|
|
578
|
-
function parseGitHubUrl(rawUrl) {
|
|
579
|
-
let url;
|
|
580
|
-
try {
|
|
581
|
-
url = new URL(rawUrl);
|
|
582
|
-
} catch {
|
|
583
|
-
throw new Error(`Invalid URL: ${rawUrl}`);
|
|
584
|
-
}
|
|
585
|
-
if (url.hostname !== "github.com") {
|
|
586
|
-
throw new Error(`URL must be a github.com URL, got: ${url.hostname}`);
|
|
587
|
-
}
|
|
588
|
-
const parts = url.pathname.replace(/^\//, "").split("/");
|
|
589
|
-
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
590
|
-
throw new Error(`GitHub URL must include owner and repo: ${rawUrl}`);
|
|
591
|
-
}
|
|
592
|
-
const owner = parts[0];
|
|
593
|
-
const repo = parts[1];
|
|
594
|
-
let branch = "HEAD";
|
|
595
|
-
let docsDir = "";
|
|
596
|
-
if (parts.length >= 4 && parts[2] === "tree") {
|
|
597
|
-
branch = parts[3];
|
|
598
|
-
if (parts.length > 4) {
|
|
599
|
-
docsDir = parts.slice(4).join("/");
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
const cloneUrl = `https://github.com/${owner}/${repo}.git`;
|
|
603
|
-
return { owner, repo, branch, docsDir, cloneUrl };
|
|
604
|
-
}
|
|
605
|
-
async function cloneRepo(source, targetDir) {
|
|
606
|
-
const parts = ["git", "clone", "--depth", "1"];
|
|
607
|
-
if (source.branch !== "HEAD") {
|
|
608
|
-
parts.push("--branch", source.branch);
|
|
609
|
-
}
|
|
610
|
-
parts.push(source.cloneUrl, targetDir);
|
|
611
|
-
const cmd = parts.join(" ");
|
|
612
|
-
try {
|
|
613
|
-
execSync2(cmd, { stdio: "pipe" });
|
|
614
|
-
} catch (err) {
|
|
615
|
-
const stderr = err.stderr?.toString().trim() ?? "";
|
|
616
|
-
const msg = stderr || (err instanceof Error ? err.message : String(err));
|
|
617
|
-
throw new Error(`Failed to clone ${source.cloneUrl}: ${msg}`);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
var MD_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx"]);
|
|
621
|
-
var ALL_DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".rst", ".txt"]);
|
|
622
|
-
function hasMdFiles(dir) {
|
|
623
|
-
let entries;
|
|
624
|
-
try {
|
|
625
|
-
entries = readdirSync2(dir);
|
|
626
|
-
} catch {
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
for (const entry of entries) {
|
|
630
|
-
const fullPath = join5(dir, entry);
|
|
631
|
-
try {
|
|
632
|
-
const stat = statSync(fullPath);
|
|
633
|
-
if (stat.isDirectory()) {
|
|
634
|
-
if (hasMdFiles(fullPath)) return true;
|
|
635
|
-
} else if (MD_EXTENSIONS.has(extname(entry).toLowerCase())) {
|
|
636
|
-
return true;
|
|
637
|
-
}
|
|
638
|
-
} catch {
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
return false;
|
|
642
|
-
}
|
|
643
|
-
function detectDocsDir(cloneDir) {
|
|
644
|
-
const candidates = [
|
|
645
|
-
"docs",
|
|
646
|
-
"documentation",
|
|
647
|
-
"content",
|
|
648
|
-
"pages",
|
|
649
|
-
"src/content",
|
|
650
|
-
"src/pages",
|
|
651
|
-
"guide",
|
|
652
|
-
"guides",
|
|
653
|
-
""
|
|
654
|
-
];
|
|
655
|
-
for (const candidate of candidates) {
|
|
656
|
-
const fullPath = candidate ? join5(cloneDir, candidate) : cloneDir;
|
|
657
|
-
try {
|
|
658
|
-
const stat = statSync(fullPath);
|
|
659
|
-
if (stat.isDirectory() && hasMdFiles(fullPath)) {
|
|
660
|
-
return candidate;
|
|
661
|
-
}
|
|
662
|
-
} catch {
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
return "";
|
|
666
|
-
}
|
|
667
|
-
function slugifySegment(seg) {
|
|
668
|
-
return seg.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
669
|
-
}
|
|
670
|
-
function derivePageId(relPath) {
|
|
671
|
-
const normalized = relPath.replace(/\\/g, "/");
|
|
672
|
-
const parts = normalized.split("/");
|
|
673
|
-
const filename = parts[parts.length - 1];
|
|
674
|
-
const dirs = parts.slice(0, -1);
|
|
675
|
-
const base = basename(filename, extname(filename));
|
|
676
|
-
if (dirs.length === 0 && base.toLowerCase() === "readme") {
|
|
677
|
-
return "introduction";
|
|
678
|
-
}
|
|
679
|
-
if (base.toLowerCase() === "index") {
|
|
680
|
-
if (dirs.length === 0) return "introduction";
|
|
681
|
-
return dirs.map(slugifySegment).join("/");
|
|
682
|
-
}
|
|
683
|
-
return [...dirs, base].map(slugifySegment).join("/");
|
|
684
|
-
}
|
|
685
|
-
function scanDir(dir, baseDir, primaryOnly, results) {
|
|
686
|
-
let entries;
|
|
687
|
-
try {
|
|
688
|
-
entries = readdirSync2(dir);
|
|
689
|
-
} catch {
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
for (const entry of entries) {
|
|
693
|
-
if (entry.startsWith("_") || entry.startsWith(".") || entry === "node_modules") continue;
|
|
694
|
-
const fullPath = join5(dir, entry);
|
|
695
|
-
let stat;
|
|
696
|
-
try {
|
|
697
|
-
stat = statSync(fullPath);
|
|
698
|
-
} catch {
|
|
699
|
-
continue;
|
|
700
|
-
}
|
|
701
|
-
if (stat.isDirectory()) {
|
|
702
|
-
scanDir(fullPath, baseDir, primaryOnly, results);
|
|
703
|
-
} else {
|
|
704
|
-
const ext = extname(entry).toLowerCase();
|
|
705
|
-
const validExt = primaryOnly ? MD_EXTENSIONS.has(ext) : ALL_DOC_EXTENSIONS.has(ext);
|
|
706
|
-
if (!validExt) continue;
|
|
707
|
-
const relPath = relative(baseDir, fullPath);
|
|
708
|
-
const pageId = derivePageId(relPath);
|
|
709
|
-
results.push({ absPath: fullPath, relPath, pageId, ext });
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
function findDocFiles(cloneDir, docsDir) {
|
|
714
|
-
const baseDir = docsDir ? join5(cloneDir, docsDir) : cloneDir;
|
|
715
|
-
const primaryResults = [];
|
|
716
|
-
scanDir(baseDir, baseDir, true, primaryResults);
|
|
717
|
-
if (primaryResults.length > 0) return primaryResults;
|
|
718
|
-
const allResults = [];
|
|
719
|
-
scanDir(baseDir, baseDir, false, allResults);
|
|
720
|
-
return allResults;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// src/lib/migrate/importer.ts
|
|
724
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
725
|
-
import { basename as basename2, extname as extname2 } from "path";
|
|
726
|
-
import matter2 from "gray-matter";
|
|
727
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
728
|
-
function titleFromFilename(relPath) {
|
|
729
|
-
const filename = relPath.replace(/\\/g, "/").split("/").pop() ?? relPath;
|
|
730
|
-
const base = basename2(filename, extname2(filename));
|
|
731
|
-
return base.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
732
|
-
}
|
|
733
|
-
function extractFirstParagraph(content) {
|
|
734
|
-
for (const line of content.split("\n")) {
|
|
735
|
-
const trimmed = line.trim();
|
|
736
|
-
if (!trimmed) continue;
|
|
737
|
-
if (trimmed.startsWith("#")) continue;
|
|
738
|
-
if (trimmed.startsWith("```") || trimmed.startsWith(":::") || trimmed.startsWith("<")) continue;
|
|
739
|
-
if (trimmed.startsWith("import ") || trimmed.startsWith("export ")) continue;
|
|
740
|
-
return trimmed.slice(0, 200);
|
|
741
|
-
}
|
|
742
|
-
return "";
|
|
743
|
-
}
|
|
744
|
-
function normalizeComponents(body) {
|
|
745
|
-
let result = body;
|
|
746
|
-
const importedComponents = /* @__PURE__ */ new Set();
|
|
747
|
-
result = result.replace(
|
|
748
|
-
/^import\s+(\w+|\{[^}]+\})\s+from\s+['"][^'"]+['"]\s*;?\s*$/gm,
|
|
749
|
-
(_, imported) => {
|
|
750
|
-
const name = imported.trim();
|
|
751
|
-
if (/^[A-Z]\w*$/.test(name)) importedComponents.add(name);
|
|
752
|
-
return "";
|
|
753
|
-
}
|
|
754
|
-
);
|
|
755
|
-
for (const name of importedComponents) {
|
|
756
|
-
result = result.replace(
|
|
757
|
-
new RegExp(`<${name}(?:\\s[^>]*)?\\/>`, "gm"),
|
|
758
|
-
`{/* <${name} /> \u2014 imported snippet component */}`
|
|
759
|
-
);
|
|
760
|
-
result = result.replace(
|
|
761
|
-
new RegExp(`<${name}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${name}>`, "g"),
|
|
762
|
-
`{/* <${name}> \u2014 imported snippet component */}`
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
result = result.replace(/<!--([\s\S]*?)-->/g, (_, inner) => `{/*${inner}*/}`);
|
|
766
|
-
result = result.replace(/<Tip>([\s\S]*?)<\/Tip>/g, (_, c) => `<Note>${c}</Note>`);
|
|
767
|
-
result = result.replace(/<Check>([\s\S]*?)<\/Check>/g, (_, c) => `<Note>${c}</Note>`);
|
|
768
|
-
result = result.replace(/<Danger>([\s\S]*?)<\/Danger>/g, (_, c) => `<Error>${c}</Error>`);
|
|
769
|
-
result = result.replace(/<Callout(?:\s[^>]*)?>([\s\S]*?)<\/Callout>/g, (_, c) => `<Note>${c}</Note>`);
|
|
770
|
-
result = result.replace(/:::(\w+)(?:\s+[^\n]*)?\n([\s\S]*?):::/g, (_, type, content) => {
|
|
771
|
-
const tag = mapAdmonitionToThallyTag(type.toLowerCase());
|
|
772
|
-
return `<${tag}>
|
|
773
|
-
${content.trim()}
|
|
774
|
-
</${tag}>`;
|
|
775
|
-
});
|
|
776
|
-
result = result.replace(
|
|
777
|
-
/\{%\s*hint\s+style="(\w+)"\s*%\}([\s\S]*?)\{%\s*endhint\s*%\}/g,
|
|
778
|
-
(_, style, content) => {
|
|
779
|
-
const tag = mapGitBookStyleToThallyTag(style.toLowerCase());
|
|
780
|
-
return `<${tag}>
|
|
781
|
-
${content.trim()}
|
|
782
|
-
</${tag}>`;
|
|
783
|
-
}
|
|
784
|
-
);
|
|
785
|
-
result = result.replace(/<AccordionGroup[^>]*>\n?([\s\S]*?)\n?<\/AccordionGroup>/g, (_, inner) => inner.trim());
|
|
786
|
-
result = result.replace(/<Expandable(\s[^>]*)?>/g, (_, attrs = "") => {
|
|
787
|
-
const title = attrs.match(/title="([^"]*)"/)?.[1] ?? "Details";
|
|
788
|
-
return `<Accordion title="${title}">`;
|
|
789
|
-
});
|
|
790
|
-
result = result.replace(/<\/Expandable>/g, "</Accordion>");
|
|
791
|
-
result = result.replace(/<Latex>([\s\S]*?)<\/Latex>/g, (_, inner) => `\`${inner.trim()}\``);
|
|
792
|
-
result = result.replace(/<(?:ResponseField|ParamField)([^>]*)>/g, (_, attrs) => {
|
|
793
|
-
const name = attrs.match(/name="([^"]*)"/)?.[1] ?? "";
|
|
794
|
-
const type = attrs.match(/type="([^"]*)"/)?.[1] ?? "";
|
|
795
|
-
const required = /\brequired\b/.test(attrs);
|
|
796
|
-
const def = attrs.match(/default="([^"]*)"/)?.[1];
|
|
797
|
-
const deprecated = /\bdeprecated\b/.test(attrs);
|
|
798
|
-
const meta = [
|
|
799
|
-
type && `\`${type}\``,
|
|
800
|
-
required && "*(required)*",
|
|
801
|
-
deprecated && "*(deprecated)*",
|
|
802
|
-
def !== void 0 && `*(default: \`${def}\`)*`
|
|
803
|
-
].filter(Boolean).join(" ");
|
|
804
|
-
return `
|
|
805
|
-
**\`${name}\`** ${meta}
|
|
806
|
-
|
|
807
|
-
`;
|
|
808
|
-
});
|
|
809
|
-
result = result.replace(/<\/(?:ResponseField|ParamField)>/g, "\n");
|
|
810
|
-
result = result.replace(/<RequestExample[^>]*>/g, "<CodeGroup>");
|
|
811
|
-
result = result.replace(/<\/RequestExample>/g, "</CodeGroup>");
|
|
812
|
-
result = result.replace(/<ResponseExample[^>]*>/g, "<CodeGroup>");
|
|
813
|
-
result = result.replace(/<\/ResponseExample>/g, "</CodeGroup>");
|
|
814
|
-
result = result.replace(/<Panel[^>]*>([\s\S]*?)<\/Panel>/g, (_, inner) => inner.trim());
|
|
815
|
-
result = result.replace(/<Badge[^>]*>([\s\S]*?)<\/Badge>/g, (_, inner) => `**${inner.trim()}**`);
|
|
816
|
-
result = result.replace(/<Tile(\s[^>]*)?>/g, (_, attrs = "") => `<Card${attrs}>`);
|
|
817
|
-
result = result.replace(/<\/Tile>/g, "</Card>");
|
|
818
|
-
result = result.replace(/<View(\s[^>]*)?>/g, (_, attrs = "") => {
|
|
819
|
-
const title = attrs.match(/title="([^"]*)"/)?.[1] ?? "View";
|
|
820
|
-
return `<Tab title="${title}">`;
|
|
821
|
-
});
|
|
822
|
-
result = result.replace(/<\/View>/g, "</Tab>");
|
|
823
|
-
result = result.replace(/<Update(\s[^>]*)?>/g, (_, attrs = "") => {
|
|
824
|
-
const label = attrs.match(/label="([^"]*)"/)?.[1] ?? "";
|
|
825
|
-
const desc = attrs.match(/description="([^"]*)"/)?.[1] ?? "";
|
|
826
|
-
return `## ${label}${desc ? `
|
|
827
|
-
|
|
828
|
-
*${desc}*` : ""}
|
|
829
|
-
|
|
830
|
-
`;
|
|
831
|
-
});
|
|
832
|
-
result = result.replace(/<\/Update>/g, "\n");
|
|
833
|
-
result = result.replace(/<Prompt[^>]*>([\s\S]*?)<\/Prompt>/g, (_, inner) => {
|
|
834
|
-
return `\`\`\`text
|
|
835
|
-
${inner.trim()}
|
|
836
|
-
\`\`\``;
|
|
837
|
-
});
|
|
838
|
-
result = result.replace(/<Tree[^>]*>/g, "```\n");
|
|
839
|
-
result = result.replace(/<\/Tree>/g, "\n```");
|
|
840
|
-
result = result.replace(/<Tree\.Folder[^>]*name="([^"]*)"[^>]*>/g, (_, name) => `\u{1F4C1} ${name}/
|
|
841
|
-
`);
|
|
842
|
-
result = result.replace(/<\/Tree\.Folder>/g, "");
|
|
843
|
-
result = result.replace(new RegExp('<Tree\\.File[^>]*name="([^"]*)"[^>]*/>', "g"), (_, name) => ` ${name}
|
|
844
|
-
`);
|
|
845
|
-
result = result.replace(/<Color[^>]*>/g, "| Name | Value |\n|---|---|\n");
|
|
846
|
-
result = result.replace(/<\/Color>/g, "");
|
|
847
|
-
result = result.replace(/<Color\.Row[^>]*title="([^"]*)"[^>]*>/g, (_, title) => `**${title}**
|
|
848
|
-
`);
|
|
849
|
-
result = result.replace(/<\/Color\.Row>/g, "");
|
|
850
|
-
result = result.replace(
|
|
851
|
-
/<Color\.Item[^>]*name="([^"]*)"[^>]*value="([^"]*)"[^>]*\/>/g,
|
|
852
|
-
(_, name, value) => `| ${name} | \`${value}\` |
|
|
853
|
-
`
|
|
854
|
-
);
|
|
855
|
-
result = result.replace(/<Banner[^>]*>([\s\S]*?)<\/Banner>/g, "");
|
|
856
|
-
result = result.replace(/<Banner[^>]*\/>/g, "");
|
|
857
|
-
return result;
|
|
858
|
-
}
|
|
859
|
-
function mapAdmonitionToThallyTag(type) {
|
|
860
|
-
if (type === "warning" || type === "caution") return "Warning";
|
|
861
|
-
if (type === "danger") return "Error";
|
|
862
|
-
if (type === "info") return "Info";
|
|
863
|
-
return "Note";
|
|
864
|
-
}
|
|
865
|
-
function mapGitBookStyleToThallyTag(style) {
|
|
866
|
-
if (style === "warning") return "Warning";
|
|
867
|
-
if (style === "danger") return "Error";
|
|
868
|
-
if (style === "success") return "Note";
|
|
869
|
-
return "Info";
|
|
870
|
-
}
|
|
871
|
-
var RST_SYSTEM_PROMPT = `You are a documentation converter. Convert the given file content to clean MDX.
|
|
872
|
-
Respond with ONLY valid JSON \u2014 no prose, no markdown fences:
|
|
873
|
-
{
|
|
874
|
-
"frontmatter": { "title": "string", "description": "string", "keywords": ["..."] },
|
|
875
|
-
"body": "string \u2014 full MDX body"
|
|
876
|
-
}
|
|
877
|
-
Rules: preserve code blocks with language hints; convert tables to Markdown; convert callout
|
|
878
|
-
boxes to <Note> or <Warning>; preserve heading hierarchy; do not include page title as a heading.`;
|
|
879
|
-
function parseClaudeResponse(text) {
|
|
880
|
-
try {
|
|
881
|
-
return JSON.parse(text);
|
|
882
|
-
} catch {
|
|
883
|
-
const stripped = text.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
|
|
884
|
-
return JSON.parse(stripped);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
async function importFile(file, apiKey) {
|
|
888
|
-
const ext = file.ext.toLowerCase();
|
|
889
|
-
if (ext === ".md" || ext === ".mdx") {
|
|
890
|
-
const raw = readFileSync4(file.absPath, "utf8");
|
|
891
|
-
const parsed = matter2(raw);
|
|
892
|
-
const fmTitle = parsed.data.title ?? "";
|
|
893
|
-
const fmDesc = parsed.data.description ?? "";
|
|
894
|
-
const fmKeywords = parsed.data.keywords;
|
|
895
|
-
const title = fmTitle || titleFromFilename(file.relPath);
|
|
896
|
-
const description = fmDesc || extractFirstParagraph(parsed.content);
|
|
897
|
-
const keywords = Array.isArray(fmKeywords) ? fmKeywords : [];
|
|
898
|
-
const openapi = parsed.data.openapi;
|
|
899
|
-
const body = normalizeComponents(parsed.content);
|
|
900
|
-
if (openapi && !body.trim()) return null;
|
|
901
|
-
return { pageId: file.pageId, frontmatter: { title, description, keywords }, body };
|
|
902
|
-
}
|
|
903
|
-
if (!apiKey) {
|
|
904
|
-
throw new Error(`Skipping non-Markdown file (no API key): ${file.relPath}`);
|
|
905
|
-
}
|
|
906
|
-
const content = readFileSync4(file.absPath, "utf8");
|
|
907
|
-
const client = new Anthropic({ apiKey });
|
|
908
|
-
const message = await client.messages.create({
|
|
909
|
-
model: "claude-sonnet-4-6",
|
|
910
|
-
max_tokens: 4096,
|
|
911
|
-
system: RST_SYSTEM_PROMPT,
|
|
912
|
-
messages: [
|
|
913
|
-
{
|
|
914
|
-
role: "user",
|
|
915
|
-
content: `Convert this documentation file to MDX.
|
|
916
|
-
|
|
917
|
-
File: ${file.relPath}
|
|
918
|
-
|
|
919
|
-
Content:
|
|
920
|
-
${content.slice(0, 8e4)}`
|
|
921
|
-
}
|
|
922
|
-
]
|
|
923
|
-
});
|
|
924
|
-
const responseText = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
925
|
-
const claudeResult = parseClaudeResponse(responseText);
|
|
926
|
-
return {
|
|
927
|
-
pageId: file.pageId,
|
|
928
|
-
frontmatter: claudeResult.frontmatter,
|
|
929
|
-
body: claudeResult.body
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// src/lib/migrate/nav-builder.ts
|
|
934
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
935
|
-
import { join as join6 } from "path";
|
|
936
|
-
function titleCase(str) {
|
|
937
|
-
return str.split("-").map((word) => {
|
|
938
|
-
if (word.toLowerCase() === "api") return "API";
|
|
939
|
-
if (word.toLowerCase() === "sdk") return "SDK";
|
|
940
|
-
if (word.toLowerCase() === "cli") return "CLI";
|
|
941
|
-
if (word.toLowerCase() === "ui") return "UI";
|
|
942
|
-
if (word.toLowerCase() === "faq") return "FAQ";
|
|
943
|
-
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
944
|
-
}).join(" ");
|
|
945
|
-
}
|
|
946
|
-
function buildNavStructure(pages) {
|
|
947
|
-
const seen = /* @__PURE__ */ new Set();
|
|
948
|
-
const ordered = [];
|
|
949
|
-
for (const p of pages) {
|
|
950
|
-
if (!seen.has(p.pageId)) {
|
|
951
|
-
seen.add(p.pageId);
|
|
952
|
-
ordered.push(p.pageId);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
const depth1Segments = /* @__PURE__ */ new Set();
|
|
956
|
-
for (const id of ordered) {
|
|
957
|
-
const parts = id.split("/");
|
|
958
|
-
if (parts.length > 1) {
|
|
959
|
-
depth1Segments.add(parts[0]);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
const rootOnlyPages = ordered.filter((id) => !id.includes("/"));
|
|
963
|
-
const useSingleTab = depth1Segments.size === 0 || depth1Segments.size === 1 && rootOnlyPages.length === 0;
|
|
964
|
-
let tabs;
|
|
965
|
-
if (useSingleTab) {
|
|
966
|
-
const groups = buildGroups(ordered, null);
|
|
967
|
-
tabs = [{ tab: "Overview", groups }];
|
|
968
|
-
} else {
|
|
969
|
-
tabs = [];
|
|
970
|
-
if (rootOnlyPages.length > 0) {
|
|
971
|
-
const groups = buildGroups(rootOnlyPages, null);
|
|
972
|
-
tabs.push({ tab: "Overview", groups });
|
|
973
|
-
}
|
|
974
|
-
for (const seg of depth1Segments) {
|
|
975
|
-
const tabPages = ordered.filter((id) => id.startsWith(seg + "/") || id === seg);
|
|
976
|
-
const groups = buildGroups(tabPages, seg);
|
|
977
|
-
tabs.push({ tab: titleCase(seg), groups });
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
tabs.push({ tab: "Changelog", href: "/changelog" });
|
|
981
|
-
return { tabs };
|
|
982
|
-
}
|
|
983
|
-
function buildGroups(pageIds, tabSegment) {
|
|
984
|
-
const groupMap = /* @__PURE__ */ new Map();
|
|
985
|
-
for (const id of pageIds) {
|
|
986
|
-
let groupName;
|
|
987
|
-
if (tabSegment === null) {
|
|
988
|
-
groupName = "Overview";
|
|
989
|
-
} else {
|
|
990
|
-
const rel = id.startsWith(tabSegment + "/") ? id.slice(tabSegment.length + 1) : id;
|
|
991
|
-
const relParts = rel.split("/");
|
|
992
|
-
groupName = relParts.length === 1 ? titleCase(tabSegment) : titleCase(relParts[0]);
|
|
993
|
-
}
|
|
994
|
-
if (!groupMap.has(groupName)) groupMap.set(groupName, []);
|
|
995
|
-
groupMap.get(groupName).push(id);
|
|
996
|
-
}
|
|
997
|
-
const groups = [];
|
|
998
|
-
for (const [groupName, groupPages] of groupMap) {
|
|
999
|
-
const sorted = [...groupPages];
|
|
1000
|
-
const introIdx = sorted.indexOf("introduction");
|
|
1001
|
-
if (introIdx > 0) {
|
|
1002
|
-
sorted.splice(introIdx, 1);
|
|
1003
|
-
sorted.unshift("introduction");
|
|
1004
|
-
}
|
|
1005
|
-
groups.push({ group: groupName, pages: sorted });
|
|
1006
|
-
}
|
|
1007
|
-
return groups;
|
|
1008
|
-
}
|
|
1009
|
-
function detectPlatform(cloneDir) {
|
|
1010
|
-
if (existsSync5(join6(cloneDir, "mint.json"))) return "mintlify";
|
|
1011
|
-
if (existsSync5(join6(cloneDir, "docs.json"))) {
|
|
1012
|
-
try {
|
|
1013
|
-
const parsed = JSON.parse(readFileSync5(join6(cloneDir, "docs.json"), "utf8"));
|
|
1014
|
-
if (Array.isArray(parsed.tabs)) return "thally";
|
|
1015
|
-
const schema = parsed.$schema;
|
|
1016
|
-
if (schema?.includes("mintlify") || "navigation" in parsed) return "mintlify";
|
|
1017
|
-
} catch {
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
if (existsSync5(join6(cloneDir, "docusaurus.config.js")) || existsSync5(join6(cloneDir, "docusaurus.config.ts")) || existsSync5(join6(cloneDir, "docusaurus.config.mjs"))) return "docusaurus";
|
|
1021
|
-
if (existsSync5(join6(cloneDir, "SUMMARY.md"))) return "gitbook";
|
|
1022
|
-
if (existsSync5(join6(cloneDir, ".vitepress"))) return "vitepress";
|
|
1023
|
-
if (existsSync5(join6(cloneDir, "astro.config.mjs")) || existsSync5(join6(cloneDir, "astro.config.ts"))) return "starlight";
|
|
1024
|
-
if (existsSync5(join6(cloneDir, "_meta.json")) || existsSync5(join6(cloneDir, "pages", "_meta.json"))) return "nextra";
|
|
1025
|
-
return "unknown";
|
|
1026
|
-
}
|
|
1027
|
-
function slugify2(s) {
|
|
1028
|
-
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
1029
|
-
}
|
|
1030
|
-
function normalizePageRef(ref, docsDir) {
|
|
1031
|
-
let r = ref;
|
|
1032
|
-
if (docsDir && r.startsWith(docsDir + "/")) r = r.slice(docsDir.length + 1);
|
|
1033
|
-
r = r.replace(/\.(mdx?|rst|txt)$/, "");
|
|
1034
|
-
const parts = r.split("/");
|
|
1035
|
-
const last = parts[parts.length - 1].toLowerCase();
|
|
1036
|
-
if (last === "index" || last === "readme") {
|
|
1037
|
-
return parts.length === 1 ? "introduction" : parts.slice(0, -1).map(slugify2).join("/");
|
|
1038
|
-
}
|
|
1039
|
-
return parts.map(slugify2).join("/");
|
|
1040
|
-
}
|
|
1041
|
-
function convertMintTabs(tabs, docsDir) {
|
|
1042
|
-
if (tabs.length === 0) return null;
|
|
1043
|
-
function convertPageRef(page) {
|
|
1044
|
-
if (typeof page === "string") return normalizePageRef(page, docsDir);
|
|
1045
|
-
if (page !== null && typeof page === "object" && "group" in page && "pages" in page) {
|
|
1046
|
-
const p = page;
|
|
1047
|
-
return {
|
|
1048
|
-
group: String(p.group),
|
|
1049
|
-
pages: (p.pages ?? []).map(convertPageRef)
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
|
-
return String(page);
|
|
1053
|
-
}
|
|
1054
|
-
const resultTabs = tabs.map((item) => {
|
|
1055
|
-
if (item.href) return { tab: String(item.tab), href: String(item.href) };
|
|
1056
|
-
const groups = (item.groups ?? []).map((g) => ({
|
|
1057
|
-
group: String(g.group),
|
|
1058
|
-
pages: (g.pages ?? []).map(convertPageRef)
|
|
1059
|
-
}));
|
|
1060
|
-
return { tab: String(item.tab), groups };
|
|
1061
|
-
});
|
|
1062
|
-
if (!resultTabs.some((t) => t.tab === "Changelog")) {
|
|
1063
|
-
resultTabs.push({ tab: "Changelog", href: "/changelog" });
|
|
1064
|
-
}
|
|
1065
|
-
return { tabs: resultTabs };
|
|
1066
|
-
}
|
|
1067
|
-
function parseMintConfig(config, docsDir) {
|
|
1068
|
-
const nav = config.navigation;
|
|
1069
|
-
if (nav && typeof nav === "object" && !Array.isArray(nav)) {
|
|
1070
|
-
const v3Tabs = nav.tabs;
|
|
1071
|
-
if (Array.isArray(v3Tabs) && v3Tabs.length > 0) return convertMintTabs(v3Tabs, docsDir);
|
|
1072
|
-
}
|
|
1073
|
-
if (!Array.isArray(nav) || nav.length === 0) return null;
|
|
1074
|
-
if ("tab" in nav[0]) return convertMintTabs(nav, docsDir);
|
|
1075
|
-
function convertPageRef(page) {
|
|
1076
|
-
if (typeof page === "string") return normalizePageRef(page, docsDir);
|
|
1077
|
-
if (page !== null && typeof page === "object" && "group" in page && "pages" in page) {
|
|
1078
|
-
const p = page;
|
|
1079
|
-
return { group: String(p.group), pages: (p.pages ?? []).map(convertPageRef) };
|
|
1080
|
-
}
|
|
1081
|
-
return String(page);
|
|
1082
|
-
}
|
|
1083
|
-
const groups = nav.map((item) => ({
|
|
1084
|
-
group: String(item.group ?? ""),
|
|
1085
|
-
pages: (item.pages ?? []).map(convertPageRef)
|
|
1086
|
-
}));
|
|
1087
|
-
return { tabs: [{ tab: "Docs", groups }, { tab: "Changelog", href: "/changelog" }] };
|
|
1088
|
-
}
|
|
1089
|
-
function parseGitBookSummary(cloneDir, docsDir) {
|
|
1090
|
-
const candidates = [join6(cloneDir, "SUMMARY.md")];
|
|
1091
|
-
if (docsDir) candidates.push(join6(cloneDir, docsDir, "SUMMARY.md"));
|
|
1092
|
-
let raw = "";
|
|
1093
|
-
for (const p of candidates) {
|
|
1094
|
-
if (existsSync5(p)) {
|
|
1095
|
-
raw = readFileSync5(p, "utf8");
|
|
1096
|
-
break;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
if (!raw) return null;
|
|
1100
|
-
const groups = [];
|
|
1101
|
-
let currentGroupName = "Overview";
|
|
1102
|
-
let currentPages = [];
|
|
1103
|
-
for (const line of raw.split("\n")) {
|
|
1104
|
-
const groupMatch = line.match(/^##\s+(.+)/);
|
|
1105
|
-
if (groupMatch) {
|
|
1106
|
-
if (currentPages.length > 0) groups.push({ group: currentGroupName, pages: currentPages });
|
|
1107
|
-
currentGroupName = groupMatch[1].trim();
|
|
1108
|
-
currentPages = [];
|
|
1109
|
-
continue;
|
|
1110
|
-
}
|
|
1111
|
-
const pageMatch = line.match(/^\*\s+\[.+?\]\((.+?)\)/);
|
|
1112
|
-
if (pageMatch) {
|
|
1113
|
-
const ref = pageMatch[1].trim();
|
|
1114
|
-
if (ref.startsWith("http")) continue;
|
|
1115
|
-
currentPages.push(normalizePageRef(ref, docsDir));
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
if (currentPages.length > 0) groups.push({ group: currentGroupName, pages: currentPages });
|
|
1119
|
-
if (groups.length === 0) return null;
|
|
1120
|
-
return { tabs: [{ tab: "Docs", groups }, { tab: "Changelog", href: "/changelog" }] };
|
|
1121
|
-
}
|
|
1122
|
-
function parseNextraMeta(cloneDir, docsDir) {
|
|
1123
|
-
const baseDir = docsDir ? join6(cloneDir, docsDir) : cloneDir;
|
|
1124
|
-
const metaPath = join6(baseDir, "_meta.json");
|
|
1125
|
-
if (!existsSync5(metaPath)) return null;
|
|
1126
|
-
try {
|
|
1127
|
-
const meta = JSON.parse(readFileSync5(metaPath, "utf8"));
|
|
1128
|
-
const pages = [];
|
|
1129
|
-
for (const [key, value] of Object.entries(meta)) {
|
|
1130
|
-
if (typeof value === "object" && value !== null) {
|
|
1131
|
-
const v = value;
|
|
1132
|
-
if (v.type === "separator" || v.type === "menu") continue;
|
|
1133
|
-
}
|
|
1134
|
-
pages.push(key === "index" ? "introduction" : slugify2(key));
|
|
1135
|
-
}
|
|
1136
|
-
if (pages.length === 0) return null;
|
|
1137
|
-
return {
|
|
1138
|
-
tabs: [
|
|
1139
|
-
{ tab: "Docs", groups: [{ group: "Overview", pages }] },
|
|
1140
|
-
{ tab: "Changelog", href: "/changelog" }
|
|
1141
|
-
]
|
|
1142
|
-
};
|
|
1143
|
-
} catch {
|
|
1144
|
-
return null;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
var PLATFORM_LABELS = {
|
|
1148
|
-
mintlify: "Mintlify",
|
|
1149
|
-
docusaurus: "Docusaurus",
|
|
1150
|
-
gitbook: "GitBook",
|
|
1151
|
-
nextra: "Nextra",
|
|
1152
|
-
vitepress: "VitePress",
|
|
1153
|
-
starlight: "Starlight (Astro)",
|
|
1154
|
-
thally: "Thally",
|
|
1155
|
-
unknown: "unknown"
|
|
1156
|
-
};
|
|
1157
|
-
function detectNavFromConfig(cloneDir, docsDir, platform) {
|
|
1158
|
-
const detected = platform ?? detectPlatform(cloneDir);
|
|
1159
|
-
const label = PLATFORM_LABELS[detected];
|
|
1160
|
-
switch (detected) {
|
|
1161
|
-
case "thally": {
|
|
1162
|
-
try {
|
|
1163
|
-
const parsed = JSON.parse(
|
|
1164
|
-
readFileSync5(join6(cloneDir, "docs.json"), "utf8")
|
|
1165
|
-
);
|
|
1166
|
-
console.log(` \u{1F4CB} Detected ${label} \u2014 using docs.json navigation as-is`);
|
|
1167
|
-
return parsed;
|
|
1168
|
-
} catch {
|
|
1169
|
-
return null;
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
case "mintlify": {
|
|
1173
|
-
for (const file of ["docs.json", "mint.json"]) {
|
|
1174
|
-
const p = join6(cloneDir, file);
|
|
1175
|
-
if (!existsSync5(p)) continue;
|
|
1176
|
-
try {
|
|
1177
|
-
const config = JSON.parse(readFileSync5(p, "utf8"));
|
|
1178
|
-
const nav = parseMintConfig(config, docsDir);
|
|
1179
|
-
if (nav) {
|
|
1180
|
-
console.log(` \u{1F4CB} Detected ${label} (${file}) \u2014 converting navigation`);
|
|
1181
|
-
return nav;
|
|
1182
|
-
}
|
|
1183
|
-
} catch {
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
return null;
|
|
1187
|
-
}
|
|
1188
|
-
case "gitbook": {
|
|
1189
|
-
const nav = parseGitBookSummary(cloneDir, docsDir);
|
|
1190
|
-
if (nav) console.log(` \u{1F4CB} Detected ${label} (SUMMARY.md) \u2014 converting navigation`);
|
|
1191
|
-
return nav;
|
|
1192
|
-
}
|
|
1193
|
-
case "nextra": {
|
|
1194
|
-
const nav = parseNextraMeta(cloneDir, docsDir);
|
|
1195
|
-
if (nav) console.log(` \u{1F4CB} Detected ${label} (_meta.json) \u2014 converting navigation`);
|
|
1196
|
-
return nav;
|
|
1197
|
-
}
|
|
1198
|
-
case "docusaurus":
|
|
1199
|
-
case "vitepress":
|
|
1200
|
-
case "starlight":
|
|
1201
|
-
console.log(` \u{1F4CB} Detected ${label} \u2014 nav config is JavaScript, using directory structure`);
|
|
1202
|
-
return null;
|
|
1203
|
-
default:
|
|
1204
|
-
return null;
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
// src/lib/migrate/index.ts
|
|
1209
|
-
function mergeDocsJson(existing, incoming) {
|
|
1210
|
-
const existingTabNames = new Set(existing.tabs.map((t) => t.tab));
|
|
1211
|
-
const merged = { tabs: [...existing.tabs.filter((t) => t.tab !== "Changelog")] };
|
|
1212
|
-
if (existing.ai || incoming.ai) {
|
|
1213
|
-
merged.ai = { ...incoming.ai, ...existing.ai };
|
|
1214
|
-
}
|
|
1215
|
-
for (const tab of incoming.tabs) {
|
|
1216
|
-
if (tab.tab === "Changelog") continue;
|
|
1217
|
-
if (existingTabNames.has(tab.tab)) {
|
|
1218
|
-
const existingTab = merged.tabs.find((t) => t.tab === tab.tab);
|
|
1219
|
-
if (existingTab.groups && tab.groups) {
|
|
1220
|
-
const existingGroupNames = new Set(existingTab.groups.map((g) => g.group));
|
|
1221
|
-
for (const group of tab.groups) {
|
|
1222
|
-
if (existingGroupNames.has(group.group)) {
|
|
1223
|
-
const eg = existingTab.groups.find((g) => g.group === group.group);
|
|
1224
|
-
const existingPageSet = new Set(eg.pages.map((p) => typeof p === "string" ? p : p.group));
|
|
1225
|
-
for (const page of group.pages) {
|
|
1226
|
-
const key = typeof page === "string" ? page : page.group;
|
|
1227
|
-
if (!existingPageSet.has(key)) eg.pages.push(page);
|
|
1228
|
-
}
|
|
1229
|
-
} else {
|
|
1230
|
-
existingTab.groups.push(group);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
} else if (tab.groups) {
|
|
1234
|
-
existingTab.groups = tab.groups;
|
|
1235
|
-
}
|
|
1236
|
-
} else {
|
|
1237
|
-
merged.tabs.push(tab);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
merged.tabs.push({ tab: "Changelog", href: "/changelog" });
|
|
1241
|
-
return merged;
|
|
1242
|
-
}
|
|
1243
|
-
function injectApiTab(config, specFilename) {
|
|
1244
|
-
const apiTab = { tab: "API Reference", api: { source: `/${specFilename}` } };
|
|
1245
|
-
const tabs = config.tabs.filter((t) => {
|
|
1246
|
-
if (t.tab.toLowerCase().includes("api")) return false;
|
|
1247
|
-
return true;
|
|
1248
|
-
});
|
|
1249
|
-
const changelogIdx = tabs.findIndex((t) => t.tab === "Changelog");
|
|
1250
|
-
if (changelogIdx >= 0) {
|
|
1251
|
-
tabs.splice(changelogIdx, 0, apiTab);
|
|
1252
|
-
} else {
|
|
1253
|
-
tabs.push(apiTab);
|
|
1254
|
-
}
|
|
1255
|
-
return { ...config, tabs };
|
|
1256
|
-
}
|
|
1257
|
-
function installDeps2(targetDir) {
|
|
1258
|
-
execSync3("npm install", { cwd: targetDir, stdio: "inherit" });
|
|
1259
|
-
}
|
|
1260
|
-
function initGit2(targetDir) {
|
|
1261
|
-
try {
|
|
1262
|
-
execSync3("git init", { cwd: targetDir, stdio: "inherit" });
|
|
1263
|
-
execSync3("git add -A", { cwd: targetDir, stdio: "inherit" });
|
|
1264
|
-
execSync3('git commit -m "Initial commit from create-thally-docs"', { cwd: targetDir, stdio: "inherit" });
|
|
1265
|
-
} catch {
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
async function migrateDocs(opts) {
|
|
1269
|
-
const { sourceUrl, projectDir: rawProjectDir, into, apiKey, projectName } = opts;
|
|
1270
|
-
const projectDir = resolve2(rawProjectDir);
|
|
1271
|
-
const source = parseGitHubUrl(sourceUrl);
|
|
1272
|
-
if (opts.branch) source.branch = opts.branch;
|
|
1273
|
-
if (!into) {
|
|
1274
|
-
console.log(` \u{1F3D7} Scaffolding new project at ${projectDir}...`);
|
|
1275
|
-
await scaffold({
|
|
1276
|
-
projectDir,
|
|
1277
|
-
projectName: projectName ?? "My Docs",
|
|
1278
|
-
description: `Documentation migrated from ${source.owner}/${source.repo}`,
|
|
1279
|
-
brandPreset: "primary",
|
|
1280
|
-
repoUrl: `https://github.com/${source.owner}/${source.repo}`,
|
|
1281
|
-
doInstall: false
|
|
1282
|
-
});
|
|
1283
|
-
} else {
|
|
1284
|
-
if (!existsSync6(projectDir)) {
|
|
1285
|
-
throw new Error(`Project directory "${projectDir}" does not exist.`);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
const tmpBase = mkdtempSync(join7(tmpdir(), "thally-migrate-"));
|
|
1289
|
-
const cloneDir = join7(tmpBase, "repo");
|
|
1290
|
-
console.log(` \u{1F4E6} Cloning ${source.owner}/${source.repo}...`);
|
|
1291
|
-
try {
|
|
1292
|
-
await cloneRepo(source, cloneDir);
|
|
1293
|
-
const docsDir = opts.docsDir ?? (source.docsDir || detectDocsDir(cloneDir));
|
|
1294
|
-
const docFiles = findDocFiles(cloneDir, docsDir);
|
|
1295
|
-
const docsDirLabel = docsDir ? `${docsDir}/` : "repo root";
|
|
1296
|
-
console.log(` \u{1F4C4} Found ${docFiles.length} files in ${docsDirLabel}`);
|
|
1297
|
-
if (docFiles.length === 0) {
|
|
1298
|
-
console.warn(" \u26A0 No doc files found. Check the URL and try again.");
|
|
1299
|
-
return { pagesWritten: 0, projectDir };
|
|
1300
|
-
}
|
|
1301
|
-
const platform = detectPlatform(cloneDir);
|
|
1302
|
-
const detectedNav = detectNavFromConfig(cloneDir, docsDir, platform);
|
|
1303
|
-
const openApiSpec = detectOpenApiSpec(cloneDir);
|
|
1304
|
-
if (openApiSpec) {
|
|
1305
|
-
console.log(` \u{1F50C} Found OpenAPI spec: ${openApiSpec.filename}`);
|
|
1306
|
-
}
|
|
1307
|
-
const limit = pLimit(5);
|
|
1308
|
-
let doneCount = 0;
|
|
1309
|
-
const imported = (await Promise.all(
|
|
1310
|
-
docFiles.map(
|
|
1311
|
-
(file) => limit(async () => {
|
|
1312
|
-
try {
|
|
1313
|
-
const result = await importFile(file, apiKey);
|
|
1314
|
-
doneCount++;
|
|
1315
|
-
if (result) {
|
|
1316
|
-
console.log(` [${doneCount}/${docFiles.length}] ${result.pageId}`);
|
|
1317
|
-
} else {
|
|
1318
|
-
console.log(` [${doneCount}/${docFiles.length}] ${file.pageId} (openapi \u2014 wired via spec)`);
|
|
1319
|
-
}
|
|
1320
|
-
return result;
|
|
1321
|
-
} catch (err) {
|
|
1322
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1323
|
-
if (msg.includes("no API key")) {
|
|
1324
|
-
console.warn(` \u26A0 ${msg}`);
|
|
1325
|
-
} else {
|
|
1326
|
-
console.warn(` \u26A0 Skipping ${file.relPath}: ${msg}`);
|
|
1327
|
-
}
|
|
1328
|
-
doneCount++;
|
|
1329
|
-
return null;
|
|
1330
|
-
}
|
|
1331
|
-
})
|
|
1332
|
-
)
|
|
1333
|
-
)).filter(Boolean);
|
|
1334
|
-
const pageIdSeen = /* @__PURE__ */ new Set();
|
|
1335
|
-
const deduped = imported.filter((p) => {
|
|
1336
|
-
if (pageIdSeen.has(p.pageId)) return false;
|
|
1337
|
-
pageIdSeen.add(p.pageId);
|
|
1338
|
-
return true;
|
|
1339
|
-
});
|
|
1340
|
-
const contentDir = join7(projectDir, "src", "content");
|
|
1341
|
-
let pagesWritten = 0;
|
|
1342
|
-
for (const page of deduped) {
|
|
1343
|
-
const filePath = join7(contentDir, `${page.pageId}.mdx`);
|
|
1344
|
-
mkdirSync3(dirname2(filePath), { recursive: true });
|
|
1345
|
-
const mdx = [
|
|
1346
|
-
"---",
|
|
1347
|
-
`title: "${page.frontmatter.title.replace(/"/g, '\\"')}"`,
|
|
1348
|
-
`description: "${page.frontmatter.description.replace(/"/g, '\\"')}"`,
|
|
1349
|
-
page.frontmatter.keywords.length > 0 ? `keywords: [${page.frontmatter.keywords.map((k) => `"${k.replace(/"/g, '\\"')}"`).join(", ")}]` : null,
|
|
1350
|
-
"---",
|
|
1351
|
-
"",
|
|
1352
|
-
page.body
|
|
1353
|
-
].filter((line) => line !== null).join("\n");
|
|
1354
|
-
writeFileSync5(filePath, mdx, "utf8");
|
|
1355
|
-
pagesWritten++;
|
|
1356
|
-
}
|
|
1357
|
-
let finalNav = detectedNav ?? buildNavStructure(deduped);
|
|
1358
|
-
if (openApiSpec) {
|
|
1359
|
-
const publicDir = join7(projectDir, "public");
|
|
1360
|
-
mkdirSync3(publicDir, { recursive: true });
|
|
1361
|
-
copyFileSync(openApiSpec.absPath, join7(publicDir, openApiSpec.filename));
|
|
1362
|
-
console.log(` \u{1F4CB} Copied ${openApiSpec.filename} \u2192 public/${openApiSpec.filename}`);
|
|
1363
|
-
finalNav = injectApiTab(finalNav, openApiSpec.filename);
|
|
1364
|
-
}
|
|
1365
|
-
if (into && existsSync6(join7(projectDir, "docs.json"))) {
|
|
1366
|
-
const existing = readDocsJson(projectDir);
|
|
1367
|
-
const merged = mergeDocsJson(existing, finalNav);
|
|
1368
|
-
writeDocsJson(projectDir, merged);
|
|
1369
|
-
} else {
|
|
1370
|
-
writeDocsJson(projectDir, finalNav);
|
|
1371
|
-
}
|
|
1372
|
-
if (!into) {
|
|
1373
|
-
installDeps2(projectDir);
|
|
1374
|
-
initGit2(projectDir);
|
|
1375
|
-
}
|
|
1376
|
-
return { pagesWritten, projectDir };
|
|
1377
|
-
} finally {
|
|
1378
|
-
rmSync(tmpBase, { recursive: true, force: true });
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// src/tools/migrate-docs.ts
|
|
539
|
+
import { migrateDocs } from "create-thally-docs/migrate";
|
|
1383
540
|
var migrateDocsSchema = z6.object({
|
|
1384
|
-
sourceUrl: z6.string().describe("GitHub URL
|
|
541
|
+
sourceUrl: z6.string().describe("GitHub repository URL or public documentation URL to migrate"),
|
|
1385
542
|
projectDir: z6.string().describe("Path for new project or existing project dir"),
|
|
1386
543
|
into: z6.boolean().optional().default(false).describe("Migrate into existing project instead of scaffolding"),
|
|
1387
544
|
branch: z6.string().optional().describe("Git branch (default: auto-detect)"),
|
|
1388
545
|
docsDir: z6.string().optional().describe("Docs subdirectory in repo (default: auto-detect)"),
|
|
1389
|
-
apiKey: z6.string().optional().describe("Anthropic API key for non-Markdown file conversion")
|
|
546
|
+
apiKey: z6.string().optional().describe("Anthropic API key for non-Markdown file conversion"),
|
|
547
|
+
maxPages: z6.number().int().min(1).max(1e3).optional().describe("Maximum public URL pages to import")
|
|
1390
548
|
});
|
|
1391
549
|
async function handleMigrateDocs(input) {
|
|
1392
550
|
const apiKey = input.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
@@ -1397,6 +555,7 @@ async function handleMigrateDocs(input) {
|
|
|
1397
555
|
apiKey,
|
|
1398
556
|
branch: input.branch,
|
|
1399
557
|
docsDir: input.docsDir,
|
|
558
|
+
maxPages: input.maxPages,
|
|
1400
559
|
yes: true
|
|
1401
560
|
});
|
|
1402
561
|
return `Migration complete! ${result.pagesWritten} pages written to ${result.projectDir}/src/content/`;
|
|
@@ -1404,9 +563,9 @@ async function handleMigrateDocs(input) {
|
|
|
1404
563
|
|
|
1405
564
|
// src/tools/search-docs.ts
|
|
1406
565
|
import { z as z7 } from "zod";
|
|
1407
|
-
import { readdirSync as
|
|
1408
|
-
import { join as
|
|
1409
|
-
import
|
|
566
|
+
import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
567
|
+
import { join as join5, relative, extname } from "path";
|
|
568
|
+
import matter2 from "gray-matter";
|
|
1410
569
|
var searchDocsSchema = z7.object({
|
|
1411
570
|
projectDir: z7.string().describe("Path to the Thally project root"),
|
|
1412
571
|
query: z7.string().describe("Search query"),
|
|
@@ -1415,17 +574,17 @@ var searchDocsSchema = z7.object({
|
|
|
1415
574
|
function scanMdxFiles(dir, results) {
|
|
1416
575
|
let entries;
|
|
1417
576
|
try {
|
|
1418
|
-
entries =
|
|
577
|
+
entries = readdirSync2(dir);
|
|
1419
578
|
} catch {
|
|
1420
579
|
return;
|
|
1421
580
|
}
|
|
1422
581
|
for (const entry of entries) {
|
|
1423
|
-
const fullPath =
|
|
582
|
+
const fullPath = join5(dir, entry);
|
|
1424
583
|
try {
|
|
1425
|
-
const stat =
|
|
584
|
+
const stat = statSync(fullPath);
|
|
1426
585
|
if (stat.isDirectory()) {
|
|
1427
586
|
scanMdxFiles(fullPath, results);
|
|
1428
|
-
} else if (
|
|
587
|
+
} else if (extname(entry).toLowerCase() === ".mdx") {
|
|
1429
588
|
results.push(fullPath);
|
|
1430
589
|
}
|
|
1431
590
|
} catch {
|
|
@@ -1438,15 +597,15 @@ function scoreFiles(files, contentDir, query) {
|
|
|
1438
597
|
for (const filePath of files) {
|
|
1439
598
|
let raw;
|
|
1440
599
|
try {
|
|
1441
|
-
raw =
|
|
600
|
+
raw = readFileSync4(filePath, "utf8");
|
|
1442
601
|
} catch {
|
|
1443
602
|
continue;
|
|
1444
603
|
}
|
|
1445
|
-
const { data, content } =
|
|
604
|
+
const { data, content } = matter2(raw);
|
|
1446
605
|
const title = data.title ?? "";
|
|
1447
606
|
const description = data.description ?? "";
|
|
1448
607
|
const keywords = data.keywords ?? [];
|
|
1449
|
-
const pageId =
|
|
608
|
+
const pageId = relative(contentDir, filePath).replace(/\.mdx$/, "").replace(/\\/g, "/");
|
|
1450
609
|
let score = 0;
|
|
1451
610
|
for (const term of terms) {
|
|
1452
611
|
if (title.toLowerCase().includes(term)) score += 3;
|
|
@@ -1463,8 +622,8 @@ function scoreFiles(files, contentDir, query) {
|
|
|
1463
622
|
}
|
|
1464
623
|
async function handleSearchDocs(input) {
|
|
1465
624
|
const { projectDir, query, limit = 5 } = input;
|
|
1466
|
-
const contentDir =
|
|
1467
|
-
if (!
|
|
625
|
+
const contentDir = join5(projectDir, "src", "content");
|
|
626
|
+
if (!existsSync4(contentDir)) {
|
|
1468
627
|
throw new Error(`Content directory not found: ${contentDir}`);
|
|
1469
628
|
}
|
|
1470
629
|
const files = [];
|
|
@@ -1559,23 +718,23 @@ async function handleAgentReadiness(input) {
|
|
|
1559
718
|
|
|
1560
719
|
// src/tools/read-page.ts
|
|
1561
720
|
import { z as z10 } from "zod";
|
|
1562
|
-
import { existsSync as
|
|
1563
|
-
import { join as
|
|
1564
|
-
import
|
|
721
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
722
|
+
import { join as join6 } from "path";
|
|
723
|
+
import matter3 from "gray-matter";
|
|
1565
724
|
var readPageSchema = z10.object({
|
|
1566
725
|
projectDir: z10.string().describe("Path to the Thally project root"),
|
|
1567
726
|
pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
|
|
1568
727
|
});
|
|
1569
728
|
async function handleReadPage(input) {
|
|
1570
729
|
const { projectDir, pageId } = input;
|
|
1571
|
-
const contentDir =
|
|
730
|
+
const contentDir = join6(projectDir, "src", "content");
|
|
1572
731
|
const candidates = [
|
|
1573
|
-
|
|
1574
|
-
|
|
732
|
+
join6(contentDir, `${pageId}.mdx`),
|
|
733
|
+
join6(contentDir, `${pageId}/index.mdx`)
|
|
1575
734
|
];
|
|
1576
735
|
let filePath = null;
|
|
1577
736
|
for (const c of candidates) {
|
|
1578
|
-
if (
|
|
737
|
+
if (existsSync5(c)) {
|
|
1579
738
|
filePath = c;
|
|
1580
739
|
break;
|
|
1581
740
|
}
|
|
@@ -1583,8 +742,8 @@ async function handleReadPage(input) {
|
|
|
1583
742
|
if (!filePath) {
|
|
1584
743
|
throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
|
|
1585
744
|
}
|
|
1586
|
-
const raw =
|
|
1587
|
-
const { data, content } =
|
|
745
|
+
const raw = readFileSync5(filePath, "utf8");
|
|
746
|
+
const { data, content } = matter3(raw);
|
|
1588
747
|
const title = data.title ?? pageId;
|
|
1589
748
|
const description = data.description ?? "";
|
|
1590
749
|
const lines = [`# ${title}`, `*${pageId}*`, ""];
|
|
@@ -1598,9 +757,9 @@ async function handleReadPage(input) {
|
|
|
1598
757
|
|
|
1599
758
|
// src/tools/get-context.ts
|
|
1600
759
|
import { z as z11 } from "zod";
|
|
1601
|
-
import { existsSync as
|
|
1602
|
-
import { join as
|
|
1603
|
-
import
|
|
760
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
761
|
+
import { join as join7 } from "path";
|
|
762
|
+
import matter4 from "gray-matter";
|
|
1604
763
|
var getContextSchema = z11.object({
|
|
1605
764
|
projectDir: z11.string().describe("Path to the Thally project root"),
|
|
1606
765
|
topic: z11.string().describe("Topic or question to find relevant docs for"),
|
|
@@ -1608,8 +767,8 @@ var getContextSchema = z11.object({
|
|
|
1608
767
|
});
|
|
1609
768
|
async function handleGetContext(input) {
|
|
1610
769
|
const { projectDir, topic, maxTokens = 4e3 } = input;
|
|
1611
|
-
const contentDir =
|
|
1612
|
-
if (!
|
|
770
|
+
const contentDir = join7(projectDir, "src", "content");
|
|
771
|
+
if (!existsSync6(contentDir)) {
|
|
1613
772
|
throw new Error(`Content directory not found: ${contentDir}`);
|
|
1614
773
|
}
|
|
1615
774
|
const files = [];
|
|
@@ -1623,14 +782,14 @@ async function handleGetContext(input) {
|
|
|
1623
782
|
const sections = [];
|
|
1624
783
|
for (const result of scored) {
|
|
1625
784
|
const candidates = [
|
|
1626
|
-
|
|
1627
|
-
|
|
785
|
+
join7(contentDir, `${result.pageId}.mdx`),
|
|
786
|
+
join7(contentDir, `${result.pageId}/index.mdx`)
|
|
1628
787
|
];
|
|
1629
788
|
let content = "";
|
|
1630
789
|
for (const c of candidates) {
|
|
1631
|
-
if (
|
|
1632
|
-
const raw =
|
|
1633
|
-
const { content: body } =
|
|
790
|
+
if (existsSync6(c)) {
|
|
791
|
+
const raw = readFileSync6(c, "utf8");
|
|
792
|
+
const { content: body } = matter4(raw);
|
|
1634
793
|
content = body.trim();
|
|
1635
794
|
break;
|
|
1636
795
|
}
|
|
@@ -1654,9 +813,9 @@ async function handleGetContext(input) {
|
|
|
1654
813
|
|
|
1655
814
|
// src/tools/lint-project.ts
|
|
1656
815
|
import { z as z12 } from "zod";
|
|
1657
|
-
import { existsSync as
|
|
1658
|
-
import { join as
|
|
1659
|
-
import
|
|
816
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
817
|
+
import { join as join8 } from "path";
|
|
818
|
+
import matter5 from "gray-matter";
|
|
1660
819
|
var lintProjectSchema = z12.object({
|
|
1661
820
|
projectDir: z12.string().describe("Path to the Thally project root"),
|
|
1662
821
|
fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
|
|
@@ -1687,9 +846,9 @@ function addOrphanToNav(projectDir, pageId) {
|
|
|
1687
846
|
}
|
|
1688
847
|
async function handleLintProject(input) {
|
|
1689
848
|
const { projectDir, fix = false } = input;
|
|
1690
|
-
const contentDir =
|
|
849
|
+
const contentDir = join8(projectDir, "src", "content");
|
|
1691
850
|
const issues = [];
|
|
1692
|
-
if (!
|
|
851
|
+
if (!existsSync7(join8(projectDir, "docs.json"))) {
|
|
1693
852
|
throw new Error(`Not a Thally project: docs.json not found in ${projectDir}`);
|
|
1694
853
|
}
|
|
1695
854
|
const config = readDocsJson(projectDir);
|
|
@@ -1708,10 +867,10 @@ async function handleLintProject(input) {
|
|
|
1708
867
|
}
|
|
1709
868
|
for (const pageId of navPageIds) {
|
|
1710
869
|
const candidates = [
|
|
1711
|
-
|
|
1712
|
-
|
|
870
|
+
join8(contentDir, `${pageId}.mdx`),
|
|
871
|
+
join8(contentDir, `${pageId}/index.mdx`)
|
|
1713
872
|
];
|
|
1714
|
-
if (!candidates.some((c) =>
|
|
873
|
+
if (!candidates.some((c) => existsSync7(c))) {
|
|
1715
874
|
issues.push({
|
|
1716
875
|
severity: "error",
|
|
1717
876
|
message: `"${pageId}" is in docs.json but has no MDX file`,
|
|
@@ -1720,7 +879,7 @@ async function handleLintProject(input) {
|
|
|
1720
879
|
}
|
|
1721
880
|
}
|
|
1722
881
|
const allFiles = [];
|
|
1723
|
-
if (
|
|
882
|
+
if (existsSync7(contentDir)) {
|
|
1724
883
|
scanMdxFiles(contentDir, allFiles);
|
|
1725
884
|
}
|
|
1726
885
|
const fixedOrphans = [];
|
|
@@ -1738,8 +897,8 @@ async function handleLintProject(input) {
|
|
|
1738
897
|
let data = {};
|
|
1739
898
|
let content = "";
|
|
1740
899
|
try {
|
|
1741
|
-
const raw =
|
|
1742
|
-
const parsed =
|
|
900
|
+
const raw = readFileSync7(filePath, "utf8");
|
|
901
|
+
const parsed = matter5(raw);
|
|
1743
902
|
data = parsed.data;
|
|
1744
903
|
content = parsed.content;
|
|
1745
904
|
} catch {
|
|
@@ -1795,11 +954,11 @@ async function handleLintProject(input) {
|
|
|
1795
954
|
|
|
1796
955
|
// src/tools/translate-docs.ts
|
|
1797
956
|
import { z as z13 } from "zod";
|
|
1798
|
-
import { readFileSync as
|
|
1799
|
-
import { join as
|
|
1800
|
-
import
|
|
1801
|
-
import
|
|
1802
|
-
import
|
|
957
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
|
|
958
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
959
|
+
import matter6 from "gray-matter";
|
|
960
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
961
|
+
import pLimit from "p-limit";
|
|
1803
962
|
var translateDocsSchema = z13.object({
|
|
1804
963
|
projectDir: z13.string().describe("Path to the Thally project directory"),
|
|
1805
964
|
locale: z13.string().describe('Target locale code, e.g. "es", "fr"'),
|
|
@@ -1809,8 +968,8 @@ var translateDocsSchema = z13.object({
|
|
|
1809
968
|
model: z13.string().optional().default("claude-sonnet-4-6").describe("Claude model to use for translation")
|
|
1810
969
|
});
|
|
1811
970
|
function readDocsJson2(projectDir) {
|
|
1812
|
-
const docsPath =
|
|
1813
|
-
const raw =
|
|
971
|
+
const docsPath = join9(projectDir, "docs.json");
|
|
972
|
+
const raw = readFileSync8(docsPath, "utf8");
|
|
1814
973
|
return JSON.parse(raw);
|
|
1815
974
|
}
|
|
1816
975
|
function collectPageIds(pages) {
|
|
@@ -1852,12 +1011,12 @@ function getAllPageIds(config) {
|
|
|
1852
1011
|
return { ids, hrefOnlyPages };
|
|
1853
1012
|
}
|
|
1854
1013
|
function findSourceFile(projectDir, pageId) {
|
|
1855
|
-
const contentRoot =
|
|
1014
|
+
const contentRoot = join9(projectDir, "src", "content");
|
|
1856
1015
|
const candidates = [
|
|
1857
|
-
|
|
1858
|
-
|
|
1016
|
+
join9(contentRoot, `${pageId}.mdx`),
|
|
1017
|
+
join9(contentRoot, `${pageId}/index.mdx`)
|
|
1859
1018
|
];
|
|
1860
|
-
return candidates.find((p) =>
|
|
1019
|
+
return candidates.find((p) => existsSync8(p)) ?? null;
|
|
1861
1020
|
}
|
|
1862
1021
|
var TRANSLATION_SYSTEM_PROMPT = `You are a professional documentation translator. You will receive an MDX documentation file and translate it into the target language.
|
|
1863
1022
|
|
|
@@ -1909,7 +1068,7 @@ async function handleTranslateDocs(input) {
|
|
|
1909
1068
|
}
|
|
1910
1069
|
const { ids: allPageIds, hrefOnlyPages } = getAllPageIds(config);
|
|
1911
1070
|
const targetPageIds = pages ?? allPageIds;
|
|
1912
|
-
const contentRoot =
|
|
1071
|
+
const contentRoot = join9(projectDir, "src", "content");
|
|
1913
1072
|
const toTranslate = [];
|
|
1914
1073
|
const skipped = [];
|
|
1915
1074
|
for (const pageId of targetPageIds) {
|
|
@@ -1919,8 +1078,8 @@ async function handleTranslateDocs(input) {
|
|
|
1919
1078
|
continue;
|
|
1920
1079
|
}
|
|
1921
1080
|
const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
|
|
1922
|
-
const targetFile =
|
|
1923
|
-
if (
|
|
1081
|
+
const targetFile = join9(contentRoot, locale, relativeFromContent);
|
|
1082
|
+
if (existsSync8(targetFile) && !force) {
|
|
1924
1083
|
skipped.push(`${pageId} (already translated)`);
|
|
1925
1084
|
continue;
|
|
1926
1085
|
}
|
|
@@ -1929,21 +1088,21 @@ async function handleTranslateDocs(input) {
|
|
|
1929
1088
|
if (toTranslate.length === 0) {
|
|
1930
1089
|
return `Nothing to translate. ${skipped.length} page(s) skipped.`;
|
|
1931
1090
|
}
|
|
1932
|
-
const client = new
|
|
1933
|
-
const limit =
|
|
1091
|
+
const client = new Anthropic({ apiKey });
|
|
1092
|
+
const limit = pLimit(3);
|
|
1934
1093
|
const results = [];
|
|
1935
1094
|
await Promise.all(
|
|
1936
1095
|
toTranslate.map(
|
|
1937
1096
|
({ pageId, sourceFile, targetFile }) => limit(async () => {
|
|
1938
1097
|
try {
|
|
1939
|
-
const sourceContent =
|
|
1940
|
-
const parsed =
|
|
1098
|
+
const sourceContent = readFileSync8(sourceFile, "utf8");
|
|
1099
|
+
const parsed = matter6(sourceContent);
|
|
1941
1100
|
if (!parsed.data.title) {
|
|
1942
1101
|
console.warn(`[translate] ${pageId}: missing title in frontmatter`);
|
|
1943
1102
|
}
|
|
1944
1103
|
const translated = await translatePage(sourceContent, targetLocale.label, locale, model, client);
|
|
1945
|
-
|
|
1946
|
-
|
|
1104
|
+
mkdirSync3(dirname2(targetFile), { recursive: true });
|
|
1105
|
+
writeFileSync6(targetFile, translated + "\n", "utf8");
|
|
1947
1106
|
results.push({ pageId, success: true });
|
|
1948
1107
|
} catch (err) {
|
|
1949
1108
|
const msg = err instanceof Error ? err.message : String(err);
|