import { Command } from "commander"; import { readFileSync, mkdirSync, writeFileSync } from "node:fs"; import { join, resolve, basename } from "node:path"; import chalk from "chalk"; import ora from "ora"; import { parse as parseYaml } from "yaml"; interface OpenApiOperation { operationId?: string; summary?: string; description?: string; parameters?: Array<{ name: string; in: string; required?: boolean; description?: string; schema?: { type?: string; format?: string; enum?: string[] }; }>; requestBody?: { required?: boolean; content?: Record }>; }; security?: Array>; tags?: string[]; } interface OpenApiSpec { openapi?: string; swagger?: string; info?: { title?: string; description?: string; version?: string }; servers?: Array<{ url: string; description?: string }>; paths?: Record>; security?: Array>; components?: { securitySchemes?: Record< string, { type?: string; scheme?: string; in?: string; name?: string } >; }; } function slugify(text: string): string { return text .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, "") .slice(0, 60); } function yamlQuote(value: string): string { if ( /[:#{}[\],&*?|>!%@`]/.test(value) || value.startsWith("'") || value.startsWith('"') ) { return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; } return value; } function buildSkillMd(opts: { name: string; description: string; version: string; permissions: string[]; instructions: string; }): string { const permsLine = opts.permissions.length > 0 ? `\npermissions:\n${opts.permissions.map((p) => ` - ${p}`).join("\n")}` : ""; return `--- name: ${yamlQuote(opts.name)} description: ${yamlQuote(opts.description)} version: ${opts.version} platforms: - CLAUDE_CODE - CURSOR${permsLine} --- ${opts.instructions} `; } function describeParameters(params: OpenApiOperation["parameters"]): string { if (!params || params.length === 0) return ""; const lines = params.map((p) => { const req = p.required ? " (required)" : " (optional)"; const typeStr = p.schema?.type ? ` [${p.schema.type}]` : ""; const desc = p.description ? `, ${p.description}` : ""; return `- \`${p.name}\`${typeStr}${req}${desc}`; }); return `\n## Parameters\n\n${lines.join("\n")}`; } function describeRequestBody(body: OpenApiOperation["requestBody"]): string { if (!body?.content) return ""; const contentTypes = Object.keys(body.content); const req = body.required ? " (required)" : " (optional)"; return `\n## Request Body${req}\n\nContent-Type: ${contentTypes.join(", ")}`; } function describeAuth( opSecurity: OpenApiOperation["security"], globalSecurity: OpenApiSpec["security"], securitySchemes: OpenApiSpec["components"], ): string { const security = opSecurity ?? globalSecurity; if (!security || security.length === 0) return ""; const schemes = securitySchemes?.securitySchemes ?? {}; const authDescriptions = security.flatMap((sec) => Object.keys(sec).map((name) => { const scheme = schemes[name]; if (scheme?.type === "http" && scheme.scheme === "bearer") return "Bearer token authentication"; if (scheme?.type === "apiKey") return `API key in ${scheme.in}: ${scheme.name}`; if (scheme?.type === "oauth2") return "OAuth 2.0 authentication"; return `${name} authentication`; }), ); if (authDescriptions.length === 0) return ""; return `\n## Authentication\n\n${authDescriptions.map((d) => `- ${d}`).join("\n")}`; } export const generateCommand = new Command("generate") .description("Generate SKILL.md files from an OpenAPI spec") .option( "--openapi ", "Path to OpenAPI/Swagger spec file (JSON or YAML)", ) .option("-o, --output ", "Output directory", "./skills") .action(async (options) => { if (!options.openapi) { console.error(chalk.red("Error: --openapi is required")); process.exit(1); } const spinner = ora("Reading OpenAPI spec...").start(); try { const specPath = resolve(options.openapi); const raw = readFileSync(specPath, "utf-8"); let spec: OpenApiSpec; if (specPath.endsWith(".yaml") || specPath.endsWith(".yml")) { spec = parseYaml(raw) as OpenApiSpec; } else { spec = JSON.parse(raw) as OpenApiSpec; } if (!spec.openapi && !spec.swagger) { spinner.fail("File is not a valid OpenAPI/Swagger spec"); process.exit(1); } if (!spec.paths || Object.keys(spec.paths).length === 0) { spinner.fail("No paths found in spec"); process.exit(1); } const apiName = spec.info?.title ?? basename(specPath, ".json").replace(/[-_]/g, " "); const apiVersion = spec.info?.version ?? "1.0.0"; const baseUrl = spec.servers?.[0]?.url ?? "https://api.example.com"; const outputDir = resolve(options.output); mkdirSync(outputDir, { recursive: true }); let count = 0; const HTTP_METHODS = [ "get", "post", "put", "patch", "delete", "head", "options", ]; for (const [path, methods] of Object.entries(spec.paths)) { for (const [method, operation] of Object.entries(methods)) { if (!HTTP_METHODS.includes(method.toLowerCase())) continue; if (!operation || typeof operation !== "object") continue; const op = operation as OpenApiOperation; const opName = op.operationId ?? `${method.toLowerCase()}-${path .replace(/[{}\/]/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, "")}`; const slug = slugify(`${slugify(apiName)}-${opName}`); const displayName = op.summary ?? opName.replace(/[-_]/g, " "); const description = ( op.description ?? op.summary ?? `${method.toUpperCase()} ${path}` ).slice(0, 990); const hasAuth = (op.security ?? spec.security ?? []).length > 0; const permissions: string[] = ["network"]; if (hasAuth) permissions.push("api"); const instructions = [ `# ${displayName}`, "", op.description ?? op.summary ?? "", "", `## Endpoint`, "", `\`${method.toUpperCase()} ${baseUrl}${path}\``, describeParameters(op.parameters), describeRequestBody(op.requestBody), describeAuth(op.security, spec.security, spec.components), "", `## Usage`, "", `Make a ${method.toUpperCase()} request to \`${baseUrl}${path}\`.`, hasAuth ? "Include the appropriate authentication credentials." : "", ] .filter(Boolean) .join("\n"); const content = buildSkillMd({ name: displayName.slice(0, 100), description: description.slice(0, 1000), version: apiVersion, permissions, instructions, }); writeFileSync(join(outputDir, `${slug}.md`), content); count++; } } spinner.succeed( `Generated ${chalk.bold(count)} skill files in ${chalk.cyan(outputDir)}`, ); console.log(` Source: ${chalk.dim(specPath)}`); console.log(` API: ${chalk.dim(apiName)} v${apiVersion}`); console.log( `\n Next: ${chalk.yellow("skills-hub publish")} each skill to publish to Skills Hub`, ); } catch (err) { spinner.fail( chalk.red(err instanceof Error ? err.message : "Generation failed"), ); process.exit(1); } });