import fs from 'node:fs/promises' import path from 'node:path' import { convertRequest } from './request.js' import { convertManifest } from './manifest.js' import { convertFolder } from './folder.js' import { convertEnvironment } from './environment.js' import type { ConversionResult, ConversionWarning } from '../types.js' const KNOWN_REQUEST_KEYS = new Set(['info', 'http', 'runtime', 'settings', 'docs']) async function exists(filePath: string): Promise { try { await fs.access(filePath) return true } catch { return false } } async function ensureDir(dirPath: string): Promise { await fs.mkdir(dirPath, { recursive: true }) } async function walkDir(dir: string): Promise { const entries = await fs.readdir(dir, { withFileTypes: true }) const files: string[] = [] for (const entry of entries) { const full = path.join(dir, entry.name) if (entry.isDirectory()) { files.push(...await walkDir(full)) } else if (entry.isFile()) { files.push(full) } } return files } function relPath(from: string, to: string): string { return path.relative(from, to) } function outputPath(inputDir: string, outputDir: string, inputFile: string): string { const rel = relPath(inputDir, inputFile) return path.join(outputDir, rel) } async function detectUnsupportedFields(yamlContent: string, filePath: string, warnings: ConversionWarning[]): Promise { // Quick scan for top-level keys not in the known set try { const { default: yaml } = await import('js-yaml') const raw = yaml.load(yamlContent) as Record if (raw && typeof raw === 'object') { for (const key of Object.keys(raw)) { if (!KNOWN_REQUEST_KEYS.has(key)) { warnings.push({ file: filePath, message: `Unsupported field '${key}' in request file — skipped`, type: 'unsupported-field', }) } } } } catch { // parse errors handled elsewhere } } export async function convertCollection(inputDir: string, outputDir: string): Promise { const warnings: ConversionWarning[] = [] const files: string[] = [] let requestsConverted = 0 let environmentsConverted = 0 let foldersConverted = 0 let manifestGenerated = false // Verify opencollection.yml exists const manifestPath = path.join(inputDir, 'opencollection.yml') if (!(await exists(manifestPath))) { throw new Error(`No opencollection.yml found in '${inputDir}'. Is this an OpenCollection directory?`) } await ensureDir(outputDir) // Convert collection manifest const manifestYaml = await fs.readFile(manifestPath, 'utf-8') const { brunoJson, collectionBru } = convertManifest(manifestYaml) const brunoJsonPath = path.join(outputDir, 'bruno.json') const collectionBruPath = path.join(outputDir, 'collection.bru') await fs.writeFile(brunoJsonPath, brunoJson, 'utf-8') await fs.writeFile(collectionBruPath, collectionBru, 'utf-8') files.push('bruno.json', 'collection.bru') manifestGenerated = true // Walk all files const allFiles = await walkDir(inputDir) // Track generated filenames per directory to detect duplicates const usedNames = new Map>() for (const filePath of allFiles) { const rel = relPath(inputDir, filePath) const basename = path.basename(filePath) const dirName = path.dirname(rel) const isInEnvDir = dirName === 'environments' || dirName.startsWith('environments' + path.sep) // Skip root manifest if (rel === 'opencollection.yml') continue // Environment files if (isInEnvDir && basename.endsWith('.yml')) { const yamlContent = await fs.readFile(filePath, 'utf-8') const outFile = outputPath(inputDir, outputDir, filePath).replace(/\.yml$/, '.bru') await ensureDir(path.dirname(outFile)) const bruContent = convertEnvironment(yamlContent) await fs.writeFile(outFile, bruContent, 'utf-8') const outRel = relPath(outputDir, outFile) files.push(outRel) environmentsConverted++ continue } // Folder configs if (basename === 'folder.yml') { const yamlContent = await fs.readFile(filePath, 'utf-8') const outFile = outputPath(inputDir, outputDir, filePath).replace(/\.yml$/, '.bru') await ensureDir(path.dirname(outFile)) const bruContent = convertFolder(yamlContent) await fs.writeFile(outFile, bruContent, 'utf-8') const outRel = relPath(outputDir, outFile) files.push(outRel) foldersConverted++ continue } // Request files if (basename.endsWith('.yml')) { const yamlContent = await fs.readFile(filePath, 'utf-8') await detectUnsupportedFields(yamlContent, rel, warnings) let bruContent: string try { bruContent = convertRequest(yamlContent, rel) } catch (err) { warnings.push({ file: rel, message: err instanceof Error ? err.message : String(err), type: 'skipped-request', }) continue } // Duplicate name detection const outDir = path.dirname(outputPath(inputDir, outputDir, filePath)) const baseBru = basename.replace(/\.yml$/, '.bru') if (!usedNames.has(outDir)) usedNames.set(outDir, new Set()) const usedInDir = usedNames.get(outDir)! let finalBru = baseBru if (usedInDir.has(baseBru)) { let counter = 2 while (usedInDir.has(baseBru.replace('.bru', `-${counter}.bru`))) counter++ finalBru = baseBru.replace('.bru', `-${counter}.bru`) warnings.push({ file: rel, message: `Duplicate output filename '${baseBru}' renamed to '${finalBru}'`, type: 'duplicate-name', }) } usedInDir.add(finalBru) const outFile = path.join(outDir, finalBru) await ensureDir(outDir) await fs.writeFile(outFile, bruContent, 'utf-8') const outRel = relPath(outputDir, outFile) files.push(outRel) requestsConverted++ } } return { requestsConverted, environmentsConverted, foldersConverted, manifestGenerated, files, warnings, } }