Files
wehub-resource-sync 91e75e620b
CI: cua-driver distro-compat matrix / Resolve release version (push) Waiting to run
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / Distro compat summary (push) Blocked by required conditions
CI: Nix Linux Rust source / Nix / compositor build (push) Waiting to run
CI: Nix Linux Rust source / Nix / driver package (push) Waiting to run
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Waiting to run
CI: Rust Linux unit / Rust Linux unit and compile (push) Waiting to run
CI: Rust Windows unit / Rust Windows unit and compile (push) Waiting to run
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Waiting to run
CD: Docs MCP Server / build (linux/amd64) (push) Waiting to run
CD: Docs MCP Server / build (linux/arm64) (push) Waiting to run
CD: Docs MCP Server / merge (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

752 lines
22 KiB
TypeScript

#!/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);
});