Files
docs/apps/bot-docs/scripts/convert-docs.ts
T
zeekay ada2f3d2ab rebrand: complete white-label — all packages @hanzo/docs-* (one brand)
Finish the fumadocs→Hanzo Docs rebrand: rename the remaining fuma-named
workspace packages and every reference so the framework is uniformly
@hanzo/docs-*. One brand, no Fuma in package names or product surface.

Package renames (name field + all refs: deps, imports, CSS @import,
configs, .changeset, next/vite build config):
  fumadocs-core        → @hanzo/docs-core
  fumadocs-mdx         → @hanzo/docs-mdx
  fumadocs-ui          → @hanzo/docs-ui          (packages/radix-ui)
  fumadocs-openapi     → @hanzo/docs-openapi
  fumadocs-preview     → @hanzo/docs-preview
  @fumadocs/base-ui    → @hanzo/docs-base-ui
  @fumadocs/basehub    → @hanzo/docs-basehub
  @fumadocs/language   → @hanzo/docs-language
  @fumadocs/local-md   → @hanzo/docs-local-md
  @fumadocs/sanity     → @hanzo/docs-sanity
  @fumadocs/shadcn     → @hanzo/docs-shadcn
  @fumadocs/vite       → @hanzo/docs-vite
  @fumari/stf          → @hanzo/docs-stf
  create-fumadocs-app  → @hanzo/create-docs
  create-fumadocs-versions → @hanzo/docs-create-versions

Stale @fumadocs/{cli,story,tailwind,mdx-remote,...} references collapsed to
their existing @hanzo/docs-* canonical packages (no dup created).

De-branded product strings: Fumadocs/Fuma prose → Hanzo Docs/Hanzo; site
branding, CLI output, doc copy, AI-assistant name, metadata. URLs
fumadocs.dev → docs.hanzo.ai, repo fuma-nama/fumadocs → hanzoai/docs.
Filesystem renames: packages/python/fumapy → hanzodocs_py, preview config,
what-is-fumadocs.mdx → what-is-hanzo-docs.mdx, api/fumadocs-ui route.

KEPT (real external npm deps / attribution — NOT renamed):
  fuma-cli, fuma-content, @fumari/json-schema-ts (published upstream pkgs),
  typesense-/trieve-fumadocs-adapter (3rd-party search adapters),
  LICENSE "Copyright (c) 2023 Fuma" (OSS attribution, all 7 LICENSE files
  untouched), CHANGELOG.md history (accurate release records, left as-is).

Build: node 24, pnpm install clean; all 4 target apps next build GREEN
(spec, gui-docs, zen-docs, base-docs) + full package closure (18/18 turbo).
Gate: zero @hanzo-package fumadocs-/@fumadocs/ references remain.
2026-06-24 23:21:42 -07:00

333 lines
9.2 KiB
TypeScript

