Files
openclaw/scripts/docs-sync-publish.mjs
Peter Steinberger ce0fafefce fix(tooling): avoid build and test startup stalls on shared tsx caches (#130924)
* fix(tooling): avoid shared tsx cache startup stalls

Use the shared preloader before tsx initializes so maintained build, check, and test commands retain memory transforms without scanning other checkouts' disk caches. Preserve changed-cwd forks and copied tooling closures.

* test: align command contracts with tooling bootstrap
2026-08-27 10:36:12 -07:00

887 lines
24 KiB
JavaScript

#!/usr/bin/env node
// Syncs source docs into the generated publish tree.
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { renderDocsHeadingMap } from "./docs-list.js";
import { repairMintlifyAccordionIndentation } from "./lib/mintlify-accordion.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const ROOT = resolveRepoRoot(import.meta.url);
const SOURCE_DOCS_DIR = path.join(ROOT, "docs");
const SOURCE_CONFIG_PATH = path.join(SOURCE_DOCS_DIR, "docs.json");
const INTERNAL_DOCS_DIRS = ["internal"];
const DEFAULT_CLAWHUB_SOURCE_REPO = "openclaw/clawhub";
const CLAWHUB_DOCS_TARGET_DIR = "clawhub";
const CLAWHUB_REPO_ENV = "OPENCLAW_DOCS_SYNC_CLAWHUB_REPO";
const DEFAULT_CLAWHUB_REPO_CANDIDATES = [
path.resolve(ROOT, "..", "clawhub-docs-clawhub"),
path.resolve(ROOT, "..", "clawhub"),
];
const SYNC_SUPPORT_FILES = [
{
source: path.join(ROOT, "scripts", "check-docs-mdx.mjs"),
target: path.join(".openclaw-sync", "check-docs-mdx.mjs"),
},
{
source: path.join(ROOT, "scripts", "check-docs-mdx.mts"),
target: path.join(".openclaw-sync", "check-docs-mdx.mts"),
},
{
source: path.join(ROOT, "scripts", "lib", "tsx-cli-shim.mjs"),
target: path.join(".openclaw-sync", "lib", "tsx-cli-shim.mjs"),
},
{
source: path.join(ROOT, "scripts", "lib", "local-check-runtime.mts"),
target: path.join(".openclaw-sync", "lib", "local-check-runtime.mts"),
},
{
source: path.join(ROOT, "scripts", "tsx.mjs"),
target: path.join(".openclaw-sync", "tsx.mjs"),
},
{
source: path.join(ROOT, "scripts", "lib", "mintlify-accordion.mjs"),
target: path.join(".openclaw-sync", "lib", "mintlify-accordion.mjs"),
},
{
source: path.join(ROOT, ".github", "codex", "prompts", "docs-mdx-repair.md"),
target: path.join(".openclaw-sync", "docs-mdx-repair.md"),
},
];
const GENERATED_LOCALES = [
{
language: "zh-Hans",
dir: "zh-CN",
navFile: "zh-Hans-navigation.json",
tmFile: "zh-CN.tm.jsonl",
navMode: "overlay",
},
{
language: "zh-Hant",
dir: "zh-TW",
navFile: "zh-Hant-navigation.json",
tmFile: "zh-TW.tm.jsonl",
navMode: "clone-en",
},
{
language: "ja",
dir: "ja-JP",
navFile: "ja-navigation.json",
tmFile: "ja-JP.tm.jsonl",
navMode: "clone-en",
},
{
language: "es",
dir: "es",
navFile: "es-navigation.json",
tmFile: "es.tm.jsonl",
navMode: "clone-en",
},
{
language: "pt-BR",
dir: "pt-BR",
navFile: "pt-BR-navigation.json",
tmFile: "pt-BR.tm.jsonl",
navMode: "clone-en",
},
{
language: "ko",
dir: "ko",
navFile: "ko-navigation.json",
tmFile: "ko.tm.jsonl",
navMode: "clone-en",
},
{
language: "de",
dir: "de",
navFile: "de-navigation.json",
tmFile: "de.tm.jsonl",
navMode: "clone-en",
},
{
language: "fr",
dir: "fr",
navFile: "fr-navigation.json",
tmFile: "fr.tm.jsonl",
navMode: "clone-en",
},
{
language: "hi",
dir: "hi",
navFile: "hi-navigation.json",
tmFile: "hi.tm.jsonl",
navMode: "clone-en",
},
{
language: "ar",
dir: "ar",
navFile: "ar-navigation.json",
tmFile: "ar.tm.jsonl",
navMode: "clone-en",
},
{
language: "it",
dir: "it",
navFile: "it-navigation.json",
tmFile: "it.tm.jsonl",
navMode: "clone-en",
},
{
language: "vi",
dir: "vi",
navFile: "vi-navigation.json",
tmFile: "vi.tm.jsonl",
navMode: "clone-en",
},
{
language: "nl",
dir: "nl",
navFile: "nl-navigation.json",
tmFile: "nl.tm.jsonl",
navMode: "clone-en",
},
{
language: "fa",
dir: "fa",
navFile: "fa-navigation.json",
tmFile: "fa.tm.jsonl",
navMode: "clone-en",
// Mintlify does not currently accept `fa` in navigation.languages.
// Keep generated docs and translation memory so the locale stays available
// once the docs host accepts it.
navigation: false,
},
{
language: "tr",
dir: "tr",
navFile: "tr-navigation.json",
tmFile: "tr.tm.jsonl",
navMode: "clone-en",
},
{
language: "uk",
dir: "uk",
navFile: "uk-navigation.json",
tmFile: "uk.tm.jsonl",
navMode: "clone-en",
},
{
language: "id",
dir: "id",
navFile: "id-navigation.json",
tmFile: "id.tm.jsonl",
navMode: "clone-en",
},
{
language: "pl",
dir: "pl",
navFile: "pl-navigation.json",
tmFile: "pl.tm.jsonl",
navMode: "clone-en",
},
{
language: "th",
dir: "th",
navFile: "th-navigation.json",
tmFile: "th.tm.jsonl",
navMode: "clone-en",
// Mintlify does not currently accept `th` in navigation.languages.
// Keep generated docs and translation memory so the locale stays available
// once the docs host accepts it.
navigation: false,
},
{
language: "ru",
dir: "ru",
navFile: "ru-navigation.json",
tmFile: "ru.tm.jsonl",
navMode: "clone-en",
},
];
function readOptionValue(argv, index, optionName) {
const value = argv[index + 1];
if (value === undefined || value === "" || value.startsWith("-")) {
throw new Error(`${optionName} requires a value`);
}
return value;
}
export function parseArgs(argv) {
const args = {
target: "",
sourceRepo: "",
sourceSha: "",
clawhubRepo: process.env[CLAWHUB_REPO_ENV] || "",
clawhubSourceRepo:
process.env.OPENCLAW_DOCS_SYNC_CLAWHUB_SOURCE_REPO || DEFAULT_CLAWHUB_SOURCE_REPO,
clawhubSourceSha: process.env.OPENCLAW_DOCS_SYNC_CLAWHUB_SOURCE_SHA || "",
};
for (let index = 0; index < argv.length; index += 1) {
const part = argv[index];
switch (part) {
case "--target":
args.target = readOptionValue(argv, index, part);
index += 1;
break;
case "--source-repo":
args.sourceRepo = readOptionValue(argv, index, part);
index += 1;
break;
case "--source-sha":
args.sourceSha = readOptionValue(argv, index, part);
index += 1;
break;
case "--clawhub-repo":
args.clawhubRepo = readOptionValue(argv, index, part);
index += 1;
break;
case "--clawhub-source-repo":
args.clawhubSourceRepo = readOptionValue(argv, index, part);
index += 1;
break;
case "--clawhub-source-sha":
args.clawhubSourceSha = readOptionValue(argv, index, part);
index += 1;
break;
default:
throw new Error(`unknown arg: ${part}`);
}
}
if (!args.target) {
throw new Error("missing --target");
}
return args;
}
function run(command, args, options = {}) {
execFileSync(command, args, {
cwd: ROOT,
stdio: "inherit",
...options,
});
}
function ensureDir(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
}
function normalizeSlashes(value) {
return value.replace(/\\/g, "/");
}
function walkFiles(entryPath, out = []) {
if (!fs.existsSync(entryPath)) {
return out;
}
const stat = fs.statSync(entryPath);
if (stat.isFile()) {
out.push(entryPath);
return out;
}
for (const entry of fs.readdirSync(entryPath, { withFileTypes: true })) {
if (entry.name === "node_modules" || entry.name === ".git") {
continue;
}
walkFiles(path.join(entryPath, entry.name), out);
}
return out;
}
function walkMarkdownFiles(entryPath, out = []) {
if (!fs.existsSync(entryPath)) {
return out;
}
const stat = fs.statSync(entryPath);
if (stat.isFile()) {
if (/\.mdx?$/i.test(entryPath)) {
out.push(entryPath);
}
return out;
}
for (const entry of fs.readdirSync(entryPath, { withFileTypes: true })) {
if (entry.name === "node_modules" || entry.name === ".git") {
continue;
}
walkMarkdownFiles(path.join(entryPath, entry.name), out);
}
return out;
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function writeJson(filePath, value) {
ensureDir(path.dirname(filePath));
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
}
function getGitHeadSha(repoPath) {
try {
return execFileSync("git", ["-C", repoPath, "rev-parse", "HEAD"], {
cwd: ROOT,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
} catch {
return "";
}
}
/**
* Resolves the local ClawHub repository path used for docs mirroring.
*/
export function resolveClawHubRepoPath(value = "", options = {}) {
const required = options.required !== false;
const candidates = [
value,
process.env[CLAWHUB_REPO_ENV] || "",
...DEFAULT_CLAWHUB_REPO_CANDIDATES,
].filter((candidate) => candidate.trim().length > 0);
for (const candidate of candidates) {
const repoPath = path.resolve(candidate);
if (fs.existsSync(path.join(repoPath, "docs"))) {
return repoPath;
}
}
if (required) {
throw new Error(`missing ClawHub docs source; pass --clawhub-repo or set ${CLAWHUB_REPO_ENV}`);
}
return "";
}
function prefixLocalePage(entry, localeDir) {
if (typeof entry === "string") {
return `${localeDir}/${entry}`;
}
if (Array.isArray(entry)) {
return entry.map((item) => prefixLocalePage(item, localeDir));
}
if (!entry || typeof entry !== "object") {
return entry;
}
const clone = { ...entry };
if (typeof clone.page === "string") {
clone.page = `${localeDir}/${clone.page}`;
}
if (Array.isArray(clone.pages)) {
clone.pages = clone.pages.map((item) => prefixLocalePage(item, localeDir));
}
return clone;
}
function prefixLocaleNavGroup(group, localeDir) {
const clone = { ...group };
if (Array.isArray(clone.pages)) {
clone.pages = clone.pages.map((entry) => prefixLocalePage(entry, localeDir));
}
return clone;
}
function prefixLocaleNavTab(tab, localeDir) {
const clone = { ...tab };
if (Array.isArray(clone.pages)) {
clone.pages = clone.pages.map((entry) => prefixLocalePage(entry, localeDir));
}
if (Array.isArray(clone.groups)) {
clone.groups = clone.groups.map((group) => prefixLocaleNavGroup(group, localeDir));
}
return clone;
}
function cloneEnglishLanguageNav(englishNav, locale) {
if (!englishNav) {
throw new Error("docs/docs.json is missing navigation.languages.en");
}
return {
...englishNav,
language: locale.language,
tabs: Array.isArray(englishNav.tabs)
? englishNav.tabs
.filter((tab) => tab?.tab !== "ClawHub")
.map((tab) => prefixLocaleNavTab(tab, locale.dir))
: englishNav.tabs,
};
}
function collectNavPages(entry, pages = new Set()) {
if (typeof entry === "string") {
pages.add(entry);
return pages;
}
if (Array.isArray(entry)) {
for (const item of entry) {
collectNavPages(item, pages);
}
return pages;
}
if (!entry || typeof entry !== "object") {
return pages;
}
if (typeof entry.page === "string") {
pages.add(entry.page);
}
collectNavPages(entry.pages, pages);
collectNavPages(entry.groups, pages);
collectNavPages(entry.tabs, pages);
return pages;
}
function findBestNavMatchIndex(candidates, overlayEntry, excludedIndexes = new Set()) {
const overlayPages = collectNavPages(overlayEntry);
let bestIndex = -1;
let bestScore = 0;
for (const [index, candidate] of candidates.entries()) {
if (excludedIndexes.has(index)) {
continue;
}
const candidatePages = collectNavPages(candidate);
let score = 0;
for (const page of overlayPages) {
if (candidatePages.has(page)) {
score += 1;
}
}
if (score > bestScore) {
bestIndex = index;
bestScore = score;
}
}
return bestIndex;
}
export function applyLocaleNavLabelOverlay(fullNav, labelOverlay) {
const tabs = Array.isArray(fullNav.tabs)
? fullNav.tabs.map((tab) => ({
...tab,
groups: Array.isArray(tab.groups) ? tab.groups.map((group) => ({ ...group })) : tab.groups,
}))
: fullNav.tabs;
const composed = { ...fullNav, tabs };
if (!Array.isArray(tabs) || !Array.isArray(labelOverlay?.tabs)) {
return composed;
}
for (const overlayTab of labelOverlay.tabs) {
const tabIndex = findBestNavMatchIndex(tabs, overlayTab);
if (tabIndex < 0) {
continue;
}
const tab = tabs[tabIndex];
if (typeof overlayTab.tab === "string") {
tab.tab = overlayTab.tab;
}
if (!Array.isArray(tab.groups) || !Array.isArray(overlayTab.groups)) {
continue;
}
const matchedGroupIndexes = new Set();
for (const overlayGroup of overlayTab.groups) {
const groupIndex = findBestNavMatchIndex(tab.groups, overlayGroup, matchedGroupIndexes);
if (groupIndex >= 0 && typeof overlayGroup.group === "string") {
tab.groups[groupIndex].group = overlayGroup.group;
matchedGroupIndexes.add(groupIndex);
}
}
}
return composed;
}
function composeLocaleNav(locale, englishNav) {
const cloned = cloneEnglishLanguageNav(englishNav, locale);
if (!locale.navFile) {
return cloned;
}
const overlayPath = path.join(SOURCE_DOCS_DIR, ".i18n", locale.navFile);
if (!fs.existsSync(overlayPath)) {
return cloned;
}
return applyLocaleNavLabelOverlay(cloned, readJson(overlayPath));
}
export function composeDocsConfig() {
const sourceConfig = readJson(SOURCE_CONFIG_PATH);
const languages = sourceConfig?.navigation?.languages;
if (!Array.isArray(languages)) {
throw new Error("docs/docs.json is missing navigation.languages");
}
const englishNav = languages.find((entry) => entry?.language === "en");
const generatedLanguageSet = new Set(
GENERATED_LOCALES.filter((entry) => entry.navigation !== false).map((entry) => entry.language),
);
const withoutGenerated = languages.filter((entry) => !generatedLanguageSet.has(entry?.language));
const enIndex = withoutGenerated.findIndex((entry) => entry?.language === "en");
const generated = GENERATED_LOCALES.filter((entry) => entry.navigation !== false).map((entry) =>
composeLocaleNav(entry, englishNav),
);
if (enIndex === -1) {
withoutGenerated.push(...generated);
} else {
withoutGenerated.splice(enIndex + 1, 0, ...generated);
}
return {
...sourceConfig,
navigation: {
...sourceConfig.navigation,
languages: withoutGenerated,
},
};
}
export function reportOrphanLocaleDocs(targetDocsDir) {
let orphaned = 0;
for (const locale of GENERATED_LOCALES) {
const localeDir = path.join(targetDocsDir, locale.dir);
if (!fs.existsSync(localeDir)) {
continue;
}
for (const filePath of walkMarkdownFiles(localeDir)) {
const relativePath = path.relative(localeDir, filePath);
// Check the assembled publish tree so externally mirrored docs, such as
// ClawHub pages, count as valid English sources too.
const englishBase = path.join(targetDocsDir, relativePath);
const englishMd = englishBase.replace(/\.mdx?$/i, ".md");
const englishMdx = englishBase.replace(/\.mdx?$/i, ".mdx");
if (fs.existsSync(englishMd) || fs.existsSync(englishMdx)) {
continue;
}
orphaned += 1;
}
}
if (orphaned > 0) {
// Translation artifacts update inbound links and delete their old target
// together. Docs sync must not publish the deletion ahead of that step.
console.log(`Deferred ${orphaned} orphan localized doc(s) to translation finalization.`);
}
return orphaned;
}
function repairGeneratedLocaleDocs(targetDocsDir) {
let repaired = 0;
for (const locale of GENERATED_LOCALES) {
const localeDir = path.join(targetDocsDir, locale.dir);
for (const filePath of walkMarkdownFiles(localeDir)) {
const raw = fs.readFileSync(filePath, "utf8");
const repairedRaw = repairMintlifyAccordionIndentation(raw);
if (repairedRaw === raw) {
continue;
}
fs.writeFileSync(filePath, repairedRaw);
repaired += 1;
}
}
if (repaired > 0) {
console.log(`Repaired Mintlify accordion indentation in ${repaired} generated locale doc(s).`);
}
}
function pruneInternalDocs(targetDocsDir) {
let pruned = 0;
for (const relativeDir of INTERNAL_DOCS_DIRS) {
const dirPath = path.join(targetDocsDir, relativeDir);
if (!fs.existsSync(dirPath)) {
continue;
}
fs.rmSync(dirPath, { recursive: true, force: true });
pruned += 1;
}
if (pruned > 0) {
console.log(`Pruned ${pruned} internal-only docs director${pruned === 1 ? "y" : "ies"}.`);
}
}
function shouldExcludeClawHubDocsPath(relativePath) {
const normalized = normalizeSlashes(relativePath);
return (
normalized === "specs" || normalized.startsWith("specs/") || normalized.includes("/specs/")
);
}
function toClawHubTargetRelativePath(relativePath) {
const normalized = normalizeSlashes(relativePath);
if (normalized === "README.md") {
return "";
}
if (normalized === "clawhub.md") {
return "index.md";
}
return normalized.replace(/\/README\.md$/iu, "/index.md");
}
function toClawHubDocsRoute(relativePath) {
const targetRelativePath = toClawHubTargetRelativePath(relativePath);
if (!targetRelativePath) {
return "";
}
const normalized = targetRelativePath.replace(/\.mdx?$/iu, "");
if (normalized === "index") {
return `/${CLAWHUB_DOCS_TARGET_DIR}`;
}
if (normalized.endsWith("/index")) {
return `/${CLAWHUB_DOCS_TARGET_DIR}/${normalized.slice(0, -"/index".length)}`;
}
return `/${CLAWHUB_DOCS_TARGET_DIR}/${normalized}`;
}
function splitLinkTarget(value) {
const match = /^(\S+)(.*)$/su.exec(value);
return {
target: match?.[1] ?? value,
suffix: match?.[2] ?? "",
};
}
function splitTargetParts(value) {
const hashIndex = value.indexOf("#");
const queryIndex = value.indexOf("?");
const splitIndexes = [hashIndex, queryIndex].filter((index) => index >= 0);
const splitIndex = splitIndexes.length > 0 ? Math.min(...splitIndexes) : -1;
if (splitIndex === -1) {
return { pathPart: value, rest: "" };
}
return {
pathPart: value.slice(0, splitIndex),
rest: value.slice(splitIndex),
};
}
function rewriteClawHubMarkdownLinkTarget(rawTarget, relativeSourceDir, source) {
const { target, suffix } = splitLinkTarget(rawTarget);
if (/^(?:https?:|mailto:|tel:|data:|#)/iu.test(target) || target.startsWith("/")) {
return rawTarget;
}
const { pathPart, rest } = splitTargetParts(target);
if (!pathPart) {
return rawTarget;
}
let normalizedRelative;
if (pathPart.startsWith("docs/")) {
normalizedRelative = normalizeSlashes(pathPart.slice("docs/".length));
} else if (
pathPart.startsWith("./") ||
pathPart.startsWith("../") ||
/\.mdx?$/iu.test(pathPart)
) {
normalizedRelative = normalizeSlashes(path.normalize(path.join(relativeSourceDir, pathPart)));
} else {
return rawTarget;
}
if (normalizedRelative.startsWith("../")) {
const sourceRef = source.sha || "main";
const repoRelative = normalizeSlashes(
path.normalize(path.join("docs", relativeSourceDir, pathPart)),
).replace(/^(?:\.\.\/)+/u, "");
return `https://github.com/${source.repository}/blob/${sourceRef}/${repoRelative}${rest}${suffix}`;
}
if (!/\.mdx?$/iu.test(normalizedRelative)) {
return rawTarget;
}
const route = toClawHubDocsRoute(normalizedRelative);
return route ? `${route}${rest}${suffix}` : rawTarget;
}
function rewriteClawHubMarkdownLinks(raw, relativeSourcePath, source) {
const relativeSourceDir = normalizeSlashes(path.dirname(relativeSourcePath));
const baseDir = relativeSourceDir === "." ? "" : relativeSourceDir;
return raw.replace(/(!?\[[^\]]*\]\()([^)]+)(\))/gu, (_match, prefix, target, suffix) => {
return `${prefix}${rewriteClawHubMarkdownLinkTarget(target, baseDir, source)}${suffix}`;
});
}
/**
* Mirrors ClawHub docs into the target docs tree.
*/
export function syncClawHubDocsTree(targetDocsDir, options = {}) {
const repoPath = resolveClawHubRepoPath(options.repoPath || "", {
required: options.required !== false,
});
if (!repoPath) {
return {
repository: options.sourceRepo || DEFAULT_CLAWHUB_SOURCE_REPO,
sha: options.sourceSha || "",
path: "",
files: 0,
};
}
const sourceDocsDir = path.join(repoPath, "docs");
const targetDir = path.join(targetDocsDir, CLAWHUB_DOCS_TARGET_DIR);
const source = {
repository: options.sourceRepo || DEFAULT_CLAWHUB_SOURCE_REPO,
sha: options.sourceSha || getGitHeadSha(repoPath),
};
fs.rmSync(targetDir, { recursive: true, force: true });
ensureDir(targetDir);
let copied = 0;
for (const sourcePath of walkFiles(sourceDocsDir)) {
const relativeSourcePath = normalizeSlashes(path.relative(sourceDocsDir, sourcePath));
if (shouldExcludeClawHubDocsPath(relativeSourcePath)) {
continue;
}
const targetRelativePath = toClawHubTargetRelativePath(relativeSourcePath);
if (!targetRelativePath) {
continue;
}
const targetPath = path.join(targetDir, targetRelativePath);
ensureDir(path.dirname(targetPath));
if (/\.mdx?$/iu.test(sourcePath)) {
const raw = fs.readFileSync(sourcePath, "utf8");
fs.writeFileSync(
targetPath,
rewriteClawHubMarkdownLinks(raw, relativeSourcePath, source),
"utf8",
);
} else {
fs.copyFileSync(sourcePath, targetPath);
}
copied += 1;
}
console.log(`Synced ${copied} ClawHub doc asset(s) from ${repoPath}.`);
return {
...source,
path: repoPath,
files: copied,
};
}
function syncDocsTree(targetRoot, options = {}) {
const targetDocsDir = path.join(targetRoot, "docs");
ensureDir(targetDocsDir);
const localeFilters = GENERATED_LOCALES.flatMap((entry) => [
"--filter",
`P ${entry.dir}/`,
"--filter",
`P .i18n/${entry.tmFile}`,
"--exclude",
`${entry.dir}/`,
"--exclude",
`.i18n/${entry.tmFile}`,
]);
run("rsync", [
"-a",
"--delete",
"--filter",
"P .i18n/README.md",
"--exclude",
".i18n/README.md",
...INTERNAL_DOCS_DIRS.flatMap((dir) => ["--exclude", `${dir}/`]),
...localeFilters,
`${SOURCE_DOCS_DIR}/`,
`${targetDocsDir}/`,
]);
pruneInternalDocs(targetDocsDir);
writePublishedDocsMap(targetDocsDir);
for (const locale of GENERATED_LOCALES) {
const sourceTmPath = path.join(SOURCE_DOCS_DIR, ".i18n", locale.tmFile);
const targetTmPath = path.join(targetDocsDir, ".i18n", locale.tmFile);
if (!fs.existsSync(targetTmPath) && fs.existsSync(sourceTmPath)) {
ensureDir(path.dirname(targetTmPath));
fs.copyFileSync(sourceTmPath, targetTmPath);
}
}
const clawhubSource = syncClawHubDocsTree(targetDocsDir, {
repoPath: options.clawhubRepo,
sourceRepo: options.clawhubSourceRepo,
sourceSha: options.clawhubSourceSha,
});
reportOrphanLocaleDocs(targetDocsDir);
repairGeneratedLocaleDocs(targetDocsDir);
writeJson(path.join(targetDocsDir, "docs.json"), composeDocsConfig());
return { clawhub: clawhubSource };
}
/** Writes the public heading map into the publish tree without committing an expanded mirror. */
export function writePublishedDocsMap(targetDocsDir) {
const outputPath = path.join(targetDocsDir, "docs_map.md");
fs.writeFileSync(outputPath, renderDocsHeadingMap(SOURCE_DOCS_DIR), "utf8");
return outputPath;
}
function writeSyncMetadata(targetRoot, args, sources) {
const metadata = {
repository: args.sourceRepo || "",
sha: args.sourceSha || "",
sources: {
openclaw: {
repository: args.sourceRepo || "",
sha: args.sourceSha || "",
},
clawhub: {
repository:
sources.clawhub.repository || args.clawhubSourceRepo || DEFAULT_CLAWHUB_SOURCE_REPO,
sha: sources.clawhub.sha || args.clawhubSourceSha || "",
},
},
syncedAt: new Date().toISOString(),
};
writeJson(path.join(targetRoot, ".openclaw-sync", "source.json"), metadata);
}
function syncSupportFiles(targetRoot) {
for (const entry of SYNC_SUPPORT_FILES) {
const targetPath = path.join(targetRoot, entry.target);
ensureDir(path.dirname(targetPath));
fs.copyFileSync(entry.source, targetPath);
}
}
function main() {
const args = parseArgs(process.argv.slice(2));
const targetRoot = path.resolve(args.target);
if (!fs.existsSync(targetRoot)) {
throw new Error(`target does not exist: ${targetRoot}`);
}
const clawhubRepo = resolveClawHubRepoPath(args.clawhubRepo);
const sources = syncDocsTree(targetRoot, {
clawhubRepo,
clawhubSourceRepo: args.clawhubSourceRepo,
clawhubSourceSha: args.clawhubSourceSha,
});
syncSupportFiles(targetRoot);
writeSyncMetadata(targetRoot, args, sources);
}
function isCliEntry() {
const cliArg = process.argv[1];
return cliArg ? import.meta.url === pathToFileURL(cliArg).href : false;
}
if (isCliEntry()) {
main();
}