chore: import upstream snapshot with attribution
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:03:19 +08:00
commit 91e75e620b
3227 changed files with 1307078 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
# Documentation Generators
This directory contains auto-documentation generators for the Cua libraries. These generators ensure that documentation stays synchronized with source code.
## Architecture
```
scripts/docs-generators/
├── config.json # Central configuration for all generators
├── runner.ts # Main orchestrator that runs generators
├── cua-driver.ts # cua-driver (Rust) generator
├── lume.ts # Lume (Swift) generator
├── cua-cli.ts # Cua CLI (TypeScript) generator (planned)
├── mcp-server.ts # MCP Server (Python) generator (planned)
├── python-sdk.ts # Python SDK generator (planned)
├── typescript-sdk.ts # TypeScript SDK generator (planned)
└── README.md # This file
```
## Quick Start
```bash
# Generate all documentation
npm run docs:generate
# Check for drift (CI mode)
npm run docs:check
# List all configured generators
npm run docs:list
# Generate specific library docs
npx tsx scripts/docs-generators/runner.ts --library lume
```
## How It Works
1. **config.json** defines all libraries that need documentation generation:
- Source paths to watch
- Output paths for generated docs
- Generator scripts to run
- Build commands (if needed)
2. **runner.ts** orchestrates generation:
- Reads config.json
- Detects changed files (in CI)
- Runs appropriate generators
- Reports success/failure
3. **Library-specific generators** (e.g., `lume.ts`):
- Extract metadata from source code
- Generate MDX documentation
- Handle language-specific concerns
## Adding a New Library
1. **Add configuration** to `config.json`:
```json
{
"generators": {
"my-library": {
"name": "My Library",
"language": "python",
"sourcePath": "libs/my-library/src",
"docsOutputPath": "docs/content/docs/cua/reference/my-library",
"generatorScript": "scripts/docs-generators/my-library.ts",
"watchPaths": ["libs/my-library/src/**/*.py"],
"enabled": true
}
}
}
```
2. **Create generator script** (e.g., `my-library.ts`):
```typescript
#!/usr/bin/env npx tsx
// Follow the pattern in lume.ts
```
3. **Update CI workflow** if needed (paths are usually auto-detected from config)
## Extraction Methods
Different libraries use different extraction methods:
| Language | Method | Description |
| ---------- | ------------------------ | ---------------------------------- |
| Rust | `dump-docs` | Built-in command that outputs JSON |
| Swift | `dump-docs` | Built-in command that outputs JSON |
| TypeScript | `yargs-parse` | Parse yargs CLI definitions |
| Python | `argparse-introspection` | Introspect argparse commands |
| Python | `sphinx-autodoc` | Generate from docstrings |
| TypeScript | `typedoc` | Generate from TSDoc comments |
## CI Integration
The `.github/workflows/docs-sync-check.yml` workflow:
1. Detects which libraries have changes
2. Only runs generators for changed libraries
3. Fails if documentation is out of sync
4. Provides helpful fix instructions
## Generator Status
| Generator | Status | Notes |
| ----------------------- | -------------- | ------------------------- |
| cua-driver | ✅ Implemented | CLI + MCP tools |
| lume | ✅ Implemented | CLI + HTTP API |
| cua-cli | ⏸️ Planned | Needs yargs introspection |
| mcp-server | ⏸️ Planned | Needs MCP tool extraction |
| computer-sdk-python | ⏸️ Planned | Needs Sphinx/pydoc |
| computer-sdk-typescript | ⏸️ Planned | Needs TypeDoc |
| agent-sdk-python | ⏸️ Planned | Needs Sphinx/pydoc |
| agent-sdk-typescript | ⏸️ Planned | Needs TypeDoc |
## Development
To test generators locally:
```bash
# Run with verbose output
DEBUG=1 npx tsx scripts/docs-generators/runner.ts
# Generate only one library
npx tsx scripts/docs-generators/runner.ts --library lume
# Check without modifying files
npx tsx scripts/docs-generators/runner.ts --check
```
+186
View File
@@ -0,0 +1,186 @@
{
"$schema": "./config.schema.json",
"description": "Configuration for auto-generating documentation from source code",
"generators": {
"cua-driver": {
"name": "Cua Driver",
"language": "rust",
"sourcePath": "libs/cua-driver/rust/crates",
"docsOutputPath": "docs/content/docs/reference/cua-driver",
"generatorScript": "scripts/docs-generators/cua-driver.ts",
"watchPaths": [
"libs/cua-driver/rust/crates/**/*.rs",
"libs/cua-driver/rust/Cargo.toml",
"libs/cua-driver/rust/Cargo.lock"
],
"buildCommand": "cargo build -p cua-driver --release",
"buildDirectory": "libs/cua-driver/rust",
"extractionMethod": "dump-docs",
"outputs": [
{
"type": "mcp",
"outputFile": "mcp-tools.mdx",
"extractCommand": "target/release/cua-driver dump-docs --type mcp"
},
{
"type": "cli",
"outputFile": "cli-reference.mdx",
"extractCommand": "target/release/cua-driver dump-docs --type cli"
}
],
"enabled": true,
"notes": "Generates cli-reference.mdx + mcp-tools.mdx from the Rust binary's `dump-docs`. MCP tool docs come straight from the live tool registry; CLI docs come from a hand-maintained `cli_docs_json()` literal in crates/cua-driver/src/cli.rs (the CLI is hand-parsed, not clap, so there is nothing to introspect yet — introspecting it is a tracked follow-up). Curated cross-cutting content lives in the sibling non-generated files mcp-tool-notes.mdx and macos-permissions.mdx."
},
"lume": {
"name": "Lume VM Manager",
"language": "swift",
"sourcePath": "libs/lume/src",
"docsOutputPath": "docs/content/docs/reference/lume",
"generatorScript": "scripts/docs-generators/lume.ts",
"watchPaths": [
"libs/lume/src/Commands/**/*.swift",
"libs/lume/src/Server/**/*.swift",
"libs/lume/src/Utils/CommandDocExtractor.swift",
"libs/lume/src/Server/APIDocExtractor.swift"
],
"buildCommand": "swift build -c release",
"buildDirectory": "libs/lume",
"extractionMethod": "dump-docs",
"outputs": [
{
"type": "cli",
"outputFile": "cli-reference.mdx",
"extractCommand": ".build/release/lume dump-docs --type cli"
},
{
"type": "api",
"outputFile": "http-api.mdx",
"extractCommand": ".build/release/lume dump-docs --type api"
}
],
"enabled": true,
"notes": "Generates cli-reference.mdx + http-api.mdx from the Swift binary's `dump-docs` output. These files live under the Diátaxis Reference tree; task-oriented Lume docs belong under how-to-guides/lume."
},
"cua-cli": {
"name": "Cua CLI",
"language": "typescript",
"sourcePath": "libs/typescript/cua-cli/src",
"docsOutputPath": "docs/content/docs/cua/reference/cli-playbook",
"generatorScript": "scripts/docs-generators/cua-cli.ts",
"watchPaths": ["libs/typescript/cua-cli/src/**/*.ts"],
"buildCommand": null,
"buildDirectory": "libs/typescript/cua-cli",
"extractionMethod": "yargs-parse",
"outputs": [
{
"type": "cli",
"outputFile": "commands.mdx",
"extractCommand": null
}
],
"enabled": false,
"notes": "Requires yargs introspection or AST parsing - to be implemented"
},
"mcp-server": {
"name": "MCP Server",
"language": "python",
"sourcePath": "libs/python/mcp-server/src",
"docsOutputPath": "docs/content/docs/cua/reference/mcp-server",
"generatorScript": "scripts/docs-generators/mcp-server.ts",
"watchPaths": ["libs/python/mcp-server/src/**/*.py"],
"buildCommand": null,
"buildDirectory": "libs/python/mcp-server",
"extractionMethod": "mcp-introspection",
"outputs": [
{
"type": "tools",
"outputFile": "tools.mdx",
"extractCommand": null
}
],
"enabled": false,
"notes": "Requires MCP tool introspection - to be implemented"
},
"python-sdk": {
"name": "Python SDKs (Computer + Agent)",
"language": "python",
"sourcePath": "libs/python",
"docsOutputPath": "docs/content/docs/cua/reference",
"generatorScript": "scripts/docs-generators/python-sdk.ts",
"watchPaths": ["libs/python/computer/computer/**/*.py", "libs/python/agent/agent/**/*.py"],
"buildCommand": null,
"buildDirectory": "libs/python",
"extractionMethod": "griffe",
"outputs": [
{
"type": "api",
"outputFile": "computer-sdk/api.mdx",
"extractCommand": "python3 scripts/docs-generators/extract_python_docs.py"
},
{
"type": "api",
"outputFile": "agent-sdk/api.mdx",
"extractCommand": "python3 scripts/docs-generators/extract_python_docs.py"
}
],
"enabled": false,
"notes": "Disabled while these SDK docs are not part of the public docs nav; prevents docs:generate from resurrecting the old docs/content/docs/cua/reference tree."
},
"computer-sdk-typescript": {
"name": "Computer SDK (TypeScript)",
"language": "typescript",
"sourcePath": "libs/typescript/computer/src",
"docsOutputPath": "docs/content/docs/cua/reference/computer-sdk",
"generatorScript": "scripts/docs-generators/typescript-sdk.ts",
"watchPaths": ["libs/typescript/computer/src/**/*.ts"],
"extractionMethod": "typedoc",
"outputs": [
{
"type": "api",
"outputFile": "typescript-api.mdx",
"extractCommand": null
}
],
"enabled": false,
"notes": "Requires TypeDoc or ts-morph introspection - to be implemented"
},
"agent-sdk-typescript": {
"name": "Agent SDK (TypeScript)",
"language": "typescript",
"sourcePath": "libs/typescript/agent/src",
"docsOutputPath": "docs/content/docs/cua/reference/agent-sdk",
"generatorScript": "scripts/docs-generators/typescript-sdk.ts",
"watchPaths": ["libs/typescript/agent/src/**/*.ts"],
"extractionMethod": "typedoc",
"outputs": [
{
"type": "api",
"outputFile": "typescript-api.mdx",
"extractCommand": null
}
],
"enabled": false,
"notes": "Requires TypeDoc or ts-morph introspection - to be implemented"
},
"cuabot": {
"name": "Cua-Bot",
"language": "typescript",
"sourcePath": "libs/cuabot/src",
"docsOutputPath": "docs/content/docs/cuabot/reference",
"generatorScript": "scripts/docs-generators/typescript-sdk.ts",
"watchPaths": ["libs/cuabot/src/**/*.ts"],
"buildCommand": null,
"buildDirectory": "libs/cuabot",
"extractionMethod": "regex-parse",
"outputs": [
{
"type": "api",
"outputFile": "index.mdx",
"extractCommand": null
}
],
"enabled": false,
"notes": "Disabled while cuabot docs are not part of the public docs nav; prevents docs:generate from resurrecting docs/content/docs/cuabot/reference."
}
}
}
+820
View File
@@ -0,0 +1,820 @@
#!/usr/bin/env npx tsx
/**
* Cua Driver Documentation Generator
*
* Generates MDX documentation files from the cua-driver `dump-docs` command output.
* This ensures documentation stays synchronized with the source code.
*
* Usage:
* npx tsx scripts/docs-generators/cua-driver.ts # Generate docs
* npx tsx scripts/docs-generators/cua-driver.ts --check # Check for drift (CI mode)
*/
import { execFileSync, execSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
// ============================================================================
// Types
// ============================================================================
export interface CLIDocumentation {
name: string;
version: string;
abstract: string;
commands: CommandDoc[];
}
export interface CommandDoc {
name: string;
abstract: string;
discussion?: string;
arguments: ArgumentDoc[];
options: OptionDoc[];
flags: FlagDoc[];
subcommands: CommandDoc[];
}
export interface ArgumentDoc {
name: string;
help: string;
type: string;
is_optional: boolean;
}
export interface OptionDoc {
name: string;
short_name?: string | null;
help: string;
type: string;
default_value?: string | null;
is_optional: boolean;
}
export interface FlagDoc {
name: string;
short_name?: string | null;
help: string;
default_value: boolean;
}
export interface MCPToolDoc {
name: string;
description: string;
input_schema: MCPInputSchema;
}
export interface MCPInputSchema {
type: string;
required?: string[];
properties?: Record<string, MCPPropertyDoc>;
}
export interface MCPPropertyDoc {
type: string;
description: string;
items?: { type: string };
}
export interface MCPDocumentation {
version: string;
tools: MCPToolDoc[];
}
export interface DumpDocsOutput {
cli: CLIDocumentation;
mcp: MCPDocumentation;
}
// ============================================================================
// Configuration
// ============================================================================
const ROOT_DIR = path.resolve(__dirname, '../..');
const CUA_DRIVER_DIR = path.join(ROOT_DIR, 'libs', 'cua-driver', 'rust');
const CUA_DRIVER_BIN = path.join(
CUA_DRIVER_DIR,
'target',
'release',
process.platform === 'win32' ? 'cua-driver.exe' : 'cua-driver'
);
const DOCS_OUTPUT_DIR = path.join(ROOT_DIR, 'docs', 'content', 'docs', 'reference', 'cua-driver');
const TAG_PREFIX = 'cua-driver-rs-v';
// ============================================================================
// Version Discovery
// ============================================================================
function resolveCargoCommand(): string {
if (process.env.CARGO) {
return process.env.CARGO;
}
const candidates = [
'cargo',
path.join(os.homedir() || '', '.cargo', 'bin', 'cargo'),
'/opt/homebrew/bin/cargo',
'/usr/local/bin/cargo',
];
for (const candidate of candidates) {
try {
execFileSync(candidate, ['--version'], { stdio: 'ignore' });
return candidate;
} catch {
// Try the next candidate.
}
}
throw new Error('cargo not found on PATH; install Rust or set CARGO=/path/to/cargo');
}
/**
* Get the latest released version from git tags.
*/
export function getLatestReleasedVersion(): string {
try {
const output = execSync(`git tag | grep "^${TAG_PREFIX}" | sort -V | tail -1`, {
encoding: 'utf-8',
cwd: ROOT_DIR,
}).trim();
if (output) {
return output.replace(TAG_PREFIX, '');
}
} catch {
// Fall through
}
return '0.0.0';
}
// ============================================================================
// Main
// ============================================================================
async function main() {
const args = process.argv.slice(2);
const checkOnly = args.includes('--check') || args.includes('--check-only');
console.log('Cua Driver Documentation Generator');
console.log('===================================\n');
// Step 1: Build cua-driver
console.log('Building cua-driver...');
try {
const cargo = resolveCargoCommand();
execFileSync(cargo, ['build', '-p', 'cua-driver', '--release'], {
cwd: CUA_DRIVER_DIR,
stdio: 'inherit',
});
} catch (error) {
console.error('Failed to build cua-driver');
process.exit(1);
}
// Step 2: Extract all docs in a single invocation
console.log('\nExtracting documentation...');
const binary = process.env.CUA_DRIVER_BINARY || CUA_DRIVER_BIN;
const dumpDocsJson = execFileSync(binary, ['dump-docs', '--type', 'all', '--pretty'], {
cwd: CUA_DRIVER_DIR,
encoding: 'utf-8',
});
const dumpDocs: DumpDocsOutput = JSON.parse(dumpDocsJson);
console.log(` Found ${dumpDocs.cli.commands.length} CLI commands`);
console.log(` Found ${dumpDocs.mcp.tools.length} MCP tools`);
// Step 3: Generate MDX files
console.log('\nGenerating documentation files...');
const releasedVersion = getLatestReleasedVersion();
// Use the version embedded in the binary itself as the canonical current
// version — it's always correct even on unreleased branches where no git
// tag exists yet. Fall back to the latest released tag only when the
// binary reports an empty/missing version.
const currentVersion = dumpDocs.cli.version || dumpDocs.mcp.version || releasedVersion;
const cliMdx = generateCLIReferenceMDX(dumpDocs.cli, currentVersion);
const mcpMdx = generateMCPToolsMDX(dumpDocs.mcp, currentVersion);
const cliPath = path.join(DOCS_OUTPUT_DIR, 'cli-reference.mdx');
const mcpPath = path.join(DOCS_OUTPUT_DIR, 'mcp-tools.mdx');
if (checkOnly) {
// Check mode: compare with existing files
console.log('\nChecking for documentation drift...');
let hasDrift = false;
if (fs.existsSync(cliPath)) {
const existingCli = fs.readFileSync(cliPath, 'utf-8');
if (existingCli !== cliMdx) {
console.error('cli-reference.mdx is out of sync with source code');
hasDrift = true;
} else {
console.log('cli-reference.mdx is up to date');
}
} else {
console.error('cli-reference.mdx does not exist');
hasDrift = true;
}
if (fs.existsSync(mcpPath)) {
const existingMcp = fs.readFileSync(mcpPath, 'utf-8');
if (existingMcp !== mcpMdx) {
console.error('mcp-tools.mdx is out of sync with source code');
hasDrift = true;
} else {
console.log('mcp-tools.mdx is up to date');
}
} else {
console.error('mcp-tools.mdx does not exist');
hasDrift = true;
}
if (hasDrift) {
console.error(
"\nRun 'npx tsx scripts/docs-generators/cua-driver.ts' to update documentation"
);
process.exit(1);
}
console.log('\nAll cua-driver documentation is up to date!');
} else {
// Generate mode: write files
fs.mkdirSync(DOCS_OUTPUT_DIR, { recursive: true });
fs.writeFileSync(cliPath, cliMdx);
console.log(` Generated ${path.relative(ROOT_DIR, cliPath)}`);
fs.writeFileSync(mcpPath, mcpMdx);
console.log(` Generated ${path.relative(ROOT_DIR, mcpPath)}`);
console.log('\ncua-driver documentation generated successfully!');
}
}
// ============================================================================
// CLI Reference Generator
// ============================================================================
export function generateCLIReferenceMDX(docs: CLIDocumentation, releasedVersion: string): string {
const lines: string[] = [];
// Frontmatter — must be at the very beginning of the file
lines.push('---');
lines.push('title: CLI Reference');
lines.push('description: Command-line interface specification for Cua Driver');
lines.push('---');
lines.push('');
lines.push(`{/*
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/cua-driver.ts
Source: cua-driver dump-docs
Version: ${releasedVersion}
*/}`);
lines.push('');
// Introduction
lines.push(escapeMdxText(docs.abstract) + ' Install via the official script:');
lines.push('');
lines.push('```sh');
lines.push(
'curl -fsSL https://cua.ai/driver/install.sh | bash'
);
lines.push('```');
lines.push('');
lines.push(
`Documented against Cua Driver **${releasedVersion}**. Run \`cua-driver --version\` for your installed version.`
);
lines.push('');
lines.push(
'The macOS-only `cua-driver permissions` command is documented separately in [macOS permissions](/reference/cua-driver/macos-permissions).'
);
lines.push('');
lines.push('---');
lines.push('');
// Group commands by category
const toolDispatch = ['call', 'list-tools', 'describe'];
const daemonManagement = ['serve', 'stop', 'status', 'mcp', 'mcp-config'];
const trajectoryRecording = ['recording'];
const configuration = ['config'];
const diagnostics = ['diagnose', 'update', 'check-update', 'doctor'];
lines.push('## Tool dispatch');
lines.push('');
for (const cmd of docs.commands.filter((c) => toolDispatch.includes(c.name))) {
lines.push(...generateCommandDoc(cmd));
}
lines.push('## Daemon management');
lines.push('');
for (const cmd of docs.commands.filter((c) => daemonManagement.includes(c.name))) {
lines.push(...generateCommandDoc(cmd));
}
lines.push('## Trajectory recording');
lines.push('');
for (const cmd of docs.commands.filter((c) => trajectoryRecording.includes(c.name))) {
lines.push(...generateCommandDoc(cmd));
}
lines.push('## Configuration');
lines.push('');
for (const cmd of docs.commands.filter((c) => configuration.includes(c.name))) {
lines.push(...generateCommandDoc(cmd));
}
lines.push('## Diagnostics');
lines.push('');
for (const cmd of docs.commands.filter((c) => diagnostics.includes(c.name))) {
lines.push(...generateCommandDoc(cmd));
}
// Any commands not yet categorised above
const allCategorised = [
...toolDispatch,
...daemonManagement,
...trajectoryRecording,
...configuration,
...diagnostics,
];
// Documented on their own pages, so keep them out of this reference.
const excludedFromReference = ['permissions']; // -> reference/cua-driver/macos-permissions
const uncategorised = docs.commands.filter(
(c) => !allCategorised.includes(c.name) && !excludedFromReference.includes(c.name)
);
if (uncategorised.length > 0) {
lines.push('## Other commands');
lines.push('');
for (const cmd of uncategorised) {
lines.push(...generateCommandDoc(cmd));
}
}
// Global options footer
lines.push('## Global options');
lines.push('');
lines.push('Available on all commands:');
lines.push('');
lines.push('- `--help` — Show help information.');
lines.push('- `--version` — Show version number.');
lines.push('');
return lines.join('\n');
}
export function generateCommandDoc(cmd: CommandDoc): string[] {
const lines: string[] = [];
lines.push(`### \`cua-driver ${cmd.name}\``);
lines.push('');
lines.push(escapeMdxText(cmd.abstract));
lines.push('');
if (cmd.discussion) {
lines.push(escapeMdxText(cmd.discussion));
lines.push('');
}
// Arguments table
if (cmd.arguments.length > 0) {
lines.push('**Arguments:**');
lines.push('');
lines.push('| Name | Type | Required | Description |');
lines.push('| ---- | ---- | -------- | ----------- |');
for (const arg of cmd.arguments) {
const required = arg.is_optional ? 'No' : 'Yes';
lines.push(
`| \`<${arg.name}>\` | ${escapeTableCell(arg.type)} | ${required} | ${escapeTableCell(arg.help)} |`
);
}
lines.push('');
}
// Options table
if (cmd.options.length > 0) {
lines.push('**Options:**');
lines.push('');
lines.push('| Name | Type | Default | Description |');
lines.push('| ---- | ---- | ------- | ----------- |');
for (const opt of cmd.options) {
const nameCell = opt.short_name
? `\`-${opt.short_name}\`, \`--${opt.name}\``
: `\`--${opt.name}\``;
const defaultVal = opt.default_value != null ? opt.default_value : '—';
lines.push(
`| ${nameCell} | ${escapeTableCell(opt.type)} | ${escapeTableCell(defaultVal)} | ${escapeTableCell(opt.help)} |`
);
}
lines.push('');
}
// Flags table
if (cmd.flags.length > 0) {
lines.push('**Flags:**');
lines.push('');
lines.push('| Name | Description |');
lines.push('| ---- | ----------- |');
for (const flag of cmd.flags) {
const nameCell = flag.short_name
? `\`-${flag.short_name}\`, \`--${flag.name}\``
: `\`--${flag.name}\``;
lines.push(`| ${nameCell} | ${escapeTableCell(flag.help)} |`);
}
lines.push('');
}
// Subcommands
if (cmd.subcommands.length > 0) {
for (const sub of cmd.subcommands) {
lines.push(`#### \`cua-driver ${cmd.name} ${sub.name}\``);
lines.push('');
lines.push(escapeMdxText(sub.abstract));
lines.push('');
if (sub.discussion) {
lines.push(escapeMdxText(sub.discussion));
lines.push('');
}
if (sub.arguments.length > 0) {
lines.push('**Arguments:**');
lines.push('');
lines.push('| Name | Type | Required | Description |');
lines.push('| ---- | ---- | -------- | ----------- |');
for (const arg of sub.arguments) {
const required = arg.is_optional ? 'No' : 'Yes';
lines.push(
`| \`<${arg.name}>\` | ${escapeTableCell(arg.type)} | ${required} | ${escapeTableCell(arg.help)} |`
);
}
lines.push('');
}
if (sub.options.length > 0) {
lines.push('**Options:**');
lines.push('');
lines.push('| Name | Type | Default | Description |');
lines.push('| ---- | ---- | ------- | ----------- |');
for (const opt of sub.options) {
const nameCell = opt.short_name
? `\`-${opt.short_name}\`, \`--${opt.name}\``
: `\`--${opt.name}\``;
const defaultVal = opt.default_value != null ? opt.default_value : '—';
lines.push(
`| ${nameCell} | ${escapeTableCell(opt.type)} | ${escapeTableCell(defaultVal)} | ${escapeTableCell(opt.help)} |`
);
}
lines.push('');
}
if (sub.flags.length > 0) {
lines.push('**Flags:**');
lines.push('');
lines.push('| Name | Description |');
lines.push('| ---- | ----------- |');
for (const flag of sub.flags) {
const nameCell = flag.short_name
? `\`-${flag.short_name}\`, \`--${flag.name}\``
: `\`--${flag.name}\``;
lines.push(`| ${nameCell} | ${escapeTableCell(flag.help)} |`);
}
lines.push('');
}
// Nested subcommands
if (sub.subcommands.length > 0) {
for (const nested of sub.subcommands) {
lines.push(`##### \`cua-driver ${cmd.name} ${sub.name} ${nested.name}\``);
lines.push('');
lines.push(escapeMdxText(nested.abstract));
lines.push('');
if (nested.discussion) {
lines.push(escapeMdxText(nested.discussion));
lines.push('');
}
if (nested.arguments.length > 0) {
lines.push('**Arguments:**');
lines.push('');
lines.push('| Name | Type | Required | Description |');
lines.push('| ---- | ---- | -------- | ----------- |');
for (const arg of nested.arguments) {
const required = arg.is_optional ? 'No' : 'Yes';
lines.push(
`| \`<${arg.name}>\` | ${escapeTableCell(arg.type)} | ${required} | ${escapeTableCell(arg.help)} |`
);
}
lines.push('');
}
if (nested.options.length > 0) {
lines.push('**Options:**');
lines.push('');
lines.push('| Name | Type | Default | Description |');
lines.push('| ---- | ---- | ------- | ----------- |');
for (const opt of nested.options) {
const nameCell = opt.short_name
? `\`-${opt.short_name}\`, \`--${opt.name}\``
: `\`--${opt.name}\``;
const defaultVal = opt.default_value != null ? opt.default_value : '—';
lines.push(
`| ${nameCell} | ${escapeTableCell(opt.type)} | ${escapeTableCell(defaultVal)} | ${escapeTableCell(opt.help)} |`
);
}
lines.push('');
}
if (nested.flags.length > 0) {
lines.push('**Flags:**');
lines.push('');
lines.push('| Name | Description |');
lines.push('| ---- | ----------- |');
for (const flag of nested.flags) {
const nameCell = flag.short_name
? `\`-${flag.short_name}\`, \`--${flag.name}\``
: `\`--${flag.name}\``;
lines.push(`| ${nameCell} | ${escapeTableCell(flag.help)} |`);
}
lines.push('');
}
}
}
}
}
return lines;
}
// ============================================================================
// MCP Tools Generator
// ============================================================================
export function generateMCPToolsMDX(docs: MCPDocumentation, releasedVersion: string): string {
const lines: string[] = [];
// Frontmatter — must be at the very beginning of the file
lines.push('---');
lines.push('title: MCP Tools');
lines.push('description: Reference for every MCP tool Cua Driver exposes');
lines.push('---');
lines.push('');
lines.push(`{/*
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/cua-driver.ts
Source: cua-driver dump-docs
Version: ${releasedVersion}
*/}`);
lines.push('');
lines.push("import { Callout } from 'fumadocs-ui/components/callout';");
lines.push('');
// Introduction — mirror the existing hand-written header prose
lines.push(
`\`cua-driver\` exposes ${docs.tools.length} MCP tools through a single stdio server (\`cua-driver mcp\`). Every tool is also callable from the shell as \`cua-driver <name> '<JSON-args>'\`.`
);
lines.push('');
lines.push(
'Tool names are `snake_case`. Responses are MCP `CallTool.Result` envelopes: a text content block prefixed with a `✅` summary (or the error reason on failure), plus optional image or structured-content blocks on tools that produce them. See the [CLI reference](/reference/cua-driver/cli-reference) for CLI-specific options like `--socket` and `--screenshot-out-file`.'
);
lines.push('');
lines.push(
'For the cross-cutting parameter contract (shared parameters, required-parameter rules, platform-specific parameters) and the action response shape, see [MCP tool notes](/reference/cua-driver/mcp-tool-notes).'
);
lines.push('');
lines.push('<Callout type="info">');
lines.push(
' Tool names here match the CLI form exactly. `cua-driver list_apps` and the MCP `list_apps` tool run the same code path.'
);
lines.push('</Callout>');
lines.push('');
lines.push('<Callout type="info">');
lines.push(
" **TCC auto-delegation.** When an MCP client spawns `cua-driver mcp` from an IDE terminal (Claude Code, Cursor, VS Code, Warp), macOS attributes the subprocess to the parent terminal — not `CuaDriver.app` — so AX probes fail against the wrong bundle id. `mcp` detects this and auto-launches a `cua-driver serve` daemon via `open -n -g -a CuaDriver --args serve`, then proxies every tool call through the daemon's Unix socket. Tool semantics are identical to the in-process path; no Python bridge is needed. Pass `--no-daemon-relaunch` (or set `CUA_DRIVER_MCP_NO_RELAUNCH=1`) to force in-process execution. See the [process model](/reference/cua-driver/process-model) for the full lifecycle, failure modes, and wrapper-author guidance."
);
lines.push('</Callout>');
lines.push('');
const categories: Array<{ title: string; tools: string[] }> = [
{
title: 'Inspection tools',
tools: [
'list_apps',
'list_windows',
'get_window_state',
'get_accessibility_tree',
'get_desktop_state',
'get_screen_size',
'get_cursor_position',
'get_config',
'get_recording_state',
'get_agent_cursor_state',
],
},
{
title: 'Action tools',
tools: [
'launch_app',
'kill_app',
'bring_to_front',
'click',
'double_click',
'right_click',
'drag',
'type_text',
'press_key',
'hotkey',
'set_value',
'scroll',
'move_cursor',
'zoom',
],
},
{
title: 'Browser tools',
tools: ['page'],
},
{
title: 'Recording tools',
tools: ['start_recording', 'stop_recording', 'replay_trajectory'],
},
{
title: 'Configuration tools',
tools: [
'set_config',
'start_session',
'end_session',
'set_agent_cursor_enabled',
'set_agent_cursor_motion',
'set_agent_cursor_style',
],
},
{
title: 'Maintenance tools',
tools: [
'check_permissions',
'health_report',
'check_for_update',
'install_ffmpeg',
],
},
];
const byName = new Map(docs.tools.map((tool) => [tool.name, tool]));
const emitted = new Set<string>();
for (const category of categories) {
const tools = category.tools
.map((name) => byName.get(name))
.filter((tool): tool is MCPToolDoc => Boolean(tool));
if (tools.length === 0) continue;
lines.push(`## ${category.title}`);
lines.push('');
for (const tool of tools) {
emitted.add(tool.name);
lines.push(...generateMCPToolDoc(tool));
}
}
const uncategorised = docs.tools.filter((tool) => !emitted.has(tool.name));
if (uncategorised.length > 0) {
lines.push('## Other tools');
lines.push('');
for (const tool of uncategorised) {
lines.push(...generateMCPToolDoc(tool));
}
}
return lines.join('\n');
}
export function generateMCPToolDoc(tool: MCPToolDoc): string[] {
const lines: string[] = [];
lines.push(`### \`${tool.name}\``);
lines.push('');
lines.push(escapeMdxText(tool.description));
lines.push('');
const properties = tool.input_schema.properties ?? {};
const required = new Set(tool.input_schema.required ?? []);
const propertyNames = Object.keys(properties);
if (propertyNames.length === 0) {
lines.push('**Arguments:** none.');
lines.push('');
} else {
lines.push('**Arguments:**');
lines.push('');
for (const propName of propertyNames) {
const prop = properties[propName];
const isRequired = required.has(propName);
const requiredLabel = isRequired ? 'required' : 'optional';
const typeLabel = formatPropertyType(prop);
const description = escapeMdxText(prop.description ?? '');
const suffix = description ? `: ${description}` : '';
lines.push(`- \`${propName}\` (${typeLabel}, ${requiredLabel})${suffix}`);
}
lines.push('');
}
// Synthesize a minimal JSON example from required params only.
// Skip the example entirely when there are no required params (tools
// like check_permissions where everything is optional) or when the
// required set is empty after filtering — avoid emitting {} for tools
// that are only valid with at least one of several mutually-optional groups.
const exampleObj: Record<string, unknown> = {};
for (const propName of propertyNames) {
if (required.has(propName)) {
exampleObj[propName] = syntheticExampleValue(propName, properties[propName]);
}
}
if (Object.keys(exampleObj).length > 0) {
lines.push('```json');
lines.push(JSON.stringify(exampleObj));
lines.push('```');
lines.push('');
}
return lines;
}
function escapeTableCell(value: string): string {
return escapeMdxText(value.replace(/\n/g, ' ')).replace(/\|/g, '\\|');
}
function escapeMdxText(value: string): string {
return value
.split(/(`[^`]*`)/g)
.map((segment) => {
if (segment.startsWith('`') && segment.endsWith('`')) {
return segment;
}
return segment
.replace(/\{/g, '&#123;')
.replace(/\}/g, '&#125;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
})
.join('');
}
function formatPropertyType(prop: MCPPropertyDoc): string {
if (prop.type === 'array') {
const itemType = prop.items?.type ?? 'unknown';
return `array of ${itemType}`;
}
return prop.type;
}
function syntheticExampleValue(name: string, prop: MCPPropertyDoc): unknown {
switch (prop.type) {
case 'integer':
if (name === 'pid') return 844;
if (name === 'window_id') return 10725;
if (name === 'element_index') return 14;
if (name === 'x' || name === 'x1' || name === 'x2' || name === 'from_x' || name === 'to_x')
return 100;
if (name === 'y' || name === 'y1' || name === 'y2' || name === 'from_y' || name === 'to_y')
return 200;
return 1;
case 'number':
if (name === 'x' || name === 'x1' || name === 'x2' || name === 'from_x' || name === 'to_x')
return 100;
if (name === 'y' || name === 'y1' || name === 'y2' || name === 'from_y' || name === 'to_y')
return 200;
return 0.5;
case 'boolean':
return false;
case 'array':
if (name === 'keys') return ['cmd', 'c'];
if (name === 'modifiers') return ['cmd'];
return [];
case 'string':
if (name === 'text') return 'hello';
if (name === 'key') return 'return';
if (name === 'direction') return 'down';
if (name === 'action') return 'get_text';
if (name === 'bundle_id') return 'com.apple.finder';
if (name === 'value') return '42';
if (name === 'key_path' || name === 'key') return 'capture_mode';
if (name === 'dir' || name === 'output_dir') return '~/cua-trajectories/demo1';
return 'example';
default:
return 'value';
}
}
// ============================================================================
// Run
// ============================================================================
main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});
@@ -0,0 +1,309 @@
#!/usr/bin/env python3
"""
Extract Python API documentation using griffe.
This script extracts structured documentation from Python packages
without requiring them to be installed or imported.
Usage:
python extract_python_docs.py <package_path> <package_name>
Example:
python extract_python_docs.py libs/python/computer/computer computer
"""
import json
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from griffe import (
Alias,
Attribute,
Class,
DocstringSectionKind,
Function,
Module,
Object,
load,
)
def extract_docstring_sections(obj: Object) -> Dict[str, Any]:
"""Extract parsed docstring sections from an object."""
result = {
"description": "",
"params": [],
"returns": None,
"raises": [],
"examples": [],
}
if not obj.docstring:
return result
# Get parsed sections (griffe parses automatically based on detected style)
try:
sections = obj.docstring.parsed
except Exception:
# Fallback to raw docstring
result["description"] = str(obj.docstring.value) if obj.docstring else ""
return result
for section in sections:
if section.kind == DocstringSectionKind.text:
result["description"] = str(section.value).strip()
elif section.kind == DocstringSectionKind.parameters:
for param in section.value:
result["params"].append(
{
"name": param.name,
"type": str(param.annotation) if param.annotation else "",
"description": param.description or "",
"default": str(param.default) if param.default else None,
}
)
elif section.kind == DocstringSectionKind.returns:
ret = section.value
if ret:
result["returns"] = {
"type": (
str(ret.annotation) if hasattr(ret, "annotation") and ret.annotation else ""
),
"description": ret.description if hasattr(ret, "description") else str(ret),
}
elif section.kind == DocstringSectionKind.raises:
for exc in section.value:
result["raises"].append(
{
"type": str(exc.annotation) if exc.annotation else "",
"description": exc.description or "",
}
)
elif section.kind == DocstringSectionKind.examples:
result["examples"].append(str(section.value))
return result
def extract_function(fn: Function, is_method: bool = False) -> Dict[str, Any]:
"""Extract function/method documentation."""
# Check if async (stored in labels)
is_async = "async" in getattr(fn, "labels", set())
# Build signature
params = []
for param in fn.parameters:
param_str = param.name
if param.annotation:
param_str += f": {param.annotation}"
if param.default is not None:
param_str += f" = {param.default}"
params.append(param_str)
signature = f"{'async ' if is_async else ''}def {fn.name}({', '.join(params)})"
if fn.returns:
signature += f" -> {fn.returns}"
docstring_data = extract_docstring_sections(fn)
return {
"name": fn.name,
"signature": signature,
"is_async": is_async,
"is_method": is_method,
"description": docstring_data["description"],
"parameters": docstring_data["params"],
"returns": docstring_data["returns"],
"raises": docstring_data["raises"],
"examples": docstring_data["examples"],
"is_private": fn.name.startswith("_") and not fn.name.startswith("__"),
"is_dunder": fn.name.startswith("__") and fn.name.endswith("__"),
}
def extract_attribute(attr: Attribute) -> Dict[str, Any]:
"""Extract attribute/property documentation."""
return {
"name": attr.name,
"type": str(attr.annotation) if attr.annotation else "",
"description": str(attr.docstring.value) if attr.docstring else "",
"default": str(attr.value) if attr.value else None,
"is_private": attr.name.startswith("_"),
}
def resolve_member(member: Any) -> Any:
"""Resolve an alias to its target if needed."""
if isinstance(member, Alias):
try:
return member.target
except Exception:
return member
return member
def extract_class(cls: Class) -> Dict[str, Any]:
"""Extract class documentation."""
docstring_data = extract_docstring_sections(cls)
# Extract methods
methods = []
for name, member in cls.members.items():
member = resolve_member(member)
if isinstance(member, Function):
method_doc = extract_function(member, is_method=True)
# Skip private methods except __init__
if not method_doc["is_private"] or name == "__init__":
methods.append(method_doc)
# Extract attributes/properties
attributes = []
for name, member in cls.members.items():
member = resolve_member(member)
if isinstance(member, Attribute) and not name.startswith("_"):
attributes.append(extract_attribute(member))
# Get base classes
bases = []
for base in cls.bases:
bases.append(str(base))
return {
"name": cls.name,
"description": docstring_data["description"],
"bases": bases,
"methods": methods,
"attributes": attributes,
"is_private": cls.name.startswith("_"),
}
def extract_module(module: Module, include_private: bool = False) -> Dict[str, Any]:
"""Extract module documentation."""
# Get version from module if available
version = "unknown"
if "__version__" in module.members:
version_attr = module.members["__version__"]
if hasattr(version_attr, "value") and version_attr.value:
version = str(version_attr.value).strip("'\"")
# Get __all__ exports if defined
exports = None
if "__all__" in module.members:
all_attr = module.members["__all__"]
if hasattr(all_attr, "value") and all_attr.value:
try:
# Try to parse __all__ as a list
exports = eval(str(all_attr.value))
except Exception:
exports = None
# Extract classes
classes = []
for name, member in module.members.items():
member = resolve_member(member)
if isinstance(member, Class):
# Include if in __all__ or if public
if exports is None:
if not name.startswith("_") or include_private:
classes.append(extract_class(member))
elif name in exports:
classes.append(extract_class(member))
# Extract module-level functions
functions = []
for name, member in module.members.items():
member = resolve_member(member)
if isinstance(member, Function):
if exports is None:
if not name.startswith("_") or include_private:
functions.append(extract_function(member))
elif name in exports:
functions.append(extract_function(member))
return {
"name": module.name,
"version": version,
"docstring": str(module.docstring.value) if module.docstring else "",
"exports": exports,
"classes": classes,
"functions": functions,
}
def extract_package_docs(package_path: str, package_name: str) -> Dict[str, Any]:
"""
Extract documentation from a Python package.
Args:
package_path: Path to the package directory (e.g., 'libs/python/computer/computer')
package_name: Name of the package to load (e.g., 'computer')
Returns:
Dictionary containing structured documentation
"""
# Resolve paths
package_dir = Path(package_path).resolve()
search_path = package_dir.parent
# Load the package using griffe
try:
package = load(
package_name,
search_paths=[str(search_path)],
)
except Exception as e:
return {
"error": f"Failed to load package: {e}",
"name": package_name,
"version": "unknown",
"modules": [],
}
# Extract main module
main_module = extract_module(package)
# Extract submodules
submodules = []
for name, member in package.members.items():
member = resolve_member(member)
if isinstance(member, Module) and not name.startswith("_"):
submodule_doc = extract_module(member)
if submodule_doc["classes"] or submodule_doc["functions"]:
submodules.append(submodule_doc)
return {
"name": package_name,
"version": main_module["version"],
"docstring": main_module["docstring"],
"exports": main_module["exports"],
"classes": main_module["classes"],
"functions": main_module["functions"],
"submodules": submodules,
}
def main():
"""Main entry point."""
if len(sys.argv) < 3:
print("Usage: python extract_python_docs.py <package_path> <package_name>", file=sys.stderr)
print(
"Example: python extract_python_docs.py libs/python/computer/computer computer",
file=sys.stderr,
)
sys.exit(1)
package_path = sys.argv[1]
package_name = sys.argv[2]
docs = extract_package_docs(package_path, package_name)
print(json.dumps(docs, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,546 @@
#!/usr/bin/env npx tsx
/**
* Changelog Generator
*
* Generates changelog MDX files for each SDK by parsing GitHub releases.
* Fetches all releases, cleans up noisy auto-generated body content, and
* produces clean per-SDK changelogs grouped by major.minor version.
*
* Usage:
* npx tsx scripts/docs-generators/generate-changelog.ts # Generate all
* npx tsx scripts/docs-generators/generate-changelog.ts --check # Check for drift
* npx tsx scripts/docs-generators/generate-changelog.ts --sdk=computer # Specific SDK
*/
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
// ============================================================================
// Types
// ============================================================================
interface GitHubRelease {
tagName: string;
name: string;
body: string;
publishedAt: string;
isDraft: boolean;
isPrerelease: boolean;
}
interface SDKConfig {
name: string;
displayName: string;
tagPrefix: string;
outputPath: string;
}
// ============================================================================
// Configuration
// ============================================================================
const ROOT_DIR = path.resolve(__dirname, '../..');
const DOCS_BASE = path.join(ROOT_DIR, 'docs/content/docs');
const GITHUB_REPO = 'trycua/cua';
const SDK_CONFIGS: SDKConfig[] = [
{
name: 'computer',
displayName: 'Computer SDK',
tagPrefix: 'computer-v',
outputPath: 'cua/reference/computer-sdk/changelog.mdx',
},
{
name: 'agent',
displayName: 'Agent SDK',
tagPrefix: 'agent-v',
outputPath: 'cua/reference/agent-sdk/changelog.mdx',
},
{
name: 'computer-server',
displayName: 'Desktop Sandbox',
tagPrefix: 'computer-server-v',
outputPath: 'cua/reference/desktop-sandbox/changelog.mdx',
},
{
name: 'lume',
displayName: 'Lume',
tagPrefix: 'lume-v',
outputPath: 'reference/lume/changelog.mdx',
},
{
name: 'cuabot',
displayName: 'Cua-Bot',
tagPrefix: 'cuabot-v',
outputPath: 'cuabot/reference/changelog.mdx',
},
];
// ============================================================================
// Main
// ============================================================================
async function main() {
const args = process.argv.slice(2);
const checkOnly = args.includes('--check');
const sdkArg = args.find((a) => a.startsWith('--sdk='));
const targetSdk = sdkArg?.split('=')[1];
console.log('Changelog Generator');
console.log('======================\n');
// Fetch ALL releases once (across all SDKs)
console.log('Fetching all GitHub releases...');
const allReleases = fetchAllReleases();
console.log(`Found ${allReleases.length} total releases\n`);
let hasErrors = false;
for (const config of SDK_CONFIGS) {
if (targetSdk && targetSdk !== config.name) {
continue;
}
console.log(`Processing ${config.displayName}...`);
// Filter releases for this SDK
const sdkReleases = allReleases.filter(
(r) => r.tagName.startsWith(config.tagPrefix) && !r.isDraft
);
// Sort by version descending
sdkReleases.sort((a, b) => {
const versionA = a.tagName.replace(config.tagPrefix, '');
const versionB = b.tagName.replace(config.tagPrefix, '');
return compareVersions(versionB, versionA);
});
console.log(` Found ${sdkReleases.length} releases`);
if (sdkReleases.length === 0) {
console.log(` No releases found for ${config.displayName}`);
continue;
}
// Fetch body for each release
const releasesWithBody = fetchBodies(sdkReleases);
// Generate MDX
const mdx = generateChangelogMDX(config, releasesWithBody);
// Output path
const outputPath = path.join(DOCS_BASE, config.outputPath);
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
if (checkOnly) {
if (fs.existsSync(outputPath)) {
const existing = fs.readFileSync(outputPath, 'utf-8');
if (existing !== mdx) {
console.error(` ${config.name} changelog is out of sync`);
hasErrors = true;
} else {
console.log(` ${config.name} changelog is up to date`);
}
} else {
console.error(` ${config.name} changelog does not exist`);
hasErrors = true;
}
} else {
fs.writeFileSync(outputPath, mdx);
console.log(` Generated ${path.relative(ROOT_DIR, outputPath)}`);
}
}
if (hasErrors) {
if (checkOnly) {
console.error("\nRun 'npx tsx scripts/docs-generators/generate-changelog.ts' to update");
}
process.exit(1);
}
console.log('\nChangelog generation complete!');
}
// ============================================================================
// GitHub Release Fetching
// ============================================================================
/**
* Fetch all releases at once (without body) to avoid per-SDK limit issues.
* Uses --limit 500 to get all releases across the entire repo.
*/
function fetchAllReleases(): GitHubRelease[] {
try {
const output = execSync(
'gh release list --limit 500 --json tagName,name,publishedAt,isDraft,isPrerelease',
{ encoding: 'utf-8', cwd: ROOT_DIR, maxBuffer: 10 * 1024 * 1024 }
);
return JSON.parse(output);
} catch (error) {
console.error(`Failed to fetch releases: ${error}`);
return [];
}
}
/**
* Fetch release bodies individually for a filtered set of releases.
*/
function fetchBodies(releases: GitHubRelease[]): GitHubRelease[] {
const results: GitHubRelease[] = [];
for (const release of releases) {
try {
const output = execSync(`gh release view "${release.tagName}" --json body`, {
encoding: 'utf-8',
cwd: ROOT_DIR,
maxBuffer: 1024 * 1024,
});
const data = JSON.parse(output);
results.push({
...release,
body: data.body || '',
});
} catch {
results.push({ ...release, body: '' });
}
}
return results;
}
function compareVersions(a: string, b: string): number {
const partsA = a.split('.').map((x) => parseInt(x, 10) || 0);
const partsB = b.split('.').map((x) => parseInt(x, 10) || 0);
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const partA = partsA[i] || 0;
const partB = partsB[i] || 0;
if (partA !== partB) {
return partA - partB;
}
}
return 0;
}
// ============================================================================
// MDX Generation
// ============================================================================
function generateChangelogMDX(config: SDKConfig, releases: GitHubRelease[]): string {
const lines: string[] = [];
// Frontmatter
lines.push('---');
lines.push(`title: Changelog`);
lines.push(`description: Release history for ${config.displayName}`);
lines.push('---');
lines.push('');
// Auto-generated notice
lines.push(`{/*`);
lines.push(` AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY`);
lines.push(` Generated by: npx tsx scripts/docs-generators/generate-changelog.ts`);
lines.push(` Last updated: ${new Date().toISOString().split('T')[0]}`);
lines.push(`*/}`);
lines.push('');
// Introduction
lines.push(`# ${config.displayName} Changelog`);
lines.push('');
lines.push(`All notable changes to the ${config.displayName} are documented here.`);
lines.push('');
// Group releases by major.minor version
const grouped = groupByMajorMinor(releases, config.tagPrefix);
for (const [majorMinor, versionReleases] of Object.entries(grouped)) {
lines.push(`## ${majorMinor}.x`);
lines.push('');
for (const release of versionReleases) {
const version = release.tagName.replace(config.tagPrefix, '');
const date = formatDate(release.publishedAt);
lines.push(`### v${version} (${date})`);
lines.push('');
// Process release body
const body = processReleaseBody(release.body);
if (body) {
lines.push(body);
lines.push('');
} else {
lines.push('Maintenance release.');
lines.push('');
}
}
}
return lines.join('\n');
}
function groupByMajorMinor(
releases: GitHubRelease[],
tagPrefix: string
): Record<string, GitHubRelease[]> {
const grouped: Record<string, GitHubRelease[]> = {};
for (const release of releases) {
const version = release.tagName.replace(tagPrefix, '');
const parts = version.split('.');
const majorMinor = `${parts[0]}.${parts[1] || '0'}`;
if (!grouped[majorMinor]) {
grouped[majorMinor] = [];
}
grouped[majorMinor].push(release);
}
// Sort keys by version descending
const sortedKeys = Object.keys(grouped).sort((a, b) => compareVersions(b, a));
const sortedGrouped: Record<string, GitHubRelease[]> = {};
for (const key of sortedKeys) {
sortedGrouped[key] = grouped[key];
}
return sortedGrouped;
}
function formatDate(isoDate: string): string {
const date = new Date(isoDate);
return date.toISOString().split('T')[0];
}
// ============================================================================
// Release Body Processing
// ============================================================================
/**
* Clean up a GitHub release body to produce focused changelog content.
*
* Strips:
* - "## What's Changed" header
* - "## Installation" / "## Installation Options" sections
* - "## Usage" / "## Quick Start" / "## Claude Desktop Integration" sections
* - "## Dependencies" section (condensed to one line if present)
* - Package description boilerplate
* - "**Full Changelog**" links
* - Heading markers ## / ### (to avoid hierarchy conflicts)
* - Commit SHAs like (abc1234)
*
* Adds:
* - Linked PR numbers: (#789) -> ([#789](https://github.com/trycua/cua/pull/789))
*
* Detects:
* - Bot-only releases (all entries are "Bump cua-X") -> "Maintenance release."
*/
function processReleaseBody(body: string): string {
if (!body || body.trim() === '') {
return '';
}
let processed = body;
// Remove "**Full Changelog**" links
processed = processed.replace(/\*\*Full Changelog\*\*:.*$/gm, '');
// Remove package description boilerplate headers
processed = processed.replace(
/^##\s*(Computer control library|Computer Server|Base package|MCP Server|Computer Vision|Toolkit for computer-use|Lightweight webUI|Unified CLI).*$/gm,
''
);
// Remove standalone description lines that match boilerplate patterns
processed = processed.replace(
/^(A FastAPI-based server implementation|This package provides (MCP|enhanced UI)|Manage cloud sandboxes).*$/gm,
''
);
// Remove raw checksum lines (sha256 hex + filename)
processed = processed.replace(/^[a-f0-9]{64}\s+\S+.*$/gm, '');
// Remove markdown table rows that look like checksum tables
processed = processed.replace(/^\|?\s*`?[a-f0-9]{64}`?\s*\|.*$/gm, '');
// Remove checksum table headers
processed = processed.replace(/^\|?\s*SHA256\s*\|.*$/gim, '');
processed = processed.replace(/^\|?\s*-+\s*\|.*$/gm, '');
// Extract dependencies from "## Dependencies" section before removing sections
let depsLine = '';
const depsContent = extractSection(processed, 'Dependencies');
if (depsContent !== null) {
const items = depsContent
.split('\n')
.filter((l) => l.trim().startsWith('*'))
.map((l) => l.replace(/^\*\s*/, '').trim());
if (items.length > 0) {
depsLine = `**Dependencies:** ${items.join(', ')}`;
}
processed = removeSection(processed, 'Dependencies');
}
// Remove boilerplate sections (BEFORE removing "What's Changed" heading,
// so section boundaries are intact for removeSection to work correctly)
const sectionsToRemove = [
'Installation Options',
'Installation with script',
'Installation',
'Usage',
'Claude Desktop Integration',
'Quick Start',
];
for (const section of sectionsToRemove) {
processed = removeSection(processed, section);
}
// NOW remove "What's Changed" / "Whats Changed" header line (after sections are removed)
processed = processed.replace(/^#{1,3}\s*What'?s Changed\s*$/gm, '');
// Handle single-line "**Dependencies:**" already in the body (from new workflow format)
const inlineDeps = processed.match(/^\*\*Dependencies:\*\*.*$/m);
if (inlineDeps) {
depsLine = inlineDeps[0].trim();
processed = processed.replace(/^\*\*Dependencies:\*\*.*$/m, '');
}
// Remove release title headings like "# cua-computer v0.4.11"
processed = processed.replace(/^#{1,3}\s*cua-\S+\s+v[\d.]+\s*$/gm, '');
// Strip heading markers (## / ###) from remaining content to avoid hierarchy issues
processed = processed.replace(/^#{1,3}\s+/gm, '');
// Link PR numbers: (#123) -> ([#123](https://github.com/trycua/cua/pull/123))
processed = processed.replace(
/\(#(\d+)\)/g,
`([#$1](https://github.com/${GITHUB_REPO}/pull/$1))`
);
// Strip commit SHAs: (7-8 char hex in parens) from entries
processed = processed.replace(/\s*\([a-f0-9]{7,8}\)/g, '');
// Clean up excessive blank lines
processed = processed.replace(/\n{3,}/g, '\n\n').trim();
// Check if all remaining bullet lines are bot bumps
const contentLines = processed
.split('\n')
.filter((l) => l.trim().startsWith('*') || l.trim().startsWith('-'));
const allBotBumps =
contentLines.length > 0 && contentLines.every((l) => /Bump (cua-|lume)/i.test(l));
if (allBotBumps && !depsLine) {
return 'Maintenance release.';
}
// Re-add dependencies line at the top if present
if (depsLine) {
processed = depsLine + (processed ? '\n\n' + processed : '');
}
// Final check: if nothing meaningful remains
if (!processed.trim()) {
return '';
}
// Escape curly braces outside of code blocks for MDX compatibility
processed = escapeMDXOutsideCode(processed);
return processed;
}
/**
* Extract the content of a markdown section (any heading level) up to the next
* same-or-higher-level heading or end of text.
* Returns null if the section is not found.
*/
function extractSection(text: string, sectionTitle: string): string | null {
const lines = text.split('\n');
let inSection = false;
let sectionLevel = 0;
let content: string[] = [];
for (const line of lines) {
const headerMatch = line.match(
new RegExp(`^(#{1,6})\\s*${escapeRegex(sectionTitle)}\\s*$`, 'i')
);
if (!inSection && headerMatch) {
inSection = true;
sectionLevel = headerMatch[1].length;
continue;
}
if (inSection) {
// Stop at next heading of same or higher level
const headingMatch = line.match(/^(#{1,6})\s/);
if (headingMatch && headingMatch[1].length <= sectionLevel) {
break;
}
content.push(line);
}
}
return inSection ? content.join('\n') : null;
}
/**
* Remove a markdown section (any heading level) and everything until the next
* same-or-higher-level heading or end of text.
*/
function removeSection(text: string, sectionTitle: string): string {
const lines = text.split('\n');
const result: string[] = [];
let inSection = false;
let sectionLevel = 0;
for (const line of lines) {
const headerMatch =
!inSection && line.match(new RegExp(`^(#{1,6})\\s*${escapeRegex(sectionTitle)}\\s*$`, 'i'));
if (headerMatch) {
inSection = true;
sectionLevel = headerMatch[1].length;
continue;
}
if (inSection) {
const headingMatch = line.match(/^(#{1,6})\s/);
if (headingMatch && headingMatch[1].length <= sectionLevel) {
inSection = false;
result.push(line);
}
continue;
}
result.push(line);
}
return result.join('\n');
}
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function escapeMDXOutsideCode(text: string): string {
const codeBlockRegex = /(```[\s\S]*?```|`[^`]+`)/g;
const parts = text.split(codeBlockRegex);
return parts
.map((part) => {
if (part.startsWith('```') || part.startsWith('`')) {
return part;
}
return part.replace(/\{/g, '\\{').replace(/\}/g, '\\}');
})
.join('');
}
// ============================================================================
// Run
// ============================================================================
main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});
@@ -0,0 +1,681 @@
#!/usr/bin/env npx tsx
/**
* Versioned Documentation Generator
*
* Generates API documentation for historical versions by checking out
* tagged versions from git and running the extraction.
*
* Usage:
* npx tsx scripts/docs-generators/generate-versioned-docs.ts # Generate all
* npx tsx scripts/docs-generators/generate-versioned-docs.ts --sdk=computer # Specific SDK
* npx tsx scripts/docs-generators/generate-versioned-docs.ts --list # List available versions
*/
import { execSync, spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
// ============================================================================
// Types
// ============================================================================
type ExtractionType = 'python-griffe' | 'swift-dump-docs' | 'typescript';
interface SDKConfig {
name: string;
displayName: string;
tagPrefix: string;
packageDir: string;
packageName: string;
outputDir: string;
extractionType: ExtractionType;
/** Skip versions below this major.minor (e.g. '0.2' skips 0.1.x) */
minVersion?: string;
/** Base docs directory (defaults to cua/reference under DOCS_BASE) */
docsBasePath?: string;
}
interface VersionInfo {
tag: string;
version: string;
majorMinor: string;
}
// ============================================================================
// Configuration
// ============================================================================
const ROOT_DIR = path.resolve(__dirname, '../..');
const DOCS_BASE = path.join(ROOT_DIR, 'docs/content/docs');
const DOCS_DIR = path.join(DOCS_BASE, 'cua/reference');
const PYTHON_SCRIPT = path.join(__dirname, 'extract_python_docs.py');
const SDK_CONFIGS: SDKConfig[] = [
{
name: 'computer',
displayName: 'Computer SDK',
tagPrefix: 'computer-v',
packageDir: 'libs/python/computer/computer',
packageName: 'computer',
outputDir: 'computer-sdk',
extractionType: 'python-griffe',
},
{
name: 'agent',
displayName: 'Agent SDK',
tagPrefix: 'agent-v',
packageDir: 'libs/python/agent/agent',
packageName: 'agent',
outputDir: 'agent-sdk',
extractionType: 'python-griffe',
},
{
name: 'bench',
displayName: 'Cua Bench',
tagPrefix: 'bench-v',
packageDir: 'libs/cua-bench/cua_bench',
packageName: 'cua_bench',
outputDir: 'reference',
extractionType: 'python-griffe',
docsBasePath: 'cuabench/reference',
},
{
name: 'lume',
displayName: 'Lume',
tagPrefix: 'lume-v',
packageDir: 'libs/lume',
packageName: 'lume',
outputDir: 'reference/lume',
extractionType: 'swift-dump-docs',
minVersion: '0.2',
docsBasePath: 'reference/lume',
},
{
name: 'cuabot',
displayName: 'Cua-Bot',
tagPrefix: 'cuabot-v',
packageDir: 'libs/cuabot/src',
packageName: 'cuabot',
outputDir: 'reference',
extractionType: 'typescript',
minVersion: '1.0',
docsBasePath: 'cuabot/reference',
},
];
// ============================================================================
// Main
// ============================================================================
async function main() {
const args = process.argv.slice(2);
const listOnly = args.includes('--list');
const sdkArg = args.find((a) => a.startsWith('--sdk='));
const targetSdk = sdkArg?.split('=')[1];
console.log('📚 Versioned Documentation Generator');
console.log('====================================\n');
// Check for uncommitted changes
const status = execSync('git status --porcelain', { encoding: 'utf-8', cwd: ROOT_DIR });
if (status.trim()) {
console.warn('⚠️ Warning: You have uncommitted changes. They will be preserved.\n');
}
for (const config of SDK_CONFIGS) {
if (targetSdk && targetSdk !== config.name) {
continue;
}
console.log(`\n📦 ${config.displayName}`);
console.log('─'.repeat(40));
// Get all version tags
const versions = getVersions(config.tagPrefix);
console.log(` Found ${versions.length} version tags`);
if (listOnly) {
// Just list versions
const grouped = groupByMajorMinor(versions);
for (const [majorMinor, versionList] of Object.entries(grouped)) {
console.log(` v${majorMinor}.x: ${versionList.map((v) => v.version).join(', ')}`);
}
continue;
}
// Group by major.minor (we only generate one doc per major.minor)
const grouped = groupByMajorMinor(versions);
for (const [majorMinor, versionList] of Object.entries(grouped)) {
// Skip versions below minVersion
if (config.minVersion && compareVersions(majorMinor, config.minVersion) < 0) {
console.log(` Skipping v${majorMinor} (below minVersion ${config.minVersion})`);
continue;
}
// Use the latest patch version for this major.minor
const latestVersion = versionList[0];
console.log(`\n Generating v${majorMinor} (from ${latestVersion.tag})...`);
try {
// Generate docs for this version
await generateVersionDocs(config, latestVersion, majorMinor);
const files =
config.extractionType === 'swift-dump-docs'
? 'cli-reference.mdx + http-api.mdx'
: 'api.mdx';
console.log(` ✅ Generated v${majorMinor}/${files}`);
} catch (error) {
console.error(` ❌ Failed: ${error}`);
}
}
}
console.log('\n✅ Versioned documentation generation complete!');
}
// ============================================================================
// Version Discovery
// ============================================================================
function getVersions(tagPrefix: string): VersionInfo[] {
try {
const output = execSync(`git tag | grep "^${tagPrefix}"`, {
encoding: 'utf-8',
cwd: ROOT_DIR,
});
return output
.trim()
.split('\n')
.filter(Boolean)
.map((tag) => {
const version = tag.replace(tagPrefix, '');
const parts = version.split('.');
const majorMinor = `${parts[0]}.${parts[1] || '0'}`;
return { tag, version, majorMinor };
})
.sort((a, b) => compareVersions(b.version, a.version)); // Descending
} catch {
return [];
}
}
function groupByMajorMinor(versions: VersionInfo[]): Record<string, VersionInfo[]> {
const grouped: Record<string, VersionInfo[]> = {};
for (const v of versions) {
if (!grouped[v.majorMinor]) {
grouped[v.majorMinor] = [];
}
grouped[v.majorMinor].push(v);
}
// Sort keys descending
const sortedKeys = Object.keys(grouped).sort((a, b) => compareVersions(b, a));
const sorted: Record<string, VersionInfo[]> = {};
for (const key of sortedKeys) {
sorted[key] = grouped[key];
}
return sorted;
}
function compareVersions(a: string, b: string): number {
const partsA = a.split('.').map((x) => parseInt(x, 10) || 0);
const partsB = b.split('.').map((x) => parseInt(x, 10) || 0);
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const partA = partsA[i] || 0;
const partB = partsB[i] || 0;
if (partA !== partB) {
return partA - partB;
}
}
return 0;
}
// ============================================================================
// Documentation Generation
// ============================================================================
async function generateVersionDocs(
config: SDKConfig,
versionInfo: VersionInfo,
majorMinor: string
): Promise<void> {
// Resolve output directory based on config
const baseDir = config.docsBasePath
? path.join(DOCS_BASE, config.docsBasePath)
: path.join(DOCS_DIR, config.outputDir);
const outputDir = path.join(baseDir, `v${majorMinor}`);
// Create output directory
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Checkout the package directory at the tagged version
const packagePath = path.join(ROOT_DIR, config.packageDir);
try {
// Save current state
execSync(`git stash push -m "versioned-docs-temp" -- ${config.packageDir}`, {
cwd: ROOT_DIR,
stdio: 'pipe',
});
} catch {
// No changes to stash, continue
}
try {
// Checkout tagged version of the package
execSync(`git checkout ${versionInfo.tag} -- ${config.packageDir}`, {
cwd: ROOT_DIR,
stdio: 'pipe',
});
if (config.extractionType === 'swift-dump-docs') {
await generateSwiftVersionDocs(config, versionInfo, majorMinor, outputDir);
} else if (config.extractionType === 'typescript') {
await generateTypeScriptVersionDocs(config, versionInfo, majorMinor, outputDir);
} else {
await generatePythonVersionDocs(config, versionInfo, majorMinor, outputDir);
}
} finally {
// Restore HEAD version
execSync(`git checkout HEAD -- ${config.packageDir}`, {
cwd: ROOT_DIR,
stdio: 'pipe',
});
// Restore stashed changes if any
try {
execSync('git stash pop', { cwd: ROOT_DIR, stdio: 'pipe' });
} catch {
// No stash to pop
}
}
}
async function generatePythonVersionDocs(
config: SDKConfig,
versionInfo: VersionInfo,
majorMinor: string,
outputDir: string
): Promise<void> {
const packagePath = path.join(ROOT_DIR, config.packageDir);
const outputPath = path.join(outputDir, 'api.mdx');
// Run extraction
const extractOutput = execSync(
`python3 "${PYTHON_SCRIPT}" "${packagePath}" "${config.packageName}"`,
{ encoding: 'utf-8', cwd: ROOT_DIR, maxBuffer: 10 * 1024 * 1024 }
);
const docs = JSON.parse(extractOutput);
// Generate MDX
const mdx = generateMDX(docs, config, versionInfo, majorMinor);
// Write output
fs.writeFileSync(outputPath, mdx);
// Create meta.json for the version folder
const metaPath = path.join(outputDir, 'meta.json');
fs.writeFileSync(
metaPath,
JSON.stringify(
{
title: `v${majorMinor}`,
description: `${config.displayName} v${majorMinor} API Reference`,
pages: ['api'],
},
null,
2
)
);
}
async function generateSwiftVersionDocs(
config: SDKConfig,
versionInfo: VersionInfo,
majorMinor: string,
outputDir: string
): Promise<void> {
const lumeDir = path.join(ROOT_DIR, config.packageDir);
// Build Lume at this version
try {
execSync('swift build -c release', {
cwd: lumeDir,
stdio: 'pipe',
timeout: 300000, // 5 min build timeout
});
} catch (error) {
throw new Error(`Swift build failed for ${versionInfo.tag}: ${error}`);
}
// Import Lume MDX generators
const { generateCLIReferenceMDX, generateHTTPAPIMDX } = await import('./lume');
// Extract CLI docs
let cliMdx: string;
try {
const cliDocsJson = execSync('.build/release/lume dump-docs --type cli', {
cwd: lumeDir,
encoding: 'utf-8',
});
const cliDocs = JSON.parse(cliDocsJson);
cliMdx = generateVersionedLumeMDX(
generateCLIReferenceMDX(cliDocs),
config,
versionInfo,
majorMinor,
'CLI Reference'
);
} catch (error) {
throw new Error(`CLI docs extraction failed for ${versionInfo.tag}: ${error}`);
}
// Extract API docs
let apiMdx: string;
try {
const apiDocsJson = execSync('.build/release/lume dump-docs --type api', {
cwd: lumeDir,
encoding: 'utf-8',
});
const apiDocs = JSON.parse(apiDocsJson);
apiMdx = generateVersionedLumeMDX(
generateHTTPAPIMDX(apiDocs),
config,
versionInfo,
majorMinor,
'HTTP API Reference'
);
} catch (error) {
throw new Error(`API docs extraction failed for ${versionInfo.tag}: ${error}`);
}
// Write outputs
fs.writeFileSync(path.join(outputDir, 'cli-reference.mdx'), cliMdx);
fs.writeFileSync(path.join(outputDir, 'http-api.mdx'), apiMdx);
// Create meta.json
fs.writeFileSync(
path.join(outputDir, 'meta.json'),
JSON.stringify(
{
title: `v${majorMinor}`,
description: `${config.displayName} v${majorMinor} Reference`,
pages: ['cli-reference', 'http-api'],
},
null,
2
)
);
}
async function generateTypeScriptVersionDocs(
config: SDKConfig,
versionInfo: VersionInfo,
majorMinor: string,
outputDir: string
): Promise<void> {
// Run the typescript-sdk generator to produce MDX, then wrap it with a version callout
const tsGenScript = path.join(__dirname, 'typescript-sdk.ts');
const outputPath = path.join(outputDir, 'api.mdx');
// Run the TS generator targeting cuabot — it will write to the configured output path
// Instead, we run it inline and capture the current version's output, then modify it
try {
// Generate using the typescript-sdk generator
spawnSync('npx', ['tsx', tsGenScript, `--sdk=${config.name}`], {
cwd: ROOT_DIR,
stdio: 'pipe',
});
// Read the generated file and wrap it with version callout
const generatedPath = path.join(ROOT_DIR, 'docs/content/docs/cuabot/reference/index.mdx');
if (fs.existsSync(generatedPath)) {
let content = fs.readFileSync(generatedPath, 'utf-8');
// Replace VersionHeader with old-version callout
const lines = content.split('\n');
const result: string[] = [];
let skipVersionHeader = false;
for (const line of lines) {
if (line.startsWith('<VersionHeader')) {
skipVersionHeader = true;
result.push('<Callout type="warn">');
result.push(
` This is documentation for **v${majorMinor}**. [View latest version](/cuabot/reference).`
);
result.push('</Callout>');
result.push('');
result.push('<div className="flex items-center gap-2 mb-6">');
result.push(
` <span className="px-2 py-1 bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200 rounded text-sm font-mono">v${versionInfo.version}</span>`
);
result.push(
` <span className="text-sm text-fd-muted-foreground">npm install -g cuabot@${versionInfo.version}</span>`
);
result.push('</div>');
result.push('');
continue;
}
if (skipVersionHeader) {
if (line.startsWith('/>')) {
skipVersionHeader = false;
continue;
}
continue;
}
result.push(line);
}
// Update title
content = result
.join('\n')
.replace(/^title: .*$/m, `title: ${config.displayName} v${majorMinor} API Reference`);
fs.writeFileSync(outputPath, content);
}
} catch (error) {
throw new Error(`TypeScript docs generation failed for ${versionInfo.tag}: ${error}`);
}
// Create meta.json
fs.writeFileSync(
path.join(outputDir, 'meta.json'),
JSON.stringify(
{
title: `v${majorMinor}`,
description: `${config.displayName} v${majorMinor} API Reference`,
pages: ['api'],
},
null,
2
)
);
}
/**
* Wrap generated Lume MDX with a version warning callout for historical versions.
* Replaces the VersionHeader (which shows current version) with an old-version notice.
*/
function generateVersionedLumeMDX(
currentMdx: string,
config: SDKConfig,
versionInfo: VersionInfo,
majorMinor: string,
title: string
): string {
// Replace the VersionHeader block with an old-version callout
const lines = currentMdx.split('\n');
const result: string[] = [];
let skipVersionHeader = false;
for (const line of lines) {
// Skip the VersionHeader block
if (line.startsWith('<VersionHeader')) {
skipVersionHeader = true;
// Insert old-version callout instead
result.push('<Callout type="warn">');
result.push(
` This is documentation for **v${majorMinor}**. [View latest version](/reference/lume/cli-reference).`
);
result.push('</Callout>');
result.push('');
result.push('<div className="flex items-center gap-2 mb-6">');
result.push(
` <span className="px-2 py-1 bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200 rounded text-sm font-mono">v${versionInfo.version}</span>`
);
result.push(
` <span className="text-sm text-fd-muted-foreground">curl -fsSL .../install.sh | bash</span>`
);
result.push('</div>');
result.push('');
continue;
}
if (skipVersionHeader) {
if (line.startsWith('/>')) {
skipVersionHeader = false;
continue;
}
continue;
}
result.push(line);
}
return result.join('\n');
}
// ============================================================================
// MDX Generation (simplified version for historical docs)
// ============================================================================
function generateMDX(
docs: any,
config: SDKConfig,
versionInfo: VersionInfo,
majorMinor: string
): string {
const lines: string[] = [];
// Frontmatter
lines.push('---');
lines.push(`title: ${config.displayName} v${majorMinor} API Reference`);
lines.push(`description: API reference for ${config.displayName} version ${majorMinor}`);
lines.push('---');
lines.push('');
// Auto-generated notice
lines.push(`{/*`);
lines.push(` AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY`);
lines.push(` Generated by: npx tsx scripts/docs-generators/generate-versioned-docs.ts`);
lines.push(` Source tag: ${versionInfo.tag}`);
lines.push(` Version: ${versionInfo.version}`);
lines.push(`*/}`);
lines.push('');
// Imports
lines.push("import { Callout } from 'fumadocs-ui/components/callout';");
lines.push('');
// Version notice — link to the folder root (index.mdx is the landing page)
const latestHref = config.docsBasePath
? `/${config.docsBasePath.replace(/\/$/, '')}/${config.outputDir}`
: `/cua/reference/${config.outputDir}`;
lines.push('<Callout type="warn">');
lines.push(
` This is documentation for **v${majorMinor}**. [View latest version](${latestHref}).`
);
lines.push('</Callout>');
lines.push('');
// Version badge
const pipName = config.packageName.replace(/_/g, '-');
const fullPipName = pipName.startsWith('cua') ? pipName : `cua-${pipName}`;
lines.push('<div className="flex items-center gap-2 mb-6">');
lines.push(
` <span className="px-2 py-1 bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200 rounded text-sm font-mono">v${versionInfo.version}</span>`
);
lines.push(
` <span className="text-sm text-fd-muted-foreground">pip install ${fullPipName}==${versionInfo.version}</span>`
);
lines.push('</div>');
lines.push('');
// Package description
if (docs.docstring) {
lines.push(escapeMDX(docs.docstring));
lines.push('');
}
// Classes
if (docs.classes && docs.classes.length > 0) {
lines.push('## Classes');
lines.push('');
lines.push('| Class | Description |');
lines.push('|-------|-------------|');
for (const cls of docs.classes) {
if (!cls.is_private) {
const desc = escapeMDX(cls.description?.split('\n')[0]) || 'No description';
lines.push(`| \`${cls.name}\` | ${desc} |`);
}
}
lines.push('');
// Class details
for (const cls of docs.classes) {
if (!cls.is_private) {
lines.push(`## ${cls.name}`);
lines.push('');
if (cls.description) {
lines.push(escapeMDX(cls.description));
lines.push('');
}
// Methods
const publicMethods = (cls.methods || []).filter(
(m: any) => !m.is_private && !m.is_dunder && m.name !== '__init__'
);
if (publicMethods.length > 0) {
lines.push('### Methods');
lines.push('');
for (const method of publicMethods) {
lines.push(`#### ${cls.name}.${method.name}`);
lines.push('');
lines.push('```python');
lines.push(method.signature || `def ${method.name}(...)`);
lines.push('```');
lines.push('');
if (method.description) {
lines.push(escapeMDX(method.description));
lines.push('');
}
}
}
}
}
}
return lines.join('\n');
}
function escapeMDX(text: string): string {
if (!text) return text;
return text.replace(/\{/g, '\\{').replace(/\}/g, '\\}');
}
// ============================================================================
// Run
// ============================================================================
main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});
+751
View File
@@ -0,0 +1,751 @@
#!/usr/bin/env npx tsx
/**
* Lume Documentation Generator
*
* Generates MDX documentation files from Lume's dump-docs command output.
* This ensures documentation stays synchronized with the source code.
*
* Usage:
* npx tsx scripts/docs-generators/lume.ts # Generate docs
* npx tsx scripts/docs-generators/lume.ts --check # Check for drift (CI mode)
*/
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
// ============================================================================
// Types
// ============================================================================
export interface CLIDocumentation {
name: string;
version: string;
abstract: string;
commands: CommandDoc[];
}
export interface CommandDoc {
name: string;
abstract: string;
discussion?: string;
arguments: ArgumentDoc[];
options: OptionDoc[];
flags: FlagDoc[];
subcommands: CommandDoc[];
}
export interface ArgumentDoc {
name: string;
help: string;
type: string;
is_optional: boolean;
}
export interface OptionDoc {
name: string;
short_name?: string;
help: string;
type: string;
default_value?: string;
is_optional: boolean;
}
export interface FlagDoc {
name: string;
short_name?: string;
help: string;
default_value: boolean;
}
export interface HTTPAPIDocumentation {
base_path: string;
version: string;
description: string;
endpoints: APIEndpointDoc[];
}
export interface APIEndpointDoc {
method: string;
path: string;
description: string;
category: string;
path_parameters: APIParameterDoc[];
query_parameters: APIParameterDoc[];
request_body?: APIRequestBodyDoc;
response_body: APIResponseDoc;
status_codes: APIStatusCodeDoc[];
}
export interface APIParameterDoc {
name: string;
type: string;
required: boolean;
description: string;
}
export interface APIRequestBodyDoc {
content_type: string;
description: string;
fields: APIFieldDoc[];
}
export interface APIResponseDoc {
content_type: string;
description: string;
fields?: APIFieldDoc[];
}
export interface APIFieldDoc {
name: string;
type: string;
required: boolean;
description: string;
default_value?: string;
}
export interface APIStatusCodeDoc {
code: number;
description: string;
}
// ============================================================================
// Configuration
// ============================================================================
const ROOT_DIR = path.resolve(__dirname, '../..');
const LUME_DIR = path.join(ROOT_DIR, 'libs', 'lume');
const DOCS_OUTPUT_DIR = path.join(ROOT_DIR, 'docs', 'content', 'docs', 'reference', 'lume');
const TAG_PREFIX = 'lume-v';
// ============================================================================
// Version Discovery
// ============================================================================
interface VersionInfo {
version: string;
href: string;
isCurrent: boolean;
}
/**
* Get the latest released version from git tags.
*/
export function getLatestReleasedVersion(): string {
try {
const output = execSync(`git tag | grep "^${TAG_PREFIX}" | sort -V | tail -1`, {
encoding: 'utf-8',
cwd: ROOT_DIR,
}).trim();
if (output) {
return output.replace(TAG_PREFIX, '');
}
} catch {
// Fall through
}
return '0.0.0';
}
/**
* Discover available versioned doc folders and build version list.
*/
export function discoverVersions(currentVersion: string): VersionInfo[] {
const versions: VersionInfo[] = [];
const currentMajorMinor = currentVersion.split('.').slice(0, 2).join('.');
// Add current version (latest)
versions.push({
version: currentMajorMinor,
href: '/reference/lume/cli-reference',
isCurrent: true,
});
// Discover versioned folders (v0.2, v0.1, etc.)
if (fs.existsSync(DOCS_OUTPUT_DIR)) {
const entries = fs.readdirSync(DOCS_OUTPUT_DIR, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('v')) {
const version = entry.name.substring(1);
if (version === currentMajorMinor) continue;
versions.push({
version,
href: `/reference/lume/${entry.name}/cli-reference`,
isCurrent: false,
});
}
}
}
// Sort descending
versions.sort((a, b) => {
const partsA = a.version.split('.').map((x) => parseInt(x, 10) || 0);
const partsB = b.version.split('.').map((x) => parseInt(x, 10) || 0);
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const partA = partsA[i] || 0;
const partB = partsB[i] || 0;
if (partA !== partB) return partB - partA;
}
return 0;
});
return versions;
}
// ============================================================================
// Main
// ============================================================================
async function main() {
const args = process.argv.slice(2);
const checkOnly = args.includes('--check') || args.includes('--check-only');
console.log('🔧 Lume Documentation Generator');
console.log('================================\n');
// Step 1: Build Lume if needed
console.log('📦 Building Lume...');
try {
execSync('swift build -c release', {
cwd: LUME_DIR,
stdio: 'inherit',
});
} catch (error) {
console.error('❌ Failed to build Lume');
process.exit(1);
}
// Step 2: Get CLI documentation
console.log('\n📖 Extracting CLI documentation...');
const cliDocsJson = execSync('.build/release/lume dump-docs --type cli', {
cwd: LUME_DIR,
encoding: 'utf-8',
});
const cliDocs: CLIDocumentation = JSON.parse(cliDocsJson);
console.log(` Found ${cliDocs.commands.length} commands`);
// Step 3: Get API documentation
console.log('📖 Extracting API documentation...');
const apiDocsJson = execSync('.build/release/lume dump-docs --type api', {
cwd: LUME_DIR,
encoding: 'utf-8',
});
const apiDocs: HTTPAPIDocumentation = JSON.parse(apiDocsJson);
console.log(` Found ${apiDocs.endpoints.length} endpoints`);
// Step 4: Generate MDX files
console.log('\n📝 Generating documentation files...');
const cliMdx = generateCLIReferenceMDX(cliDocs);
const apiMdx = generateHTTPAPIMDX(apiDocs);
const cliPath = path.join(DOCS_OUTPUT_DIR, 'cli-reference.mdx');
const apiPath = path.join(DOCS_OUTPUT_DIR, 'http-api.mdx');
if (checkOnly) {
// Check mode: compare with existing files
console.log('\n🔍 Checking for documentation drift...');
let hasDrift = false;
if (fs.existsSync(cliPath)) {
const existingCli = fs.readFileSync(cliPath, 'utf-8');
if (existingCli !== cliMdx) {
console.error('❌ cli-reference.mdx is out of sync with source code');
hasDrift = true;
} else {
console.log('✅ cli-reference.mdx is up to date');
}
} else {
console.error('❌ cli-reference.mdx does not exist');
hasDrift = true;
}
if (fs.existsSync(apiPath)) {
const existingApi = fs.readFileSync(apiPath, 'utf-8');
if (existingApi !== apiMdx) {
console.error('❌ http-api.mdx is out of sync with source code');
hasDrift = true;
} else {
console.log('✅ http-api.mdx is up to date');
}
} else {
console.error('❌ http-api.mdx does not exist');
hasDrift = true;
}
if (hasDrift) {
console.error("\n💡 Run 'npx tsx scripts/docs-generators/lume.ts' to update documentation");
process.exit(1);
}
console.log('\n✅ All Lume documentation is up to date!');
} else {
// Generate mode: write files
fs.writeFileSync(cliPath, cliMdx);
console.log(` ✅ Generated ${path.relative(ROOT_DIR, cliPath)}`);
fs.writeFileSync(apiPath, apiMdx);
console.log(` ✅ Generated ${path.relative(ROOT_DIR, apiPath)}`);
console.log('\n✅ Lume documentation generated successfully!');
}
}
// ============================================================================
// CLI Reference Generator
// ============================================================================
export function generateCLIReferenceMDX(docs: CLIDocumentation): string {
const lines: string[] = [];
const documentedVersion = docs.version || getLatestReleasedVersion();
// Header - frontmatter MUST be at the very beginning of the file
lines.push('---');
lines.push('title: CLI Reference');
lines.push('description: Command Line Interface reference for Lume');
lines.push('---');
lines.push('');
lines.push(`{/*
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/lume.ts
Source: lume dump-docs --type cli
Version: ${documentedVersion}
*/}`);
lines.push('');
lines.push(`${docs.abstract}`);
lines.push('');
lines.push(
`Documented against Lume **${documentedVersion}**. Run \`lume --version\` for your installed version.`
);
lines.push('');
lines.push('For installation steps, see [Install Lume](/how-to-guides/lume/install-lume).');
lines.push('');
const groups = [
{ title: 'VM Management', commands: ['create', 'run', 'stop', 'delete', 'clone'] },
{ title: 'VM Information and Configuration', commands: ['ls', 'get', 'set'] },
{
title: 'Image Management',
commands: ['images', 'pull', 'push', 'convert', 'ipsw', 'prune'],
},
{ title: 'Guest Access and Security', commands: ['ssh', 'setup', 'sip'] },
{
title: 'Configuration and Server',
commands: ['config', 'serve', 'logs', 'check-update', 'update'],
},
{ title: 'Developer Tools', commands: ['dump-docs'] },
];
const assignedCommands = groups.flatMap((group) => group.commands);
const duplicateAssignments = assignedCommands.filter(
(name, index) => assignedCommands.indexOf(name) !== index
);
const ungroupedCommands = docs.commands
.map((command) => command.name)
.filter((name) => !assignedCommands.includes(name));
if (duplicateAssignments.length > 0 || ungroupedCommands.length > 0) {
throw new Error(
`CLI command grouping mismatch. Ungrouped: ${[...new Set(ungroupedCommands)].sort().join(', ') || 'none'}; duplicated: ${[...new Set(duplicateAssignments)].sort().join(', ') || 'none'}`
);
}
for (const group of groups) {
lines.push(`## ${group.title}`);
lines.push('');
for (const commandName of group.commands) {
const command = docs.commands.find((candidate) => candidate.name === commandName);
if (command) {
lines.push(...generateCommandDoc(command, '###'));
}
}
}
// Global options
lines.push('## Global Options');
lines.push('');
lines.push('These options are available for all commands:');
lines.push('');
lines.push('- `--help` - Show help information');
lines.push('- `--version` - Show version number');
lines.push('');
return lines.join('\n');
}
export function generateCommandDoc(cmd: CommandDoc, heading: string): string[] {
const lines: string[] = [];
lines.push(`${heading} lume ${cmd.name}`);
lines.push('');
lines.push(cmd.abstract);
lines.push('');
// Arguments
if (cmd.arguments.length > 0) {
lines.push('**Arguments:**');
lines.push('');
lines.push('| Name | Type | Required | Description |');
lines.push('| ---- | ---- | -------- | ----------- |');
for (const arg of cmd.arguments) {
const required = arg.is_optional ? 'No' : 'Yes';
lines.push(`| \`<${arg.name}>\` | ${arg.type} | ${required} | ${arg.help} |`);
}
lines.push('');
}
// Options
if (cmd.options.length > 0) {
lines.push('**Options:**');
lines.push('');
lines.push('| Name | Type | Default | Description |');
lines.push('| ---- | ---- | ------- | ----------- |');
for (const opt of cmd.options) {
const shortFlag = opt.short_name ? `-${opt.short_name}, ` : '';
const defaultVal = opt.default_value || '-';
lines.push(`| \`${shortFlag}--${opt.name}\` | ${opt.type} | ${defaultVal} | ${opt.help} |`);
}
lines.push('');
}
// Flags
if (cmd.flags.length > 0) {
lines.push('**Flags:**');
lines.push('');
lines.push('| Name | Default | Description |');
lines.push('| ---- | ------- | ----------- |');
for (const flag of cmd.flags) {
const shortFlag = flag.short_name ? `-${flag.short_name}, ` : '';
lines.push(`| \`${shortFlag}--${flag.name}\` | ${flag.default_value} | ${flag.help} |`);
}
lines.push('');
}
// Subcommands
if (cmd.subcommands.length > 0) {
lines.push('**Subcommands:**');
lines.push('');
for (const sub of cmd.subcommands) {
lines.push(`- \`lume ${cmd.name} ${sub.name}\` - ${sub.abstract}`);
// Show subcommand details
if (sub.arguments.length > 0 || sub.options.length > 0) {
for (const arg of sub.arguments) {
lines.push(` - \`<${arg.name}>\` - ${arg.help}`);
}
for (const opt of sub.options) {
const shortFlag = opt.short_name ? `-${opt.short_name}, ` : '';
lines.push(` - \`${shortFlag}--${opt.name}\` - ${opt.help}`);
}
}
// Nested subcommands
if (sub.subcommands.length > 0) {
for (const nested of sub.subcommands) {
lines.push(` - \`lume ${cmd.name} ${sub.name} ${nested.name}\` - ${nested.abstract}`);
}
}
}
lines.push('');
}
return lines;
}
// ============================================================================
// HTTP API Reference Generator
// ============================================================================
export function generateHTTPAPIMDX(docs: HTTPAPIDocumentation): string {
const lines: string[] = [];
const documentedVersion = docs.version || getLatestReleasedVersion();
// Header - frontmatter MUST be at the very beginning of the file
lines.push('---');
lines.push('title: API Reference');
lines.push('description: HTTP API reference for Lume server');
lines.push('---');
lines.push('');
lines.push(`{/*
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/lume.ts
Source: lume dump-docs --type api
Version: ${documentedVersion}
*/}`);
lines.push('');
lines.push("import { Tabs, Tab } from 'fumadocs-ui/components/tabs';");
lines.push('');
// Introduction
lines.push(docs.description);
lines.push('');
lines.push(
`Documented against Lume **${documentedVersion}**. Run \`lume --version\` for your installed version.`
);
lines.push('');
lines.push('## Default URL');
lines.push('');
lines.push('```');
lines.push('http://localhost:7777');
lines.push('```');
lines.push('');
lines.push(
'Start the server with `lume serve` or specify a custom port with `lume serve --port <port>`.'
);
lines.push('');
// Group endpoints by category
const categories = [...new Set(docs.endpoints.map((e) => e.category))];
for (const category of categories) {
lines.push(`## ${category}`);
lines.push('');
const categoryEndpoints = docs.endpoints.filter((e) => e.category === category);
for (const endpoint of categoryEndpoints) {
lines.push(...generateEndpointDoc(endpoint));
}
}
return lines.join('\n');
}
export function generateEndpointDoc(endpoint: APIEndpointDoc): string[] {
const lines: string[] = [];
lines.push(`### ${endpoint.description}`);
lines.push('');
lines.push(endpoint.description);
lines.push('');
lines.push(`\`${endpoint.method}: ${endpoint.path}\``);
lines.push('');
// Parameters table (path + query)
const allParams = [
...endpoint.path_parameters.map((p) => ({ ...p, location: 'path' })),
...endpoint.query_parameters.map((p) => ({ ...p, location: 'query' })),
];
if (allParams.length > 0) {
lines.push('#### Parameters');
lines.push('');
lines.push('| Name | Type | Required | Description |');
lines.push('| ---- | ---- | -------- | ----------- |');
for (const param of allParams) {
const required = param.required ? 'Yes' : 'No';
lines.push(`| ${param.name} | ${param.type} | ${required} | ${param.description} |`);
}
lines.push('');
}
// Request body
if (endpoint.request_body) {
lines.push('#### Request Body');
lines.push('');
lines.push('| Name | Type | Required | Description |');
lines.push('| ---- | ---- | -------- | ----------- |');
for (const field of endpoint.request_body.fields) {
const required = field.required ? 'Yes' : 'No';
const defaultStr = field.default_value ? ` (default: ${field.default_value})` : '';
lines.push(
`| ${field.name} | ${field.type} | ${required} | ${field.description}${defaultStr} |`
);
}
lines.push('');
}
// Example request
lines.push('#### Example Request');
lines.push('');
lines.push("<Tabs groupId=\"language\" persist items={['Curl', 'Python', 'TypeScript']}>");
// Generate curl example
lines.push(' <Tab value="Curl">');
lines.push('```bash');
lines.push(generateCurlExample(endpoint));
lines.push('```');
lines.push(' </Tab>');
// Generate Python example
lines.push(' <Tab value="Python">');
lines.push('```python');
lines.push(generatePythonExample(endpoint));
lines.push('```');
lines.push(' </Tab>');
// Generate TypeScript example
lines.push(' <Tab value="TypeScript">');
lines.push('```typescript');
lines.push(generateTypeScriptExample(endpoint));
lines.push('```');
lines.push(' </Tab>');
lines.push('</Tabs>');
lines.push('');
// Status codes
lines.push('#### Response');
lines.push('');
for (const status of endpoint.status_codes) {
lines.push(`- **${status.code}**: ${status.description}`);
}
lines.push('');
lines.push('---');
lines.push('');
return lines;
}
function generateCurlExample(endpoint: APIEndpointDoc): string {
const path = endpoint.path.replace(/:(\w+)/g, (_, name) => getExamplePathValue(name));
const url = `http://localhost:7777${path}`;
if (endpoint.method === 'GET' || endpoint.method === 'DELETE') {
if (endpoint.method === 'DELETE') {
return `curl -X DELETE "${url}"`;
}
return `curl "${url}"`;
}
// POST/PATCH with body
if (endpoint.request_body && endpoint.request_body.fields.length > 0) {
const bodyObj: Record<string, unknown> = {};
for (const field of endpoint.request_body.fields) {
if (field.required) {
bodyObj[field.name] = getExampleValue(field);
}
}
const bodyJson = JSON.stringify(bodyObj, null, 2);
return `curl -X ${endpoint.method} "http://localhost:7777${path}" \\
-H "Content-Type: application/json" \\
-d '${bodyJson}'`;
}
return `curl -X ${endpoint.method} "${url}"`;
}
function generatePythonExample(endpoint: APIEndpointDoc): string {
const path = endpoint.path.replace(/:(\w+)/g, (_, name) => getExamplePathValue(name));
const method = endpoint.method.toLowerCase();
const lines: string[] = [];
lines.push('import requests');
lines.push('');
if (
endpoint.request_body &&
endpoint.request_body.fields.length > 0 &&
(endpoint.method === 'POST' || endpoint.method === 'PATCH')
) {
lines.push('data = {');
for (const field of endpoint.request_body.fields) {
if (field.required) {
const value = getExampleValuePython(field);
lines.push(` "${field.name}": ${value},`);
}
}
lines.push('}');
lines.push('');
lines.push(`response = requests.${method}("http://localhost:7777${path}", json=data)`);
} else {
lines.push(`response = requests.${method}("http://localhost:7777${path}")`);
}
lines.push('print(response.json())');
return lines.join('\n');
}
function generateTypeScriptExample(endpoint: APIEndpointDoc): string {
const path = endpoint.path.replace(/:(\w+)/g, (_, name) => getExamplePathValue(name));
const lines: string[] = [];
if (
endpoint.request_body &&
endpoint.request_body.fields.length > 0 &&
(endpoint.method === 'POST' || endpoint.method === 'PATCH')
) {
lines.push(`const response = await fetch(\`http://localhost:7777${path}\`, {`);
lines.push(` method: "${endpoint.method}",`);
lines.push(' headers: { "Content-Type": "application/json" },');
lines.push(' body: JSON.stringify({');
for (const field of endpoint.request_body.fields) {
if (field.required) {
const value = getExampleValueTS(field);
lines.push(` ${field.name}: ${value},`);
}
}
lines.push(' }),');
lines.push('});');
} else if (endpoint.method === 'DELETE') {
lines.push(`const response = await fetch(\`http://localhost:7777${path}\`, {`);
lines.push(` method: "DELETE",`);
lines.push('});');
} else {
lines.push(`const response = await fetch(\`http://localhost:7777${path}\`);`);
}
lines.push('const data = await response.json();');
return lines.join('\n');
}
function getExampleValue(field: APIFieldDoc): unknown {
switch (field.type) {
case 'string':
if (field.name === 'name') return 'my-vm';
if (field.name === 'os') return 'macOS';
if (field.name === 'memory') return '8GB';
if (field.name === 'diskSize') return '50GB';
if (field.name === 'display') return '1024x768';
if (field.name === 'image') return 'macos-tahoe-vanilla:latest';
if (field.name === 'path') return '/path/to/storage';
return 'example';
case 'integer':
if (field.name === 'cpu') return 4;
return 1;
case 'boolean':
return false;
case 'array':
if (field.name === 'tags') return ['latest'];
return [];
default:
return 'value';
}
}
function getExampleValuePython(field: APIFieldDoc): string {
const val = getExampleValue(field);
if (typeof val === 'string') return `"${val}"`;
if (typeof val === 'boolean') return val ? 'True' : 'False';
if (Array.isArray(val)) return JSON.stringify(val);
return String(val);
}
function getExampleValueTS(field: APIFieldDoc): string {
const val = getExampleValue(field);
if (typeof val === 'string') return `"${val}"`;
if (Array.isArray(val)) return JSON.stringify(val);
return String(val);
}
function getExamplePathValue(name: string): string {
if (name === 'name') return 'my-vm';
if (name === 'id') return 'example-id';
return `example-${name}`;
}
// ============================================================================
// Run
// ============================================================================
main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});
+932
View File
@@ -0,0 +1,932 @@
#!/usr/bin/env npx tsx
/**
* Python SDK Documentation Generator
*
* Generates MDX API reference documentation from Python source code docstrings.
* Uses griffe to extract documentation without importing packages.
*
* Usage:
* npx tsx scripts/docs-generators/python-sdk.ts # Generate all
* npx tsx scripts/docs-generators/python-sdk.ts --sdk=computer # Generate specific SDK
* npx tsx scripts/docs-generators/python-sdk.ts --check # Check for drift (CI mode)
*/
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
// ============================================================================
// Types
// ============================================================================
interface PythonPackage {
name: string;
version: string;
docstring: string;
exports: string[] | null;
classes: ClassDoc[];
functions: FunctionDoc[];
submodules: ModuleDoc[];
error?: string;
}
interface ModuleDoc {
name: string;
version: string;
docstring: string;
exports: string[] | null;
classes: ClassDoc[];
functions: FunctionDoc[];
}
interface ClassDoc {
name: string;
description: string;
bases: string[];
methods: FunctionDoc[];
attributes: AttributeDoc[];
is_private: boolean;
}
interface FunctionDoc {
name: string;
signature: string;
is_async: boolean;
is_method: boolean;
description: string;
parameters: ParameterDoc[];
returns: ReturnDoc | null;
raises: RaiseDoc[];
examples: string[];
is_private: boolean;
is_dunder: boolean;
}
interface ParameterDoc {
name: string;
type: string;
description: string;
default: string | null;
}
interface ReturnDoc {
type: string;
description: string;
}
interface RaiseDoc {
type: string;
description: string;
}
interface AttributeDoc {
name: string;
type: string;
description: string;
default: string | null;
is_private: boolean;
}
interface SDKConfig {
packageDir: string;
packageName: string;
outputPath: string;
displayName: string;
description: string;
outputDir: string; // For version discovery (relative to docsBaseDir)
tagPrefix: string; // Git tag prefix for version discovery
/** Absolute docs dir for this SDK (defaults to docs/content/docs/cua/reference) */
docsBaseDir?: string;
/** URL base path for hrefs (defaults to /cua/reference) */
hrefBase?: string;
/** Submodules to include in docs (if set, only these are included; if unset, all are included) */
includeSubmodules?: string[];
/** Override the page title (defaults to "${displayName} API Reference") */
pageTitle?: string;
}
// ============================================================================
// Configuration
// ============================================================================
const ROOT_DIR = path.resolve(__dirname, '../..');
const PYTHON_SCRIPT = path.join(__dirname, 'extract_python_docs.py');
const SDK_CONFIGS: Record<string, SDKConfig> = {
computer: {
packageDir: 'libs/python/computer/computer',
packageName: 'computer',
outputPath: 'docs/content/docs/cua/reference/computer-sdk/index.mdx',
displayName: 'Computer SDK',
description: 'Python API reference for controlling virtual machines and computer interfaces',
outputDir: 'computer-sdk',
tagPrefix: 'computer-v',
includeSubmodules: ['interface', 'models', 'tracing', 'helpers', 'diorama_computer'],
},
agent: {
packageDir: 'libs/python/agent/agent',
packageName: 'agent',
outputPath: 'docs/content/docs/cua/reference/agent-sdk/index.mdx',
displayName: 'Agent SDK',
description: 'Python API reference for building computer-use agents',
outputDir: 'agent-sdk',
tagPrefix: 'agent-v',
includeSubmodules: ['callbacks', 'tools', 'types'],
},
cli: {
packageDir: 'libs/python/cua-cli/cua_cli',
packageName: 'cua_cli',
outputPath: 'docs/content/docs/cua/reference/cli/index.mdx',
displayName: 'Cua CLI',
description: 'Python API reference for the Cua command-line interface',
outputDir: 'cli',
tagPrefix: 'cli-v',
},
sandbox: {
packageDir: 'libs/python/cua-sandbox/cua_sandbox',
packageName: 'cua_sandbox',
outputPath: 'docs/content/docs/cua/reference/sandbox-sdk/index.mdx',
displayName: 'Sandbox SDK',
description: 'Python API reference for cua-sandbox — creating and controlling sandboxes',
outputDir: 'sandbox-sdk',
tagPrefix: 'sandbox-v',
includeSubmodules: ['sandbox', 'image', 'localhost', 'interfaces', 'builder'],
},
bench: {
packageDir: 'libs/cua-bench/cua_bench',
packageName: 'cua_bench',
outputPath: 'docs/content/docs/cuabench/reference/api.mdx',
displayName: 'Cua Bench',
description: 'Python API reference for the desktop automation benchmarking framework',
outputDir: 'reference',
tagPrefix: 'bench-v',
docsBaseDir: 'docs/content/docs/cuabench',
hrefBase: '/cuabench',
pageTitle: 'API Reference',
},
};
// ============================================================================
// Main
// ============================================================================
async function main() {
const args = process.argv.slice(2);
const checkOnly = args.includes('--check') || args.includes('--check-only');
const sdkArg = args.find((a) => a.startsWith('--sdk='));
const targetSdk = sdkArg?.split('=')[1];
console.log('🐍 Python SDK Documentation Generator');
console.log('=====================================\n');
// Check if Python script exists
if (!fs.existsSync(PYTHON_SCRIPT)) {
console.error(`❌ Python extraction script not found: ${PYTHON_SCRIPT}`);
process.exit(1);
}
let hasErrors = false;
for (const [sdkName, config] of Object.entries(SDK_CONFIGS)) {
// Skip if targeting specific SDK
if (targetSdk && targetSdk !== sdkName) {
continue;
}
console.log(`📖 Processing ${config.displayName}...`);
// Check if package exists
const packagePath = path.join(ROOT_DIR, config.packageDir);
if (!fs.existsSync(packagePath)) {
console.error(` ❌ Package not found: ${config.packageDir}`);
hasErrors = true;
continue;
}
// Extract documentation using Python script
console.log(` Extracting documentation from ${config.packageDir}...`);
let docs: PythonPackage;
try {
// Prefer uv run --with griffe python (works cross-platform), fall back to python3
const pythonCmd = process.platform === 'win32' ? `uv run --with griffe python` : `python3`;
const output = execSync(
`${pythonCmd} "${PYTHON_SCRIPT}" "${packagePath}" "${config.packageName}"`,
{
encoding: 'utf-8',
cwd: ROOT_DIR,
timeout: 60000,
}
);
docs = JSON.parse(output);
} catch (error) {
console.error(` ❌ Failed to extract documentation: ${error}`);
hasErrors = true;
continue;
}
if (docs.error) {
console.error(` ❌ Extraction error: ${docs.error}`);
hasErrors = true;
continue;
}
console.log(` Found ${docs.classes.length} classes, ${docs.functions.length} functions`);
// Generate MDX
console.log(` Generating MDX...`);
const mdx = generateMDX(docs, config);
// Ensure output directory exists
const outputPath = path.join(ROOT_DIR, config.outputPath);
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
if (checkOnly) {
// Check mode: compare with existing file
if (fs.existsSync(outputPath)) {
const existing = fs.readFileSync(outputPath, 'utf-8');
if (existing !== mdx) {
console.error(`${path.basename(outputPath)} is out of sync with source code`);
hasErrors = true;
} else {
console.log(`${path.basename(outputPath)} is up to date`);
}
} else {
console.error(`${path.basename(outputPath)} does not exist`);
hasErrors = true;
}
} else {
// Generate mode: write file
fs.writeFileSync(outputPath, mdx);
console.log(` ✅ Generated ${path.relative(ROOT_DIR, outputPath)}`);
}
}
if (hasErrors) {
if (checkOnly) {
console.error(
"\n💡 Run 'npx tsx scripts/docs-generators/python-sdk.ts' to update documentation"
);
}
process.exit(1);
}
console.log('\n✅ Python SDK documentation generation complete!');
}
// ============================================================================
// Version Discovery
// ============================================================================
interface VersionInfo {
version: string;
href: string;
isCurrent: boolean;
}
/**
* Get the latest released version from git tags.
* Falls back to the version from source if no tags found.
*/
function getLatestReleasedVersion(config: SDKConfig, fallbackVersion: string): string {
try {
const output = execSync(`git tag | grep "^${config.tagPrefix}" | sort -V | tail -1`, {
encoding: 'utf-8',
cwd: ROOT_DIR,
}).trim();
if (output) {
return output.replace(config.tagPrefix, '');
}
} catch {
// Fall through to fallback
}
return fallbackVersion;
}
function discoverVersions(config: SDKConfig, currentVersion: string): VersionInfo[] {
const baseDir = config.docsBaseDir
? path.join(ROOT_DIR, config.docsBaseDir)
: path.join(ROOT_DIR, 'docs/content/docs/cua/reference');
const docsDir = path.join(baseDir, config.outputDir);
const hrefBase = config.hrefBase ?? '/cua/reference';
const versions: VersionInfo[] = [];
// Add current version (latest) — points to the index page (folder root)
const currentMajorMinor = currentVersion.split('.').slice(0, 2).join('.');
versions.push({
version: currentMajorMinor,
href: `${hrefBase}/${config.outputDir}`,
isCurrent: true,
});
// Discover versioned folders (v0.5, v0.4, etc.)
if (fs.existsSync(docsDir)) {
const entries = fs.readdirSync(docsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('v')) {
const version = entry.name.substring(1); // Remove 'v' prefix
// Skip if this is the current version
if (version === currentMajorMinor) continue;
versions.push({
version,
href: `${hrefBase}/${config.outputDir}/${entry.name}/api`,
isCurrent: false,
});
}
}
}
// Sort versions descending
versions.sort((a, b) => {
const partsA = a.version.split('.').map((x) => parseInt(x, 10) || 0);
const partsB = b.version.split('.').map((x) => parseInt(x, 10) || 0);
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const partA = partsA[i] || 0;
const partB = partsB[i] || 0;
if (partA !== partB) return partB - partA;
}
return 0;
});
return versions;
}
// ============================================================================
// MDX Generation
// ============================================================================
function generateMDX(docs: PythonPackage, config: SDKConfig): string {
const lines: string[] = [];
// Get the actual latest released version from git tags
const releasedVersion = getLatestReleasedVersion(config, docs.version);
// Frontmatter
const pageTitle = config.pageTitle ?? `${config.displayName} API Reference`;
lines.push('---');
lines.push(`title: ${pageTitle}`);
lines.push(`description: ${config.description}`);
lines.push('---');
lines.push('');
// Auto-generated notice
lines.push(`{/*`);
lines.push(` AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY`);
lines.push(` Generated by: npx tsx scripts/docs-generators/python-sdk.ts`);
lines.push(` Source: ${config.packageDir}`);
lines.push(` Version: ${releasedVersion}`);
lines.push(`*/}`);
lines.push('');
// Imports
lines.push("import { Callout } from 'fumadocs-ui/components/callout';");
lines.push("import { Tabs, Tab } from 'fumadocs-ui/components/tabs';");
lines.push("import { VersionHeader } from '@/components/version-selector';");
lines.push('');
// Discover available versions using the released version
const versions = discoverVersions(config, releasedVersion);
const currentMajorMinor = releasedVersion.split('.').slice(0, 2).join('.');
// Version selector and badge
lines.push('<VersionHeader');
lines.push(` versions={${JSON.stringify(versions)}}`);
lines.push(` currentVersion="${currentMajorMinor}"`);
lines.push(` fullVersion="${releasedVersion}"`);
// Use pip-style package name (underscores → hyphens)
// If it already starts with 'cua', don't add prefix
const pipName = config.packageName.replace(/_/g, '-');
const fullPipName = pipName.startsWith('cua') ? pipName : `cua-${pipName}`;
lines.push(` packageName="${fullPipName}"`);
lines.push('/>');
lines.push('');
// Package description
if (docs.docstring) {
lines.push(docs.docstring);
lines.push('');
}
// Table of contents for classes
if (docs.classes.length > 0) {
lines.push('## Classes');
lines.push('');
lines.push('| Class | Description |');
lines.push('|-------|-------------|');
for (const cls of docs.classes) {
if (!cls.is_private) {
const desc = escapeMDX(cls.description.split('\n')[0]) || 'No description';
lines.push(`| [\`${cls.name}\`](#${cls.name.toLowerCase()}) | ${desc} |`);
}
}
lines.push('');
}
// Table of contents for functions
if (docs.functions.length > 0) {
lines.push('## Functions');
lines.push('');
lines.push('| Function | Description |');
lines.push('|----------|-------------|');
for (const fn of docs.functions) {
if (!fn.is_private) {
const desc = escapeMDX(fn.description.split('\n')[0]) || 'No description';
lines.push(`| [\`${fn.name}\`](#${fn.name.toLowerCase()}) | ${desc} |`);
}
}
lines.push('');
}
// Detailed class documentation
for (const cls of docs.classes) {
if (!cls.is_private) {
lines.push(...generateClassDoc(cls));
}
}
// Detailed function documentation
for (const fn of docs.functions) {
if (!fn.is_private) {
lines.push(...generateFunctionDoc(fn, '##'));
}
}
// Submodules (for packages that expose API through submodules)
if (docs.submodules && docs.submodules.length > 0) {
let publicSubmodules = docs.submodules.filter(
(m) => !m.name.startsWith('_') && (m.classes.length > 0 || m.functions.length > 0)
);
// Filter to only included submodules if configured
if (config.includeSubmodules) {
publicSubmodules = publicSubmodules.filter((m) => config.includeSubmodules!.includes(m.name));
}
for (const mod of publicSubmodules) {
const publicClasses = mod.classes.filter((c) => !c.is_private);
const publicFunctions = mod.functions.filter((f) => !f.is_private && !f.is_dunder);
if (publicClasses.length === 0 && publicFunctions.length === 0) continue;
lines.push('---');
lines.push('');
lines.push(`## ${mod.name}`);
lines.push('');
if (mod.docstring) {
lines.push(escapeMDX(mod.docstring));
lines.push('');
}
for (const cls of publicClasses) {
lines.push(...generateClassDoc(cls));
}
for (const fn of publicFunctions) {
lines.push(...generateFunctionDoc(fn, '###'));
}
}
}
return lines.join('\n');
}
function generateClassDoc(cls: ClassDoc): string[] {
const lines: string[] = [];
lines.push('---');
lines.push('');
lines.push(`## ${cls.name}`);
lines.push('');
// Base classes
if (cls.bases.length > 0) {
const bases = cls.bases.filter((b) => b !== 'object').join(', ');
if (bases) {
lines.push(`*Inherits from: ${bases}*`);
lines.push('');
}
}
// Description
if (cls.description) {
lines.push(escapeMDX(cls.description));
lines.push('');
}
// Constructor (__init__)
const initMethod = cls.methods.find((m) => m.name === '__init__');
if (initMethod) {
lines.push('### Constructor');
lines.push('');
lines.push('```python');
lines.push(formatSignature(initMethod.signature, cls.name));
lines.push('```');
lines.push('');
if (initMethod.parameters.length > 0) {
lines.push(...generateParametersTable(initMethod.parameters));
}
}
// Attributes
if (cls.attributes.length > 0) {
lines.push('### Attributes');
lines.push('');
lines.push('| Name | Type | Description |');
lines.push('|------|------|-------------|');
for (const attr of cls.attributes) {
const type = attr.type || 'Any';
// Strip docstring sections and collapse to single line for table cells
const desc = escapeMDX(stripDocstringSections(attr.description)) || '';
lines.push(`| \`${attr.name}\` | \`${type}\` | ${desc} |`);
}
lines.push('');
}
// Methods (excluding __init__ and private)
const publicMethods = cls.methods.filter(
(m) => !m.is_private && !m.is_dunder && m.name !== '__init__'
);
if (publicMethods.length > 0) {
lines.push('### Methods');
lines.push('');
for (const method of publicMethods) {
lines.push(...generateMethodDoc(method, cls.name));
}
}
return lines;
}
function generateMethodDoc(method: FunctionDoc, className: string): string[] {
const lines: string[] = [];
lines.push(`#### ${className}.${method.name}`);
lines.push('');
lines.push('```python');
lines.push(formatSignature(method.signature));
lines.push('```');
lines.push('');
// Use structured data flags to avoid duplicating info from docstring
const hasStructuredParams = method.parameters.filter((p) => p.name !== 'self').length > 0;
const hasStructuredReturns = !!method.returns;
const hasStructuredRaises = method.raises.length > 0;
if (method.description) {
lines.push(
...formatDocstringLines(
method.description,
hasStructuredParams,
hasStructuredReturns,
hasStructuredRaises
)
);
}
// Structured parameters (from parsed signature)
if (hasStructuredParams) {
const params = method.parameters.filter((p) => p.name !== 'self');
lines.push(...generateParametersTable(params));
}
// Structured returns
if (method.returns) {
lines.push('**Returns:**');
lines.push('');
const returnType = method.returns.type || 'None';
const returnDesc = escapeMDX(method.returns.description) || '';
lines.push(`- \`${returnType}\` - ${returnDesc}`);
lines.push('');
}
// Structured raises
if (method.raises.length > 0) {
lines.push('**Raises:**');
lines.push('');
for (const exc of method.raises) {
lines.push(`- \`${exc.type}\` - ${escapeMDX(exc.description)}`);
}
lines.push('');
}
return lines;
}
function generateFunctionDoc(fn: FunctionDoc, heading: string): string[] {
const lines: string[] = [];
lines.push(`${heading} ${fn.name}`);
lines.push('');
lines.push('```python');
lines.push(formatSignature(fn.signature));
lines.push('```');
lines.push('');
const hasStructuredParams = fn.parameters.length > 0;
const hasStructuredReturns = !!fn.returns;
const hasStructuredRaises = fn.raises.length > 0;
if (fn.description) {
lines.push(
...formatDocstringLines(
fn.description,
hasStructuredParams,
hasStructuredReturns,
hasStructuredRaises
)
);
}
// Structured parameters
if (hasStructuredParams) {
lines.push(...generateParametersTable(fn.parameters));
}
// Structured returns
if (fn.returns) {
lines.push('**Returns:**');
lines.push('');
const returnType = fn.returns.type || 'None';
const returnDesc = escapeMDX(fn.returns.description) || '';
lines.push(`- \`${returnType}\` - ${returnDesc}`);
lines.push('');
}
// Structured raises
if (fn.raises.length > 0) {
lines.push('**Raises:**');
lines.push('');
for (const exc of fn.raises) {
lines.push(`- \`${exc.type}\` - ${escapeMDX(exc.description)}`);
}
lines.push('');
}
// Structured examples (from parsed data)
if (fn.examples.length > 0) {
lines.push('**Example:**');
lines.push('');
lines.push('```python');
for (const example of fn.examples) {
lines.push(example);
}
lines.push('```');
lines.push('');
}
return lines;
}
function generateParametersTable(params: ParameterDoc[]): string[] {
const lines: string[] = [];
lines.push('**Parameters:**');
lines.push('');
lines.push('| Name | Type | Description |');
lines.push('|------|------|-------------|');
for (const param of params) {
const type = param.type || 'Any';
const desc = escapeMDX(param.description) || '';
const defaultVal = param.default ? ` (default: \`${param.default}\`)` : '';
lines.push(`| \`${param.name}\` | \`${type}\` | ${desc}${defaultVal} |`);
}
lines.push('');
return lines;
}
function formatSignature(signature: string, className?: string): string {
// Replace __init__ with class name for constructors
if (className && signature.includes('__init__')) {
return signature.replace('def __init__', className);
}
return signature;
}
/**
* Escape special MDX characters in text content.
* Curly braces and HTML-like tags must be escaped outside of code blocks.
*/
function escapeMDX(text: string): string {
if (!text) return text;
return (
text
.replace(/\{/g, '\\{')
.replace(/\}/g, '\\}')
// Escape HTML-like tags that would be interpreted as JSX components
// but preserve markdown links []() and code backticks
.replace(/<(?!\/?(?:Callout|Tab|Tabs|VersionHeader|div|span|a|code|pre|br|hr)\b)/g, '&lt;')
);
}
/**
* Parse Google-style docstring sections (Args, Returns, Raises, Examples)
* and return the description text with those sections stripped,
* plus the parsed sections as structured data.
*/
interface ParsedDocstring {
description: string;
args: { name: string; type: string; description: string }[];
returns: string;
raises: { type: string; description: string }[];
examples: string[];
}
function parseDocstring(text: string): ParsedDocstring {
if (!text) return { description: '', args: [], returns: '', raises: [], examples: [] };
const lines = text.split('\n');
const result: ParsedDocstring = {
description: '',
args: [],
returns: '',
raises: [],
examples: [],
};
type Section = 'description' | 'args' | 'returns' | 'raises' | 'examples';
let currentSection: Section = 'description';
const descLines: string[] = [];
const returnsLines: string[] = [];
const exampleLines: string[] = [];
let currentArg: { name: string; type: string; description: string } | null = null;
let currentRaise: { type: string; description: string } | null = null;
for (const line of lines) {
const trimmed = line.trim();
// Detect section headers
if (/^Args?\s*:/.test(trimmed) || /^Parameters?\s*:/.test(trimmed)) {
currentSection = 'args';
continue;
}
if (/^Returns?\s*:/.test(trimmed)) {
// Check if it's a one-liner like "Returns: something"
const inlineReturn = trimmed.replace(/^Returns?\s*:\s*/, '');
if (inlineReturn) {
returnsLines.push(inlineReturn);
}
currentSection = 'returns';
continue;
}
if (/^Raises?\s*:/.test(trimmed)) {
currentSection = 'raises';
continue;
}
if (/^Examples?\s*:/.test(trimmed)) {
currentSection = 'examples';
continue;
}
switch (currentSection) {
case 'description':
descLines.push(line);
break;
case 'args': {
// Match "param_name (type): description" or "param_name: description"
const argMatch = trimmed.match(/^(\w+)\s*(?:\(([^)]+)\))?\s*:\s*(.*)$/);
if (argMatch && !line.startsWith(' ')) {
if (currentArg) result.args.push(currentArg);
currentArg = {
name: argMatch[1],
type: argMatch[2] || '',
description: argMatch[3],
};
} else if (currentArg && trimmed) {
// Continuation line for current arg
currentArg.description += ' ' + trimmed;
}
break;
}
case 'returns':
if (trimmed) returnsLines.push(trimmed);
break;
case 'raises': {
const raiseMatch = trimmed.match(/^(\w+)\s*:\s*(.*)$/);
if (raiseMatch && !line.startsWith(' ')) {
if (currentRaise) result.raises.push(currentRaise);
currentRaise = { type: raiseMatch[1], description: raiseMatch[2] };
} else if (currentRaise && trimmed) {
currentRaise.description += ' ' + trimmed;
}
break;
}
case 'examples':
exampleLines.push(line);
break;
}
}
// Flush remaining items
if (currentArg) result.args.push(currentArg);
if (currentRaise) result.raises.push(currentRaise);
result.description = descLines.join('\n').trim();
result.returns = returnsLines.join(' ').trim();
result.examples = exampleLines;
return result;
}
/**
* Format a parsed docstring into markdown lines.
* Only outputs sections not already covered by structured data.
*/
function formatDocstringLines(
text: string,
hasStructuredParams: boolean,
hasStructuredReturns: boolean,
hasStructuredRaises: boolean
): string[] {
const parsed = parseDocstring(text);
const lines: string[] = [];
// Description (always output)
if (parsed.description) {
lines.push(escapeMDX(parsed.description));
lines.push('');
}
// Args (only if no structured params)
if (!hasStructuredParams && parsed.args.length > 0) {
lines.push('**Parameters:**');
lines.push('');
lines.push('| Name | Type | Description |');
lines.push('|------|------|-------------|');
for (const arg of parsed.args) {
const type = arg.type || 'Any';
lines.push(`| \`${arg.name}\` | \`${escapeMDX(type)}\` | ${escapeMDX(arg.description)} |`);
}
lines.push('');
}
// Returns (only if no structured returns)
if (!hasStructuredReturns && parsed.returns) {
lines.push(`**Returns:** ${escapeMDX(parsed.returns)}`);
lines.push('');
}
// Raises (only if no structured raises)
if (!hasStructuredRaises && parsed.raises.length > 0) {
lines.push('**Raises:**');
lines.push('');
for (const r of parsed.raises) {
lines.push(`- \`${r.type}\` - ${escapeMDX(r.description)}`);
}
lines.push('');
}
// Examples
if (parsed.examples.length > 0) {
// Dedent example lines by removing common leading whitespace
const nonEmptyLines = parsed.examples.filter((l) => l.trim().length > 0);
if (nonEmptyLines.length > 0) {
const minIndent = Math.min(...nonEmptyLines.map((l) => l.match(/^(\s*)/)?.[1].length ?? 0));
const dedented = parsed.examples
.map((l) => (l.trim().length > 0 ? l.substring(minIndent) : ''))
.join('\n')
.trim();
if (dedented) {
lines.push('**Example:**');
lines.push('');
lines.push('```python');
lines.push(dedented);
lines.push('```');
lines.push('');
}
}
}
return lines;
}
/**
* Strip docstring sections from text for use in single-line contexts (e.g. table cells).
*/
function stripDocstringSections(text: string): string {
if (!text) return text;
const parsed = parseDocstring(text);
// Return only the first line of the description
return parsed.description.split('\n')[0].trim();
}
// ============================================================================
// Run
// ============================================================================
main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});
+2
View File
@@ -0,0 +1,2 @@
# Python dependencies for documentation generation
griffe>=0.40.0
+268
View File
@@ -0,0 +1,268 @@
#!/usr/bin/env npx tsx
/**
* Documentation Generator Runner
*
* Orchestrates documentation generation across all configured libraries.
* Reads config.json and runs appropriate generators based on what changed.
*
* Usage:
* npx tsx scripts/docs-generators/runner.ts # Generate all enabled docs
* npx tsx scripts/docs-generators/runner.ts --check # Check for drift (CI mode)
* npx tsx scripts/docs-generators/runner.ts --library lume # Generate specific library
* npx tsx scripts/docs-generators/runner.ts --list # List all configured generators
* npx tsx scripts/docs-generators/runner.ts --changed # Only run for changed files (CI)
*/
import { execSync, spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
// ============================================================================
// Types
// ============================================================================
interface GeneratorOutput {
type: string;
outputFile: string;
extractCommand: string | null;
}
interface GeneratorConfig {
name: string;
language: string;
sourcePath: string;
docsOutputPath: string;
generatorScript: string;
watchPaths: string[];
buildCommand: string | null;
buildDirectory: string;
extractionMethod: string;
outputs: GeneratorOutput[];
enabled: boolean;
notes?: string;
}
interface Config {
description: string;
generators: Record<string, GeneratorConfig>;
}
// ============================================================================
// Configuration
// ============================================================================
const ROOT_DIR = path.resolve(__dirname, '../..');
const CONFIG_PATH = path.join(__dirname, 'config.json');
// ============================================================================
// Main
// ============================================================================
async function main() {
const args = process.argv.slice(2);
// Parse arguments
const checkOnly = args.includes('--check') || args.includes('--check-only');
const listOnly = args.includes('--list');
const changedOnly = args.includes('--changed');
const libraryIndex = args.indexOf('--library');
const specificLibrary = libraryIndex !== -1 ? args[libraryIndex + 1] : null;
console.log('📚 Documentation Generator Runner');
console.log('==================================\n');
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(`❌ Config file not found: ${CONFIG_PATH}`);
process.exit(1);
}
const config: Config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
// List mode
if (listOnly) {
listGenerators(config);
return;
}
// Determine which generators to run
let generatorsToRun: string[] = [];
if (specificLibrary) {
if (!config.generators[specificLibrary]) {
console.error(`❌ Unknown library: ${specificLibrary}`);
console.log('\nAvailable libraries:', Object.keys(config.generators).join(', '));
process.exit(1);
}
generatorsToRun = [specificLibrary];
} else if (changedOnly) {
generatorsToRun = getChangedGenerators(config);
if (generatorsToRun.length === 0) {
console.log('✅ No documentation-related changes detected.');
return;
}
} else {
// Run all enabled generators
generatorsToRun = Object.entries(config.generators)
.filter(([_, cfg]) => cfg.enabled)
.map(([key, _]) => key);
}
if (generatorsToRun.length === 0) {
console.log('⚠️ No generators to run. Enable generators in config.json.');
return;
}
console.log(`📋 Generators to run: ${generatorsToRun.join(', ')}\n`);
// Run generators
let hasErrors = false;
for (const generatorKey of generatorsToRun) {
const generator = config.generators[generatorKey];
console.log(`\n${'='.repeat(50)}`);
console.log(`📦 ${generator.name} (${generatorKey})`);
console.log(`${'='.repeat(50)}\n`);
if (!generator.enabled) {
console.log(`⏭️ Skipped (disabled)`);
if (generator.notes) {
console.log(` Note: ${generator.notes}`);
}
continue;
}
const success = await runGenerator(generator, generatorKey, checkOnly);
if (!success) {
hasErrors = true;
}
}
// Summary
console.log(`\n${'='.repeat(50)}`);
if (hasErrors) {
console.error('❌ Some generators failed or detected drift.');
if (checkOnly) {
console.log("\n💡 Run 'npx tsx scripts/docs-generators/runner.ts' to update documentation");
}
process.exit(1);
} else {
console.log('✅ All documentation is up to date!');
}
}
// ============================================================================
// Generator Execution
// ============================================================================
async function runGenerator(
generator: GeneratorConfig,
key: string,
checkOnly: boolean
): Promise<boolean> {
const generatorPath = path.join(ROOT_DIR, generator.generatorScript);
// Check if generator script exists
if (!fs.existsSync(generatorPath)) {
console.log(`⚠️ Generator script not found: ${generator.generatorScript}`);
console.log(` Create this file to enable documentation generation.`);
return true; // Not an error, just not implemented
}
try {
// Run the generator
const args = checkOnly ? ['--check'] : [];
const result = spawnSync('npx', ['tsx', generatorPath, ...args], {
cwd: ROOT_DIR,
stdio: 'inherit',
encoding: 'utf-8',
});
return result.status === 0;
} catch (error) {
console.error(`❌ Error running generator for ${key}:`, error);
return false;
}
}
// ============================================================================
// Changed Files Detection (for CI)
// ============================================================================
function getChangedGenerators(config: Config): string[] {
const changedGenerators: string[] = [];
try {
// Get changed files from git
// This works for both PRs (comparing to base) and pushes
const gitDiff = execSync(
'git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only HEAD',
{ cwd: ROOT_DIR, encoding: 'utf-8' }
);
const changedFiles = gitDiff.split('\n').filter(Boolean);
console.log(`📝 Changed files: ${changedFiles.length}`);
for (const [key, generator] of Object.entries(config.generators)) {
if (!generator.enabled) continue;
// Check if any watch path matches changed files
const watchPatterns = generator.watchPaths.map(
(p) => new RegExp(p.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*').replace(/\//g, '\\/'))
);
const hasChanges = changedFiles.some((file) =>
watchPatterns.some((pattern) => pattern.test(file))
);
if (hasChanges) {
changedGenerators.push(key);
console.log(` 📌 ${key}: changes detected`);
}
}
} catch (error) {
console.warn('⚠️ Could not detect changed files, running all generators');
return Object.entries(config.generators)
.filter(([_, cfg]) => cfg.enabled)
.map(([key, _]) => key);
}
return changedGenerators;
}
// ============================================================================
// List Generators
// ============================================================================
function listGenerators(config: Config): void {
console.log('Configured Documentation Generators:\n');
const maxKeyLen = Math.max(...Object.keys(config.generators).map((k) => k.length));
for (const [key, generator] of Object.entries(config.generators)) {
const status = generator.enabled ? '✅' : '⏸️ ';
const paddedKey = key.padEnd(maxKeyLen);
console.log(`${status} ${paddedKey} ${generator.name} (${generator.language})`);
if (!generator.enabled && generator.notes) {
console.log(` ${''.padEnd(maxKeyLen)} └─ ${generator.notes}`);
}
}
console.log('\n');
console.log('Legend:');
console.log(' ✅ = Enabled (will run)');
console.log(' ⏸️ = Disabled (not yet implemented)');
}
// ============================================================================
// Run
// ============================================================================
main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});
+772
View File
@@ -0,0 +1,772 @@
#!/usr/bin/env npx tsx
/**
* TypeScript SDK Documentation Generator
*
* Generates MDX API reference documentation from TypeScript source code.
* Uses regex-based parsing to extract exports and JSDoc comments (no TS compiler dependency).
*
* Usage:
* npx tsx scripts/docs-generators/typescript-sdk.ts # Generate all
* npx tsx scripts/docs-generators/typescript-sdk.ts --sdk=cuabot # Generate specific
* npx tsx scripts/docs-generators/typescript-sdk.ts --check # Check for drift (CI mode)
*/
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
// ============================================================================
// Types
// ============================================================================
interface ExtractedClass {
name: string;
description: string;
constructorSig: string | null;
constructorParams: ParamInfo[];
methods: MethodInfo[];
properties: PropInfo[];
}
interface ExtractedInterface {
name: string;
description: string;
properties: PropInfo[];
}
interface ExtractedFunction {
name: string;
description: string;
signature: string;
params: ParamInfo[];
returnType: string;
isAsync: boolean;
}
interface ExtractedConst {
name: string;
type: string;
description: string;
}
interface MethodInfo {
name: string;
description: string;
signature: string;
params: ParamInfo[];
returnType: string;
isAsync: boolean;
}
interface ParamInfo {
name: string;
type: string;
description: string;
defaultValue: string | null;
isOptional: boolean;
}
interface PropInfo {
name: string;
type: string;
description: string;
isOptional: boolean;
}
interface ModuleDoc {
name: string;
description: string;
classes: ExtractedClass[];
interfaces: ExtractedInterface[];
functions: ExtractedFunction[];
constants: ExtractedConst[];
}
interface SDKConfig {
packageDir: string;
packageName: string;
outputPath: string;
displayName: string;
description: string;
outputDir: string;
tagPrefix: string;
docsBaseDir?: string;
hrefBase?: string;
pageTitle?: string;
includeFiles?: string[];
installCommand?: string;
}
// ============================================================================
// Configuration
// ============================================================================
const ROOT_DIR = path.resolve(__dirname, '../..');
const SDK_CONFIGS: Record<string, SDKConfig> = {
cuabot: {
packageDir: 'libs/cuabot/src',
packageName: 'cuabot',
outputPath: 'docs/content/docs/cuabot/reference/index.mdx',
displayName: 'Cua-Bot',
description: 'TypeScript API reference for the Cua-Bot sandboxed agent framework',
outputDir: 'reference',
tagPrefix: 'cuabot-v',
docsBaseDir: 'docs/content/docs/cuabot',
hrefBase: '/cuabot',
pageTitle: 'API Reference',
includeFiles: ['client.ts', 'settings.ts'],
installCommand: 'npm install -g cuabot',
},
};
// ============================================================================
// Main
// ============================================================================
async function main() {
const args = process.argv.slice(2);
const checkOnly = args.includes('--check') || args.includes('--check-only');
const sdkArg = args.find((a) => a.startsWith('--sdk='));
const targetSdk = sdkArg?.split('=')[1];
console.log('📦 TypeScript SDK Documentation Generator');
console.log('==========================================\n');
let hasErrors = false;
for (const [sdkName, config] of Object.entries(SDK_CONFIGS)) {
if (targetSdk && targetSdk !== sdkName) continue;
console.log(`📖 Processing ${config.displayName}...`);
const packagePath = path.join(ROOT_DIR, config.packageDir);
if (!fs.existsSync(packagePath)) {
console.error(` ❌ Package not found: ${config.packageDir}`);
hasErrors = true;
continue;
}
const modules = extractDocs(packagePath, config);
console.log(
` Found ${modules.reduce((n, m) => n + m.classes.length, 0)} classes, ` +
`${modules.reduce((n, m) => n + m.functions.length, 0)} functions, ` +
`${modules.reduce((n, m) => n + m.interfaces.length, 0)} interfaces`
);
const mdx = generateMDX(modules, config);
const outputPath = path.join(ROOT_DIR, config.outputPath);
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
if (checkOnly) {
// Check mode: compare with existing file
if (fs.existsSync(outputPath)) {
const existing = fs.readFileSync(outputPath, 'utf-8');
if (existing !== mdx) {
console.error(`${path.basename(outputPath)} is out of sync with source code`);
hasErrors = true;
} else {
console.log(`${path.basename(outputPath)} is up to date`);
}
} else {
console.error(`${path.basename(outputPath)} does not exist (needs generation)`);
hasErrors = true;
}
} else {
// Generate mode: write file
fs.writeFileSync(outputPath, mdx);
console.log(` ✅ Generated ${path.relative(ROOT_DIR, outputPath)}`);
}
}
if (hasErrors) {
if (checkOnly) {
console.error(
"\n💡 Run 'npx tsx scripts/docs-generators/typescript-sdk.ts' to update documentation"
);
}
process.exit(1);
}
console.log('\n✅ TypeScript SDK documentation generation complete!');
}
// ============================================================================
// Regex-based Extraction
// ============================================================================
function extractDocs(packagePath: string, config: SDKConfig): ModuleDoc[] {
const fileNames = config.includeFiles
? config.includeFiles
: fs.readdirSync(packagePath).filter((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'));
const modules: ModuleDoc[] = [];
for (const fileName of fileNames) {
const filePath = path.join(packagePath, fileName);
if (!fs.existsSync(filePath)) continue;
const source = fs.readFileSync(filePath, 'utf-8');
const moduleName = path.basename(fileName, '.ts');
const mod: ModuleDoc = {
name: moduleName,
description: extractFileDescription(source),
classes: extractClasses(source),
interfaces: extractInterfaces(source),
functions: extractFunctions(source),
constants: extractConstants(source),
};
if (
mod.classes.length > 0 ||
mod.interfaces.length > 0 ||
mod.functions.length > 0 ||
mod.constants.length > 0
) {
modules.push(mod);
}
}
return modules;
}
function extractFileDescription(source: string): string {
const match = source.match(/^\/\*\*\s*\n([\s\S]*?)\*\//);
if (!match) return '';
return match[1]
.split('\n')
.map((l) => l.replace(/^\s*\*\s?/, '').trim())
.filter(Boolean)
.join('\n');
}
/**
* Get the JSDoc comment immediately preceding a position in source.
*/
function getJSDocBefore(source: string, pos: number): string {
const before = source.substring(0, pos);
const match = before.match(/\/\*\*([\s\S]*?)\*\/\s*$/);
if (!match) return '';
return match[1]
.split('\n')
.map((l) => l.replace(/^\s*\*\s?/, ''))
.filter((l) => !l.startsWith('@'))
.join('\n')
.trim();
}
function extractClasses(source: string): ExtractedClass[] {
const classes: ExtractedClass[] = [];
const classRegex = /export\s+class\s+(\w+)(?:\s+extends\s+[\w.]+)?\s*\{/g;
let match;
while ((match = classRegex.exec(source)) !== null) {
const name = match[1];
const description = getJSDocBefore(source, match.index);
const classBodyStart = match.index + match[0].length;
const classBody = extractBraceBlock(source, classBodyStart - 1);
const cls: ExtractedClass = {
name,
description,
constructorSig: null,
constructorParams: [],
methods: [],
properties: [],
};
// Extract constructor
const ctorMatch = classBody.match(/constructor\s*\(([\s\S]*?)\)\s*\{/);
if (ctorMatch) {
cls.constructorParams = parseParams(ctorMatch[1]);
cls.constructorSig = `constructor(${ctorMatch[1].trim()})`;
}
// Extract methods (async or not, excluding private)
const methodRegex =
/(\/\*\*[\s\S]*?\*\/\s*)?(async\s+)?(\w+)\s*\(([\s\S]*?)\)\s*:\s*([\w<>\[\]|, ]+)\s*\{/g;
let mMatch;
while ((mMatch = methodRegex.exec(classBody)) !== null) {
const methodName = mMatch[3];
if (methodName === 'constructor' || methodName.startsWith('_') || methodName === 'private')
continue;
const isAsync = !!mMatch[2];
const params = parseParams(mMatch[4]);
const returnType = mMatch[5].trim();
const jsdoc = mMatch[1] ? parseJSDocBlock(mMatch[1]) : '';
// Get param descriptions from JSDoc
if (mMatch[1]) {
const paramDescs = parseJSDocParams(mMatch[1]);
for (const p of params) {
if (paramDescs[p.name]) p.description = paramDescs[p.name];
}
}
cls.methods.push({
name: methodName,
description: jsdoc,
signature: `${isAsync ? 'async ' : ''}${methodName}(${params.map((p) => formatParam(p)).join(', ')}): ${returnType}`,
params: params.filter((p) => p.name !== 'this'),
returnType,
isAsync,
});
}
classes.push(cls);
}
return classes;
}
function extractInterfaces(source: string): ExtractedInterface[] {
const interfaces: ExtractedInterface[] = [];
const ifaceRegex = /export\s+interface\s+(\w+)\s*\{/g;
let match;
while ((match = ifaceRegex.exec(source)) !== null) {
const name = match[1];
const description = getJSDocBefore(source, match.index);
const bodyStart = match.index + match[0].length;
const body = extractBraceBlock(source, bodyStart - 1);
const properties: PropInfo[] = [];
const propRegex = /(\w+)(\?)?\s*:\s*([^;\n]+)/g;
let pMatch;
while ((pMatch = propRegex.exec(body)) !== null) {
properties.push({
name: pMatch[1],
type: pMatch[3].trim().replace(/;$/, ''),
description: '',
isOptional: !!pMatch[2],
});
}
interfaces.push({ name, description, properties });
}
return interfaces;
}
function extractFunctions(source: string): ExtractedFunction[] {
const functions: ExtractedFunction[] = [];
const fnRegex =
/export\s+(async\s+)?function\s+(\w+)\s*\(([\s\S]*?)\)\s*:\s*([\w<>\[\]|, {}:]+)\s*\{/g;
let match;
while ((match = fnRegex.exec(source)) !== null) {
const isAsync = !!match[1];
const name = match[2];
if (name.startsWith('_')) continue;
const params = parseParams(match[3]);
const returnType = match[4].trim();
const description = getJSDocBefore(source, match.index);
// Get param descriptions from JSDoc
const jsdocBlock = source.substring(Math.max(0, match.index - 500), match.index);
const jsdocMatch = jsdocBlock.match(/\/\*\*([\s\S]*?)\*\/\s*$/);
if (jsdocMatch) {
const paramDescs = parseJSDocParams(jsdocMatch[0]);
for (const p of params) {
if (paramDescs[p.name]) p.description = paramDescs[p.name];
}
}
functions.push({
name,
description,
signature: `${isAsync ? 'async ' : ''}function ${name}(${params.map((p) => formatParam(p)).join(', ')}): ${returnType}`,
params,
returnType,
isAsync,
});
}
return functions;
}
function extractConstants(source: string): ExtractedConst[] {
const constants: ExtractedConst[] = [];
const constRegex = /export\s+const\s+(\w+)(?:\s*:\s*([^=]+))?\s*=/g;
let match;
while ((match = constRegex.exec(source)) !== null) {
const name = match[1];
if (name.startsWith('_')) continue;
const type = match[2]?.trim() || 'const';
const description = getJSDocBefore(source, match.index);
constants.push({ name, type, description });
}
return constants;
}
// ============================================================================
// Helpers
// ============================================================================
function extractBraceBlock(source: string, openBracePos: number): string {
let depth = 0;
let start = openBracePos;
for (let i = openBracePos; i < source.length; i++) {
if (source[i] === '{') depth++;
else if (source[i] === '}') {
depth--;
if (depth === 0) return source.substring(start + 1, i);
}
}
return source.substring(start + 1);
}
function parseParams(paramStr: string): ParamInfo[] {
if (!paramStr.trim()) return [];
const params: ParamInfo[] = [];
let depth = 0;
let current = '';
for (const char of paramStr) {
if (char === '(' || char === '<' || char === '{' || char === '[') depth++;
else if (char === ')' || char === '>' || char === '}' || char === ']') depth--;
if (char === ',' && depth === 0) {
const p = parseSingleParam(current.trim());
if (p) params.push(p);
current = '';
} else {
current += char;
}
}
if (current.trim()) {
const p = parseSingleParam(current.trim());
if (p) params.push(p);
}
return params;
}
function parseSingleParam(param: string): ParamInfo | null {
if (!param) return null;
// Match: name?: type = default
const match = param.match(/^(\w+)(\?)?\s*(?::\s*([\s\S]+?))?(?:\s*=\s*([\s\S]+))?$/);
if (!match) return null;
return {
name: match[1],
type: match[3]?.trim().replace(/\s*=\s*[\s\S]*$/, '') || 'any',
description: '',
defaultValue: match[4]?.trim() || null,
isOptional: !!match[2] || !!match[4],
};
}
function formatParam(p: ParamInfo): string {
const opt = p.isOptional && !p.defaultValue ? '?' : '';
const def = p.defaultValue ? ` = ${p.defaultValue}` : '';
return `${p.name}${opt}: ${p.type}${def}`;
}
function parseJSDocBlock(block: string): string {
return block
.replace(/^\/\*\*\s*/, '')
.replace(/\s*\*\/\s*$/, '')
.split('\n')
.map((l) => l.replace(/^\s*\*\s?/, ''))
.filter((l) => !l.startsWith('@'))
.join('\n')
.trim();
}
function parseJSDocParams(block: string): Record<string, string> {
const params: Record<string, string> = {};
const lines = block.split('\n');
for (const line of lines) {
const match = line.match(/@param\s+(\w+)\s+(.*)/);
if (match) params[match[1]] = match[2].trim();
}
return params;
}
// ============================================================================
// Version Discovery
// ============================================================================
interface VersionInfo {
version: string;
href: string;
isCurrent: boolean;
}
function getLatestReleasedVersion(config: SDKConfig, fallback: string): string {
try {
const output = execSync(`git tag | grep "^${config.tagPrefix}" | sort -V | tail -1`, {
encoding: 'utf-8',
cwd: ROOT_DIR,
}).trim();
if (output) return output.replace(config.tagPrefix, '');
} catch {
// fall through
}
return fallback;
}
function discoverVersions(config: SDKConfig, currentVersion: string): VersionInfo[] {
const baseDir = config.docsBaseDir
? path.join(ROOT_DIR, config.docsBaseDir)
: path.join(ROOT_DIR, 'docs/content/docs/cuabot');
const docsDir = path.join(baseDir, config.outputDir);
const hrefBase = config.hrefBase ?? '/cuabot';
const versions: VersionInfo[] = [];
const currentMM = currentVersion.split('.').slice(0, 2).join('.');
versions.push({ version: currentMM, href: `${hrefBase}/${config.outputDir}`, isCurrent: true });
if (fs.existsSync(docsDir)) {
for (const entry of fs.readdirSync(docsDir, { withFileTypes: true })) {
if (entry.isDirectory() && entry.name.startsWith('v')) {
const v = entry.name.substring(1);
if (v === currentMM) continue;
versions.push({
version: v,
href: `${hrefBase}/${config.outputDir}/${entry.name}/api`,
isCurrent: false,
});
}
}
}
versions.sort((a, b) => {
const pa = a.version.split('.').map(Number);
const pb = b.version.split('.').map(Number);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
if ((pa[i] || 0) !== (pb[i] || 0)) return (pb[i] || 0) - (pa[i] || 0);
}
return 0;
});
return versions;
}
function getPackageVersion(config: SDKConfig): string {
const pkgJsonPath = path.join(ROOT_DIR, path.dirname(config.packageDir), 'package.json');
try {
return JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')).version || '0.0.0';
} catch {
return '0.0.0';
}
}
// ============================================================================
// MDX Generation
// ============================================================================
function escapeMDX(text: string): string {
if (!text) return text;
return text
.replace(/\{/g, '\\{')
.replace(/\}/g, '\\}')
.replace(/<(?!\/?(?:Callout|Tab|Tabs|VersionHeader|div|span|a|code|pre|br|hr)\b)/g, '&lt;');
}
function generateMDX(modules: ModuleDoc[], config: SDKConfig): string {
const lines: string[] = [];
const pkgVersion = getPackageVersion(config);
const releasedVersion = getLatestReleasedVersion(config, pkgVersion);
const pageTitle = config.pageTitle ?? `${config.displayName} API Reference`;
// Frontmatter
lines.push('---');
lines.push(`title: ${pageTitle}`);
lines.push(`description: ${config.description}`);
lines.push('---');
lines.push('');
lines.push(`{/*`);
lines.push(` AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY`);
lines.push(` Generated by: npx tsx scripts/docs-generators/typescript-sdk.ts`);
lines.push(` Source: ${config.packageDir}`);
lines.push(` Version: ${releasedVersion}`);
lines.push(`*/}`);
lines.push('');
// Imports
lines.push("import { Callout } from 'fumadocs-ui/components/callout';");
lines.push("import { VersionHeader } from '@/components/version-selector';");
lines.push('');
// Version header
const versions = discoverVersions(config, releasedVersion);
const currentMM = releasedVersion.split('.').slice(0, 2).join('.');
lines.push('<VersionHeader');
lines.push(` versions={${JSON.stringify(versions)}}`);
lines.push(` currentVersion="${currentMM}"`);
lines.push(` fullVersion="${releasedVersion}"`);
lines.push(` packageName="${config.packageName}"`);
if (config.installCommand) {
lines.push(` installCommand="${config.installCommand}"`);
}
lines.push('/>');
lines.push('');
for (const mod of modules) {
lines.push('---');
lines.push('');
lines.push(`## ${mod.name}`);
lines.push('');
if (mod.description) {
lines.push(escapeMDX(mod.description));
lines.push('');
}
// Interfaces
for (const iface of mod.interfaces) {
lines.push(`### ${iface.name}`);
lines.push('');
if (iface.description) {
lines.push(escapeMDX(iface.description));
lines.push('');
}
lines.push('```typescript');
lines.push(`interface ${iface.name} {`);
for (const prop of iface.properties) {
const opt = prop.isOptional ? '?' : '';
lines.push(` ${prop.name}${opt}: ${prop.type};`);
}
lines.push('}');
lines.push('```');
lines.push('');
if (iface.properties.length > 0) {
lines.push('| Property | Type | Description |');
lines.push('|----------|------|-------------|');
for (const prop of iface.properties) {
const opt = prop.isOptional ? ' *(optional)*' : '';
lines.push(
`| \`${prop.name}\` | \`${escapeMDX(prop.type)}\` | ${opt}${escapeMDX(prop.description)} |`
);
}
lines.push('');
}
}
// Constants
for (const c of mod.constants) {
lines.push(`### ${c.name}`);
lines.push('');
lines.push('```typescript');
lines.push(`const ${c.name}: ${escapeMDX(c.type)}`);
lines.push('```');
lines.push('');
if (c.description) {
lines.push(escapeMDX(c.description));
lines.push('');
}
}
// Classes
for (const cls of mod.classes) {
lines.push(`### ${cls.name}`);
lines.push('');
if (cls.description) {
lines.push(escapeMDX(cls.description));
lines.push('');
}
if (cls.constructorSig) {
lines.push('#### Constructor');
lines.push('');
lines.push('```typescript');
lines.push(
`new ${cls.name}(${cls.constructorParams.map((p) => formatParam(p)).join(', ')})`
);
lines.push('```');
lines.push('');
if (cls.constructorParams.length > 0) {
lines.push(...generateParamsTable(cls.constructorParams));
}
}
if (cls.methods.length > 0) {
lines.push('#### Methods');
lines.push('');
for (const method of cls.methods) {
lines.push(`##### ${cls.name}.${method.name}`);
lines.push('');
lines.push('```typescript');
lines.push(method.signature);
lines.push('```');
lines.push('');
if (method.description) {
lines.push(escapeMDX(method.description));
lines.push('');
}
if (method.params.length > 0) {
lines.push(...generateParamsTable(method.params));
}
if (
method.returnType &&
method.returnType !== 'void' &&
method.returnType !== 'Promise<void>'
) {
lines.push(`**Returns:** \`${escapeMDX(method.returnType)}\``);
lines.push('');
}
}
}
}
// Functions
for (const fn of mod.functions) {
lines.push(`### ${fn.name}`);
lines.push('');
lines.push('```typescript');
lines.push(fn.signature);
lines.push('```');
lines.push('');
if (fn.description) {
lines.push(escapeMDX(fn.description));
lines.push('');
}
if (fn.params.length > 0) {
lines.push(...generateParamsTable(fn.params));
}
if (fn.returnType && fn.returnType !== 'void') {
lines.push(`**Returns:** \`${escapeMDX(fn.returnType)}\``);
lines.push('');
}
}
}
return lines.join('\n');
}
function generateParamsTable(params: ParamInfo[]): string[] {
const lines: string[] = [];
lines.push('**Parameters:**');
lines.push('');
lines.push('| Name | Type | Description |');
lines.push('|------|------|-------------|');
for (const p of params) {
const def = p.defaultValue ? ` (default: \`${p.defaultValue}\`)` : '';
const opt = p.isOptional ? ' *(optional)*' : '';
lines.push(
`| \`${p.name}\` | \`${escapeMDX(p.type)}\` | ${escapeMDX(p.description)}${opt}${def} |`
);
}
lines.push('');
return lines;
}
// ============================================================================
// Run
// ============================================================================
main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});