/**
* Convert OpenClaw/Bot docs from Mintlify .md → Hanzo Docs .mdx
*
* - Copies all .md files preserving directory structure
* - Converts frontmatter (summary → description, removes read_when)
* - Rebrands OpenClaw → Hanzo Bot
* - Creates meta.json for each directory
* - Renames index.md → index.mdx
*/
import { readdir, readFile, writeFile, mkdir, stat } from 'fs/promises';
import { join, relative, basename, dirname, extname } from 'path';
import { existsSync } from 'fs';
const SRC = '/Users/z/work/hanzo/bot/docs';
const DEST = '/Users/z/work/hanzo/docs/apps/bot-docs/content/docs';
// Directories to include (top-level categories)
const CATEGORIES = [
'install', 'start', 'concepts', 'cli', 'channels', 'providers',
'tools', 'gateway', 'automation', 'platforms', 'nodes', 'web',
'plugins', 'help', 'reference',
];
// Files to skip
const SKIP_FILES = new Set([
'_config.yml', 'docs.json', 'CNAME', '.gitignore',
'Gemfile', 'Gemfile.lock', '_layouts', '_includes',
]);
// Rebrand map
const REBRAND: [RegExp, string][] = [
[/\bOpenClaw\b/g, 'Hanzo Bot'],
[/\bopenclaw\b/g, 'hanzo-bot'],
[/\bOPENCLAW\b/g, 'HANZO_BOT'],
[/\bopenclaw\.ai\b/g, 'hanzo.bot'],
[/\bClawdBot\b/g, 'Hanzo Bot'],
[/\bclawdbot\b/g, 'hanzo-bot'],
[/\bCLAWDBOT\b/g, 'BOT'],
[/openclaw\.ai/g, 'hanzo.bot'],
[/docs\.openclaw\.ai/g, 'hanzo.bot/docs'],
// GitHub refs
[/github\.com\/openclaw\/openclaw/g, 'github.com/hanzoai/bot'],
[/github\.com\/bot\/bot/g, 'github.com/hanzoai/bot'],
[/github\.com\/bot\/nix-bot/g, 'github.com/hanzoai/nix-bot'],
[/github\.com\/bot\/bot-ansible/g, 'github.com/hanzoai/bot-ansible'],
// Discord
[/channels\.discord\.gg\/bot/g, 'discord.gg/XthHQQj'],
[/discord\.gg\/hanzo\b/g, 'discord.gg/XthHQQj'],
[/discord\.gg\/clawd\b/g, 'discord.gg/XthHQQj'],
// Domain
[/hanzo\.bot\/install/g, 'hanzo.bot/install'],
// Hanzo Cloud messaging
];
// Category display names and ordering
const CATEGORY_META: Record<string, { title: string; pages?: string[] }> = {
install: {
title: 'Installation',
pages: ['index', 'installer', 'node', 'docker', 'nix', 'ansible', 'bun', 'updating', 'development-channels', 'uninstall'],
},
start: {
title: 'Getting Started',
pages: ['getting-started', 'wizard', 'setup', 'bot', 'pairing', 'showcase', 'onboarding', 'hubs', 'lore'],
},
concepts: {
title: 'Core Concepts',
},
cli: {
title: 'CLI Reference',
},
channels: {
title: 'Channels',
pages: ['index', 'whatsapp', 'telegram', 'grammy', 'discord', 'slack', 'signal', 'bluebubbles', 'imessage', 'matrix', 'mattermost', 'googlechat', 'msteams', 'nostr', 'tlon', 'nextcloud-talk', 'zalo', 'zalouser', 'location', 'troubleshooting'],
},
providers: {
title: 'LLM Providers',
pages: ['index', 'openai', 'anthropic', 'ollama', 'qwen', 'openrouter', 'moonshot', 'minimax', 'venice', 'synthetic', 'vercel-ai-gateway', 'opencode', 'zai', 'glm', 'github-copilot', 'deepgram'],
},
tools: {
title: 'Skills & Tools',
},
gateway: {
title: 'Gateway',
},
automation: {
title: 'Automation',
},
platforms: {
title: 'Platforms',
},
nodes: {
title: 'Nodes & Media',
},
web: {
title: 'Web Interfaces',
},
plugins: {
title: 'Plugins',
},
help: {
title: 'Help',
},
reference: {
title: 'Reference',
},
};
function convertFrontmatter(content: string): string {
// Extract existing frontmatter
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!fmMatch) {
return content;
}
const fm = fmMatch[1];
let body = fmMatch[2];
// Parse frontmatter fields
const lines = fm.split('\n');
let title = '';
let description = '';
let isIndex = false;
for (const line of lines) {
const summaryMatch = line.match(/^summary:\s*"(.+)"$/);
if (summaryMatch) {
description = summaryMatch[1];
}
const titleMatch = line.match(/^title:\s*"?(.+?)"?\s*$/);
if (titleMatch) {
title = titleMatch[1];
}
}
// Extract title from first heading if not in frontmatter
if (!title) {
const headingMatch = body.match(/^#\s+(.+)$/m);
if (headingMatch) {
title = headingMatch[1];
// Remove the heading from body since Hanzo Docs uses frontmatter title
body = body.replace(/^#\s+.+\n+/, '');
}
}
if (!title) {
title = 'Untitled';
}
if (!description) {
description = title;
}
// Build new frontmatter
const newFm = [
'---',
`title: "${title.replace(/"/g, '\\"')}"`,
`description: "${description.replace(/"/g, '\\"')}"`,
];
if (isIndex) {
newFm.push('index: true');
}
newFm.push('---');
return newFm.join('\n') + '\n\n' + body.trim() + '\n';
}
function applyRebrand(content: string): string {
let result = content;
for (const [pattern, replacement] of REBRAND) {
result = result.replace(pattern, replacement);
}
// Add Hanzo Cloud callout to install/getting-started pages
return result;
}
async function* walkDir(dir: string): AsyncGenerator<string> {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
if (!SKIP_FILES.has(entry.name) && !entry.name.startsWith('_') && !entry.name.startsWith('.')) {
yield* walkDir(fullPath);
}
} else if (entry.name.endsWith('.md') && !SKIP_FILES.has(entry.name)) {
yield fullPath;
}
}
}
async function convertFile(srcPath: string) {
const relPath = relative(SRC, srcPath);
const parts = relPath.split('/');
// Only process files in our target categories or root-level
const category = parts[0];
if (parts.length > 1 && !CATEGORIES.includes(category)) {
return;
}
// Skip root-level files that aren't in categories
if (parts.length === 1 && parts[0] !== 'index.md') {
// Root files that should go into specific categories
const rootFileMap: Record<string, string> = {
'tui.md': 'web/tui.mdx',
'tts.md': 'nodes/tts.mdx',
'environment.md': 'reference/environment.mdx',
'scripts.md': 'reference/scripts.mdx',
'testing.md': 'reference/testing.mdx',
'debug.md': 'help/debug.mdx',
};
const mapped = rootFileMap[parts[0]];
if (!mapped) return;
const destPath = join(DEST, mapped);
await mkdir(dirname(destPath), { recursive: true });
let content = await readFile(srcPath, 'utf-8');
content = convertFrontmatter(content);
content = applyRebrand(content);
await writeFile(destPath, content, 'utf-8');
console.log(` ${relPath} → ${mapped}`);
return;
}
// Build dest path (.md → .mdx)
let destRel = relPath.replace(/\.md$/, '.mdx');
const destPath = join(DEST, destRel);
await mkdir(dirname(destPath), { recursive: true });
let content = await readFile(srcPath, 'utf-8');
content = convertFrontmatter(content);
content = applyRebrand(content);
await writeFile(destPath, content, 'utf-8');
console.log(` ${relPath} → ${destRel}`);
}
async function createMetaFiles() {
// Create meta.json for each category directory
for (const [category, meta] of Object.entries(CATEGORY_META)) {
const catDir = join(DEST, category);
if (!existsSync(catDir)) continue;
const metaContent: any = {};
if (meta.title) {
metaContent.title = meta.title;
}
if (meta.pages) {
metaContent.pages = meta.pages;
} else {
// Auto-discover pages from directory
try {
const entries = await readdir(catDir);
const pages = entries
.filter(f => f.endsWith('.mdx') && f !== 'index.mdx')
.map(f => f.replace('.mdx', ''))
.sort();
if (pages.length > 0) {
// Put index first if it exists
const hasIndex = entries.includes('index.mdx');
if (hasIndex) {
metaContent.pages = ['index', ...pages];
} else {
metaContent.pages = pages;
}
}
} catch {
// Directory might not exist yet
}
}
await writeFile(
join(catDir, 'meta.json'),
JSON.stringify(metaContent, null, 2) + '\n',
'utf-8'
);
console.log(` Created ${category}/meta.json`);
}
// Handle subdirectories (platforms/mac, reference/templates)
const subdirs = ['platforms/mac', 'reference/templates'];
for (const subdir of subdirs) {
const subdirPath = join(DEST, subdir);
if (!existsSync(subdirPath)) continue;
const entries = await readdir(subdirPath);
const pages = entries
.filter(f => f.endsWith('.mdx'))
.map(f => f.replace('.mdx', ''))
.sort();
if (pages.length > 0) {
await writeFile(
join(subdirPath, 'meta.json'),
JSON.stringify({ pages }, null, 2) + '\n',
'utf-8'
);
console.log(` Created ${subdir}/meta.json`);
}
}
}
async function main() {
console.log('Converting OpenClaw docs → Hanzo Bot Hanzo Docs MDX\n');
console.log(`Source: ${SRC}`);
console.log(`Dest: ${DEST}\n`);
let count = 0;
// Convert all markdown files
for await (const srcPath of walkDir(SRC)) {
await convertFile(srcPath);
count++;
}
console.log(`\nConverted ${count} files`);
// Create meta.json files
console.log('\nCreating meta.json files...');
await createMetaFiles();
console.log('\nDone!');
}
main().catch(console.error);