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,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);
|
||||
});
|
||||
Reference in New Issue
Block a user