#!/usr/bin/env tsx
// scripts/update-readme.ts
import { readFile, writeFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { getSchemaDescription } from '@modelcontextprotocol/sdk/server/zod-compat.js';
import { FluxSchema } from '../src/schemas/flux/index.js';
import { ScoutSchema } from '../src/schemas/scout/index.js';
import type { z } from 'zod';
// Import all individual schemas
import * as containerSchemas from '../src/schemas/flux/container.js';
import * as composeSchemas from '../src/schemas/flux/compose.js';
import * as dockerSchemas from '../src/schemas/flux/docker.js';
import * as hostSchemas from '../src/schemas/flux/host.js';
import * as scoutSimpleSchemas from '../src/schemas/scout/simple.js';
import * as scoutZfsSchemas from '../src/schemas/scout/zfs.js';
import * as scoutLogsSchemas from '../src/schemas/scout/logs.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = join(__dirname, '..');
const readmePath = join(rootDir, 'README.md');
interface OperationInfo {
subaction: string;
description: string;
isDestructive: boolean;
isStateChange: boolean;
isDiagnostic: boolean;
}
function extractSchemaInfo(schema: z.ZodTypeAny): OperationInfo | null {
try {
// Unwrap preprocess if needed (flux schemas)
let unwrapped = schema;
const def = (schema as any)?._def;
if (def?.type === 'pipe' && def?.out) {
unwrapped = def.out;
}
// Get description (it's a property on the schema, not in _def)
const description = (unwrapped as any).description || '';
if (!description) return null;
// Get action/subaction from schema shape (shape is an object, not a function)
const shape = unwrapped._def?.shape;
let subaction: string;
let actionKey: string = '';
// Try flux format first (action_subaction)
const actionSubactionSchema = shape?.action_subaction;
if (actionSubactionSchema?.values) {
const actionSubaction = [...actionSubactionSchema.values][0];
if (actionSubaction && typeof actionSubaction === 'string') {
const parts = actionSubaction.split(':');
subaction = parts.length > 1 ? parts[1] : parts[0];
actionKey = actionSubaction;
} else {
return null;
}
} else {
// Try scout ZFS/Logs format (subaction field) - uses .def.values not ._def.values
const subactionSchema = shape?.subaction;
const subactionValues = subactionSchema?.def?.values || subactionSchema?._def?.values || subactionSchema?.values;
if (subactionValues && subactionValues.length > 0) {
const subactionValue = subactionValues[0];
if (subactionValue && typeof subactionValue === 'string') {
subaction = subactionValue;
actionKey = subactionValue;
} else {
return null;
}
} else {
// Try scout simple format (just action)
const actionSchema = shape?.action;
if (actionSchema?.values) {
const action = [...actionSchema.values][0];
if (action && typeof action === 'string') {
subaction = action;
actionKey = action;
} else {
return null;
}
} else {
return null;
}
}
}
// Determine operation type from description keywords
const lowerDesc = description.toLowerCase();
const isDestructive = lowerDesc.includes('remove') ||
lowerDesc.includes('delete') ||
lowerDesc.includes('prune') ||
lowerDesc.includes('recreate') ||
actionKey.includes('down');
const isDiagnostic = lowerDesc.includes('check') ||
lowerDesc.includes('diagnostic') ||
lowerDesc.includes('health') ||
lowerDesc.includes('list') && actionKey.includes('zfs:') ||
actionKey.includes('doctor') ||
actionKey.includes('host:status');
const isStateChange = !isDestructive && !isDiagnostic && (
lowerDesc.includes('start') ||
lowerDesc.includes('stop') ||
lowerDesc.includes('restart') ||
lowerDesc.includes('pause') ||
lowerDesc.includes('resume') ||
lowerDesc.includes('pull') ||
lowerDesc.includes('build') ||
lowerDesc.includes('execute') ||
lowerDesc.includes('transfer')
);
return {
subaction,
description,
isDestructive,
isStateChange,
isDiagnostic
};
} catch (error) {
console.error(`Failed to extract schema info:`, error);
return null;
}
}
function formatOperation(op: OperationInfo): string {
const indicator = op.isDestructive ? '⚠️' : op.isStateChange ? '●' : op.isDiagnostic ? '✓' : '●';
const padding = op.isDestructive ? '' : ' '; // Extra space for non-warning symbols
return ` ${indicator}${padding} ${op.subaction.padEnd(10)} - ${op.description}`;
}
function generateFluxSection(schemas: Record<string, z.ZodTypeAny>, category: string): string[] {
const operations: OperationInfo[] = [];
for (const schema of Object.values(schemas)) {
const info = extractSchemaInfo(schema);
if (info) operations.push(info);
}
if (operations.length === 0) return [];
const lines = [`${category} (${operations.length} operations)`];
operations.forEach(op => lines.push(formatOperation(op)));
lines.push(''); // Empty line between sections
return lines;
}
function generateScoutSection(
schemas: Record<string, z.ZodTypeAny>,
category: string,
description?: string
): string[] {
const operations: OperationInfo[] = [];
const schemaValues = Object.values(schemas);
// Check if we have individual schemas (to avoid processing union schemas that would duplicate)
const hasIndividual = schemaValues.some(s => !(s as any)?._def?.options);
for (const schema of schemaValues) {
const def = (schema as any)?._def;
const isUnion = def?.options && Array.isArray(def.options);
// Skip unions if we have individual schemas (avoids duplication in simple.ts)
if (isUnion && hasIndividual) {
continue;
}
if (isUnion) {
// Extract operations from each option in the union (ZFS, Logs)
for (const option of def.options) {
const info = extractSchemaInfo(option);
if (info) operations.push(info);
}
} else {
// Regular schema (Simple actions)
const info = extractSchemaInfo(schema);
if (info) operations.push(info);
}
}
if (operations.length === 0) return [];
const header = description
? `${category} (${operations.length} operations)`
: `${category} (${operations.length} operations)`;
const lines = [header];
operations.forEach(op => lines.push(formatOperation(op)));
lines.push(''); // Empty line between sections
return lines;
}
async function updateReadme(): Promise<void> {
console.log('📖 Reading README.md...');
const readme = await readFile(readmePath, 'utf-8');
// Extract top-level descriptions from schemas
const fluxDesc = getSchemaDescription(FluxSchema) ?? 'Docker infrastructure management';
const scoutDesc = getSchemaDescription(ScoutSchema) ?? 'SSH remote operations';
console.log('✓ Flux description:', fluxDesc);
console.log('✓ Scout description:', scoutDesc);
// Generate formatted tool sections
const fluxLines = [
'### Tool 1: `flux` - Docker Infrastructure Management',
'',
'**43 operations across 5 actions** - Container lifecycle, compose orchestration, system management',
'',
'```',
'FLUX OPERATIONS:',
'',
...generateFluxSection(containerSchemas, 'Container'),
...generateFluxSection(composeSchemas, 'Compose'),
...generateFluxSection(dockerSchemas, 'Docker'),
...generateFluxSection(hostSchemas, 'Host'),
'```'
].join('\n');
const scoutLines = [
'### Tool 2: `scout` - SSH Remote Operations',
'',
'**16 operations across 11 actions** - File operations, process inspection, system logs',
'',
'```',
'SCOUT OPERATIONS:',
'',
...generateScoutSection(scoutSimpleSchemas, 'Simple Actions'),
...generateScoutSection(scoutZfsSchemas, 'ZFS'),
...generateScoutSection(scoutLogsSchemas, 'Logs'),
'```',
'',
'**Legend:**',
'- `●` State-changing operation',
'- `⚠️` Destructive operation (requires `force: true`)',
'- `✓` Diagnostic/health check',
'- `→` Port mapping notation (host→container/protocol)',
].join('\n');
let updated = readme;
// Update short descriptions in "Available Tools" section
const toolsTableRegex = /#### flux\s*\n+([\s\S]*?)\n+(?=####|##|$)/i;
const scoutTableRegex = /#### scout\s*\n+([\s\S]*?)\n+(?=####|##|$)/i;
updated = updated.replace(toolsTableRegex, `#### flux\n\n${fluxDesc}\n\n`);
updated = updated.replace(scoutTableRegex, `#### scout\n\n${scoutDesc}\n\n`);
// Replace detailed tool sections
const fluxSectionRegex = /### Tool 1: `flux`[\s\S]*?(?=---\n\n### Tool 2:|$)/;
const scoutSectionRegex = /### Tool 2: `scout`[\s\S]*?(?=---\n\n|## |$)/;
if (fluxSectionRegex.test(updated)) {
updated = updated.replace(fluxSectionRegex, fluxLines + '\n\n---\n\n');
console.log('✓ Updated flux tool section');
} else {
console.warn('⚠️ Could not find flux tool section');
}
if (scoutSectionRegex.test(updated)) {
updated = updated.replace(scoutSectionRegex, scoutLines + '\n\n');
console.log('✓ Updated scout tool section');
} else {
console.warn('⚠️ Could not find scout tool section');
}
// Check if README needs updating
if (updated === readme) {
console.log('✓ README already up-to-date');
return;
}
// Write updated README
await writeFile(readmePath, updated, 'utf-8');
console.log('✅ README.md updated successfully');
}
updateReadme().catch((error) => {
console.error('❌ Failed to update README:', error);
process.exit(1);
});