genlayer
Version:
GenLayer Command Line Tool
247 lines (214 loc) • 9.18 kB
JavaScript
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { spawnSync } from 'node:child_process';
function escapeMdx(text) {
if (!text) return '';
return String(text).replace(/</g, '<').replace(/>/g, '>');
}
function formatArg(arg) {
const base = arg.variadic ? `${arg.name}...` : arg.name;
return arg.required ? `<${base}>` : `[${base}]`;
}
function toSlug(text) {
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
}
function makeCommandFilepath(commandPath) {
const parts = commandPath.split(' ');
if (parts.length === 1) return { relDir: '', filename: `${toSlug(parts[0])}.mdx` };
return { relDir: parts.slice(0, -1).map(toSlug).join('/'), filename: `${toSlug(parts[parts.length - 1])}.mdx` };
}
function renderOptionsTable(options) {
if (!options.length) return 'No options.\n';
const rows = options
.map((o) => `| ${escapeMdx(o.short ?? '')} | ${escapeMdx(o.long ?? '')} | ${escapeMdx(o.description ?? '')} | ${o.required ? 'Yes' : 'No'} | ${o.defaultValue === undefined ? '' : `\`${escapeMdx(String(o.defaultValue))}\``} |`)
.join('\n');
return ['| Short | Long | Description | Required | Default |', '| --- | --- | --- | :---: | --- |', rows].join('\n');
}
function renderArgsList(args) {
if (!args.length) return 'No positional arguments.\n';
return args.map((a) => `- \`${formatArg(a)}\``).join('\n');
}
function generatePageForCommand(help, programName, commandPath) {
const { description, usage, args, options, subcommands } = help;
const title = commandPath || programName;
const header = `---\ntitle: ${escapeMdx(title)}\n---`;
const parts = [header];
if (description) parts.push('', description);
if (usage) parts.push('', '### Usage', '', `\`${usage}\``);
if (args && args.length) parts.push('', '### Arguments', '', renderArgsList(args));
parts.push('', '### Options', '', renderOptionsTable(options || []));
if (subcommands && subcommands.length) {
parts.push('', '### Subcommands', '');
for (const sc of subcommands) {
parts.push(`- \`${programName} ${sc.name}\` — ${escapeMdx(sc.description ?? '')}`);
}
}
const body = parts.join('\n').trim() + '\n';
const { relDir, filename } = makeCommandFilepath(commandPath || programName);
return { relDir, filename, content: body };
}
function generateIndexPage(rootHelp, programName, pkgVersion, pkgDescription) {
const title = `${programName} Commands`;
const header = `---\ntitle: ${escapeMdx(title)}\n---`;
const intro = rootHelp.description || pkgDescription || '';
const lines = [header, '', intro, `Version: \`${pkgVersion}\``, '', '### Command List', ''];
for (const sc of rootHelp.subcommands || []) {
lines.push(`- \`${programName} ${sc.name}\` — ${escapeMdx(sc.description ?? '')}`);
}
lines.push('', '---', '', 'This reference is auto-generated. Do not edit manually.');
return { relDir: '', filename: 'index.mdx', content: lines.join('\n') + '\n' };
}
function runHelp(args) {
const res = spawnSync(process.execPath, ['dist/index.js', ...args, '--help'], {
encoding: 'utf8',
timeout: 30000,
});
if ((res.status !== 0 || res.error) && res.stdout.trim() === '') {
const reason = res.error?.message || res.stderr || `exitCode=${res.status}`;
throw new Error(`Failed to run help for: ${args.join(' ')} (${reason})`);
}
return res.stdout;
}
function parseHelp(text, programName, commandPath) {
const lines = text.split(/\r?\n/);
let description = '';
let usage = '';
const options = [];
const subcommands = [];
const args = [];
// Accumulate description between the first blank after Usage and before Options/Commands
let inOptions = false;
let inCommands = false;
// Usage
const usageIdx = lines.findIndex((l) => l.startsWith('Usage:'));
if (usageIdx !== -1) {
const usageLine = lines[usageIdx];
// Replace program binary name (index) by programName
const afterColon = usageLine.replace(/^Usage:\s*/, '');
const replaced = afterColon.replace(/^\S+/, programName);
usage = `$ ${replaced}`;
}
// Description
for (let i = usageIdx + 1; i < lines.length; i += 1) {
const l = lines[i];
if (l.trim() === '') continue;
if (l.startsWith('Options:') || l.startsWith('Commands:')) break;
description += (description ? '\n' : '') + l.trim();
}
// Parse sections
for (let i = 0; i < lines.length; i += 1) {
const l = lines[i];
if (l.startsWith('Options:')) { inOptions = true; inCommands = false; continue; }
if (l.startsWith('Commands:')) { inCommands = true; inOptions = false; continue; }
if (/^\s*$/.test(l)) continue;
if (inOptions) {
// e.g., " -V, --version output the version number"
const m = l.match(/^\s*(-\w)?,?\s*(--[\w-]+)?\s{2,}(.+)$/);
if (m) {
const short = m[1] || '';
const long = m[2] || '';
const desc = m[3] || '';
options.push({ short, long, description: desc, required: false });
}
} else if (inCommands) {
// e.g., " deploy [options] Deploy intelligent contracts"
const m = l.match(/^\s*([\w-]+)(?:\s<[^>]+>|\s\[[^\]]+\])*\s{2,}(.+)$/);
if (m) {
const cmdToken = m[1];
const desc = m[2] || '';
const name = cmdToken.trim();
if (name !== 'help') {
subcommands.push({ name, description: desc });
}
}
}
}
// Derive args from usage (after commandPath)
if (usage) {
const usageCmd = usage.replace(/^`?\$\s+/, '').replace(/`?$/, '');
const tokens = usageCmd.split(/\s+/);
// find starting index of commandPath tokens
const cmdTokens = (commandPath ? `${programName} ${commandPath}` : programName).split(' ');
const start = tokens.findIndex((t, idx) => tokens.slice(idx, idx + cmdTokens.length).join(' ') === cmdTokens.join(' '));
const after = start >= 0 ? tokens.slice(start + cmdTokens.length) : [];
for (const t of after) {
if (t === '[options]') continue;
const m = t.match(/^<(.*)>$/) || t.match(/^\[(.*)\]$/);
if (m) {
const variadic = m[1].endsWith('...');
const name = variadic ? m[1].slice(0, -3) : m[1];
const required = t.startsWith('<');
args.push({ name, variadic, required });
}
}
}
return { description, usage, options, subcommands, args };
}
async function ensureDir(dir) {
await fs.mkdir(dir, { recursive: true });
}
async function writePages(root, pages) {
for (const page of pages) {
const dir = path.join(root, page.relDir);
await ensureDir(dir);
const fullpath = path.join(dir, page.filename);
await fs.writeFile(fullpath, page.content, 'utf8');
}
}
async function rmrf(dir) {
try {
await fs.rm(dir, { recursive: true, force: true });
} catch {}
}
async function readPackageInfo() {
const here = path.dirname(fileURLToPath(import.meta.url));
const pkgPath = path.join(here, '..', 'package.json');
const raw = await fs.readFile(pkgPath, 'utf8');
const json = JSON.parse(raw);
return { version: json.version, description: json.description };
}
async function main() {
const { version: pkgVersion, description: pkgDescription } = await readPackageInfo();
const programName = 'genlayer';
// Ensure CLI is built before attempting to read help output
const here = path.dirname(fileURLToPath(import.meta.url));
const cliPath = path.join(here, '..', 'dist', 'index.js');
try {
await fs.access(cliPath);
} catch {
throw new Error('CLI not built. Please run "npm run build" first.');
}
const rootHelpText = runHelp([]);
const rootHelp = parseHelp(rootHelpText, programName, '');
const outputDirFromEnv = process.env.DOCS_OUTPUT_DIR;
const clean = (process.env.DOCS_CLEAN || '').toLowerCase() === 'true';
const outputs = [];
// filter out auto 'help' just in case
rootHelp.subcommands = (rootHelp.subcommands || []).filter((c) => c.name !== 'help');
outputs.push(generateIndexPage(rootHelp, programName, pkgVersion, pkgDescription));
// BFS through subcommands by invoking help for each
const queue = [...(rootHelp.subcommands || [])].map((c) => ({ path: c.name }));
while (queue.length) {
const { path: cmdPath } = queue.shift();
const helpText = runHelp(cmdPath.split(' '));
const help = parseHelp(helpText, programName, cmdPath);
outputs.push(generatePageForCommand(help, programName, cmdPath));
for (const sc of help.subcommands || []) {
queue.push({ path: `${cmdPath} ${sc.name}` });
}
}
const defaultOut = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'docs', 'api-references');
const rootOut = outputDirFromEnv ? outputDirFromEnv : defaultOut;
if (clean) await rmrf(rootOut);
await writePages(rootOut, outputs);
const meta = {};
for (const c of (rootHelp.subcommands || []).map((c) => toSlug(c.name))) meta[c] = c;
await fs.writeFile(path.join(rootOut, '_meta.json'), JSON.stringify(meta, null, 2), 'utf8');
console.log(`Generated ${outputs.length} pages at ${rootOut}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});