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
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:
@@ -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
|
||||
```
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, '{')
|
||||
.replace(/\}/g, '}')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
})
|
||||
.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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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, '<')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
# Python dependencies for documentation generation
|
||||
griffe>=0.40.0
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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, '<');
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user