#!/usr/bin/env node const fs = require('fs'); const path = require('path'); function findWorkspaceRoot(startDir) { let current = startDir; while (true) { if (fs.existsSync(path.join(current, 'workspace.yml'))) { return current; } const parent = path.dirname(current); if (parent === current) { throw new Error('workspace.yml not found from the provided start directory'); } current = parent; } } function readText(filePath) { return fs.readFileSync(filePath, 'utf8'); } function stripQuotes(value) { const trimmed = String(value).trim(); if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { return trimmed.slice(1, -1); } return trimmed; } function parseWorkspace(workspacePath) { const text = readText(workspacePath); const lines = text.split(/\r?\n/); const collections = []; let inCollections = false; let currentCollection = null; for (const line of lines) { const trimmed = line.trim(); if (!inCollections && trimmed === 'collections:') { inCollections = true; continue; } if (!inCollections) { continue; } if (!line.startsWith(' ') && !line.startsWith('\t') && trimmed) { break; } const nameMatch = line.match(/^\s*-\s+name:\s*(.+)$/); if (nameMatch) { currentCollection = { name: stripQuotes(nameMatch[1]) }; collections.push(currentCollection); continue; } const pathMatch = line.match(/^\s*path:\s*(.+)$/); if (pathMatch && currentCollection) { currentCollection.path = stripQuotes(pathMatch[1]); } } return { collections }; } function sanitizeVarName(value) { return String(value) .trim() .replace(/[{}]/g, '') .replace(/[^A-Za-z0-9_]/g, '_') .replace(/^([0-9])/, '_$1') || 'value'; } function collectPlaceholders(value) { if (typeof value !== 'string') { return []; } const placeholders = []; const regex = /\{\{([^{}]+)\}\}/g; let match; while ((match = regex.exec(value)) !== null) { placeholders.push(match[1]); } return placeholders; } function findBlock(lines, keyName) { for (let i = 0; i < lines.length; i += 1) { const trimmed = lines[i].trim(); if (trimmed !== keyName && !trimmed.startsWith(`${keyName}:`)) { continue; } const lineIndent = lines[i].match(/^\s*/)[0].length; const block = []; for (let j = i + 1; j < lines.length; j += 1) { const currentLine = lines[j]; const currentTrimmed = currentLine.trim(); if (!currentTrimmed) { block.push(currentLine); continue; } const currentIndent = currentLine.match(/^\s*/)[0].length; if (currentIndent <= lineIndent && !currentLine.startsWith(' ')) { break; } if (currentIndent <= lineIndent && currentTrimmed.startsWith('#')) { block.push(currentLine); continue; } if (currentIndent <= lineIndent) { break; } block.push(currentLine); } return block; } return []; } function parseRequestInfo(text) { const lines = text.split(/\r?\n/); const infoLines = findBlock(lines, 'info'); const nameMatch = infoLines.join('\n').match(/^\s*name:\s*(.+)$/m); return nameMatch ? stripQuotes(nameMatch[1]) : ''; } function parseHttpBlock(text) { const lines = text.split(/\r?\n/); const blockLines = findBlock(lines, 'http'); const blockText = blockLines.join('\n'); const methodMatch = blockText.match(/^\s*method:\s*(.+)$/m); const urlMatch = blockText.match(/^\s*url:\s*(.+)$/m); const params = []; const paramLines = blockLines.join('\n').split(/\r?\n/); let inParamsBlock = false; let paramsIndent = 0; let currentParam = null; for (const line of paramLines) { const trimmed = line.trim(); if (!inParamsBlock) { if (trimmed === 'params:') { inParamsBlock = true; paramsIndent = line.match(/^\s*/)[0].length; } continue; } if (!trimmed) { continue; } const indent = line.match(/^\s*/)[0].length; if (indent <= paramsIndent) { break; } const listItemMatch = line.match(/^\s*-\s+name:\s*(.+)$/); if (listItemMatch) { currentParam = { name: stripQuotes(listItemMatch[1]) }; params.push(currentParam); continue; } if (!currentParam) { continue; } const propertyMatch = line.match(/^\s*([A-Za-z0-9_]+):\s*(.+)$/); if (propertyMatch) { const [, key, value] = propertyMatch; currentParam[key] = stripQuotes(value); } } return { method: methodMatch ? stripQuotes(methodMatch[1]) : 'GET', url: urlMatch ? stripQuotes(urlMatch[1]) : '', params, }; } function buildRequestContent(request, requestName) { const lines = []; const placeholders = new Set(); const addPlaceholders = (value) => { for (const placeholder of collectPlaceholders(value)) { placeholders.add(placeholder); } }; let url = request.url || ''; if (url) { addPlaceholders(url); url = url.replace(/:([A-Za-z0-9_]+)/g, (_, name) => `{{${name}}}`); } const queryParams = []; const headers = []; for (const param of request.params || []) { const name = param.name || ''; const value = param.value || ''; const type = (param.type || 'query').toLowerCase(); const disabled = String(param.disabled).toLowerCase() === 'true'; if (disabled) { continue; } addPlaceholders(value); if (type === 'header') { headers.push({ name, value }); } else { queryParams.push({ name, value }); } } if (placeholders.size > 0) { lines.push(`# Variables for ${requestName}`); for (const placeholder of [...placeholders].sort()) { lines.push(`@${sanitizeVarName(placeholder)} = YOUR_VALUE_HERE`); } lines.push(''); } const method = (request.method || 'GET').toUpperCase(); let requestUrl = url; for (const param of queryParams) { if (!param.name) { continue; } const separator = requestUrl.includes('?') ? '&' : '?'; requestUrl = `${requestUrl}${separator}${param.name}=${param.value}`; } lines.push(`${method} ${requestUrl}`); for (const header of headers) { lines.push(`${header.name}: ${header.value}`); } return lines.join('\n'); } function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); } function walkYamlFiles(rootDir) { const results = []; const entries = fs.readdirSync(rootDir, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith('.') || entry.name === 'node_modules') { continue; } const fullPath = path.join(rootDir, entry.name); if (entry.isDirectory()) { results.push(...walkYamlFiles(fullPath)); } else if (entry.isFile() && /\.ya?ml$/i.test(entry.name)) { results.push(fullPath); } } return results; } function main() { const workspaceRoot = findWorkspaceRoot(__dirname); const workspaceFile = path.join(workspaceRoot, 'workspace.yml'); const workspace = parseWorkspace(workspaceFile); const collections = Array.isArray(workspace.collections) ? workspace.collections : []; if (collections.length === 0) { throw new Error('No collections found in workspace.yml'); } for (const collection of collections) { if (!collection || !collection.name || !collection.path) { continue; } const sourceDir = path.join(workspaceRoot, collection.path); if (!fs.existsSync(sourceDir)) { console.warn(`Skipping missing collection path: ${collection.path}`); continue; } const outputRoot = path.join(workspaceRoot, 'autodocs', 'http', collection.name); ensureDir(outputRoot); const yamlFiles = walkYamlFiles(sourceDir); let processed = 0; for (const yamlFile of yamlFiles) { const content = readText(yamlFile); const requestName = parseRequestInfo(content) || path.basename(yamlFile, path.extname(yamlFile)); const httpBlock = parseHttpBlock(content); if (!httpBlock || !httpBlock.url) { continue; } const relativePath = path.relative(sourceDir, yamlFile); const parsedPath = path.parse(relativePath); const targetDir = path.join(outputRoot, parsedPath.dir); ensureDir(targetDir); const outputFile = path.join(targetDir, `${parsedPath.name}.http`); const requestContent = buildRequestContent(httpBlock, requestName); fs.writeFileSync(outputFile, `${requestContent}\n`, 'utf8'); processed += 1; } console.log(`Generated ${processed} .http file(s) for ${collection.name}`); } } try { main(); } catch (error) { console.error(error.message); process.exit(1); }