Skip to main content
Glama
bruno-cli.ts9.2 kB
import { createRequire } from 'module'; import { execa } from 'execa'; import type { ConfigLoader } from './config.js'; import type { IBrunoCLI } from './interfaces.js'; import type { PerformanceManager } from './performance.js'; import { CollectionDiscoveryService } from './services/CollectionDiscoveryService.js'; import { EnvironmentService } from './services/EnvironmentService.js'; import { RequestExecutionService } from './services/RequestExecutionService.js'; import { ValidationService } from './services/ValidationService.js'; export interface BrunoRunOptions { environment?: string; envVariables?: Record<string, string>; folderPath?: string; format?: 'json' | 'junit' | 'html'; output?: string; recursive?: boolean; testsOnly?: boolean; bail?: boolean; // Report generation options reporterJson?: string; // Path to write JSON report reporterJunit?: string; // Path to write JUnit XML report reporterHtml?: string; // Path to write HTML report } export interface BrunoRunResult { stdout: string; stderr: string; exitCode: number; summary?: { totalRequests: number; passedRequests: number; failedRequests: number; totalDuration: number; }; results?: Array<{ name: string; passed: boolean; status: number; duration: number; error?: string; request?: { method?: string; url?: string; headers?: Record<string, string>; body?: any; }; response?: { status?: number; statusText?: string; headers?: Record<string, string>; body?: any; responseTime?: number; }; assertions?: Array<{ name: string; passed: boolean; error?: string; }>; }>; } export interface BrunoRequest { name: string; method?: string; url?: string; folder?: string; path?: string; } /** * Facade for Bruno CLI operations * Delegates to focused service classes following Single Responsibility Principle */ export class BrunoCLI implements IBrunoCLI { private brunoCommand: string = 'bru'; private requestExecutionService: RequestExecutionService; private collectionDiscoveryService: CollectionDiscoveryService; private environmentService: EnvironmentService; private validationService: ValidationService; private configLoader: ConfigLoader; private performanceManager: PerformanceManager; constructor(configLoader: ConfigLoader, performanceManager: PerformanceManager, brunoPath?: string) { this.configLoader = configLoader; this.performanceManager = performanceManager; if (brunoPath) { this.brunoCommand = brunoPath; } else { // Check configuration for custom Bruno CLI path const config = this.configLoader.getConfig(); if (config.brunoCliPath) { this.brunoCommand = config.brunoCliPath; } else { // Try to find the local Bruno CLI installation this.brunoCommand = this.findBrunoCLI(); } } // Initialize services with dependencies this.requestExecutionService = new RequestExecutionService(this.brunoCommand, this.configLoader); this.collectionDiscoveryService = new CollectionDiscoveryService(this.performanceManager); this.environmentService = new EnvironmentService(this.performanceManager); this.validationService = new ValidationService(); } /** * Find the Bruno CLI executable */ private findBrunoCLI(): string { const require = createRequire(import.meta.url); const fsSync = require('fs'); try { // Try to resolve the Bruno CLI package const brunoPkgPath = require.resolve('@usebruno/cli/package.json'); const brunoPkgDir = brunoPkgPath.substring(0, brunoPkgPath.lastIndexOf('/')); const brunoBinPath = `${brunoPkgDir}/bin/bru.js`; // Check if the binary exists if (fsSync.existsSync(brunoBinPath)) { return brunoBinPath; // Return the full path to bru.js } } catch { // Package not found, try global installation } // Fall back to trying the global 'bru' command return 'bru'; } /** * Check if Bruno CLI is available */ async isAvailable(): Promise<boolean> { try { await execa(this.brunoCommand, ['--version'], { timeout: 5000, reject: false }); return true; } catch { return false; } } /** * Run a single request * Delegates to RequestExecutionService */ async runRequest( collectionPath: string, requestName: string, options: BrunoRunOptions = {} ): Promise<BrunoRunResult> { try { return await this.requestExecutionService.runRequest( collectionPath, requestName, options, (cp, rn) => this.collectionDiscoveryService.findRequestFile(cp, rn) ); } catch (error) { throw this.handleError(error); } } /** * Run a collection or folder * Delegates to RequestExecutionService */ async runCollection( collectionPath: string, options: BrunoRunOptions = {} ): Promise<BrunoRunResult> { try { return await this.requestExecutionService.runCollection(collectionPath, options); } catch (error) { throw this.handleError(error); } } /** * List all requests in a collection * Delegates to CollectionDiscoveryService */ async listRequests(collectionPath: string): Promise<BrunoRequest[]> { return this.collectionDiscoveryService.listRequests(collectionPath); } /** * Discover Bruno collections in a directory * Delegates to CollectionDiscoveryService */ async discoverCollections(searchPath: string, maxDepth: number = 5): Promise<string[]> { return this.collectionDiscoveryService.discoverCollections(searchPath, maxDepth); } /** * List environments in a collection * Delegates to EnvironmentService */ async listEnvironments(collectionPath: string): Promise<Array<{ name: string; path: string; variables?: Record<string, string>; }>> { return this.environmentService.listEnvironments(collectionPath); } /** * Validate an environment file * Delegates to EnvironmentService */ async validateEnvironment(collectionPath: string, environmentName: string): Promise<{ valid: boolean; exists: boolean; errors: string[]; warnings: string[]; variables?: Record<string, string>; }> { return this.environmentService.validateEnvironment(collectionPath, environmentName); } /** * Get detailed information about a request * Delegates to ValidationService via CollectionDiscoveryService */ async getRequestDetails(collectionPath: string, requestName: string): Promise<{ name: string; method: string; url: string; headers: Record<string, string>; body?: { type: string; content: string; }; auth: string; tests?: string[]; metadata: { type: string; seq?: number; }; }> { // Find the request file const requestFile = await this.collectionDiscoveryService.findRequestFile(collectionPath, requestName); if (!requestFile) { throw new Error(`Request "${requestName}" not found in collection`); } // Get details from validation service return this.validationService.getRequestDetails(requestFile, requestName); } /** * Validate a Bruno collection * Delegates to ValidationService with callbacks to other services */ async validateCollection(collectionPath: string): Promise<{ valid: boolean; errors: string[]; warnings: string[]; summary: { hasBrunoJson: boolean; totalRequests: number; validRequests: number; invalidRequests: number; environments: number; }; }> { return this.validationService.validateCollection( collectionPath, () => this.listRequests(collectionPath), () => this.listEnvironments(collectionPath), (envName) => this.validateEnvironment(collectionPath, envName), (reqName) => this.getRequestDetails(collectionPath, reqName) ); } /** * Handle errors from Bruno CLI */ private handleError(error: any): Error { // Check if it's an execa error by checking for specific properties if (error && typeof error === 'object' && 'stderr' in error) { if (error.stderr?.includes('command not found') || error.stderr?.includes('not recognized') || error.stderr?.includes('ENOENT')) { return new Error( `Bruno CLI not found at: ${this.brunoCommand}. ` + 'Please ensure Bruno CLI is installed by running: npm install' ); } if (error.stderr?.includes('You can run only at the root of a collection')) { return new Error( 'Invalid collection directory. Bruno CLI must be run from the root of a Bruno collection. ' + 'Ensure the directory contains a bruno.json or collection.bru file.' ); } if (error.stderr) { return new Error(`Bruno CLI error: ${error.stderr}`); } return new Error(`Bruno CLI failed: ${error.message || 'Unknown error'}`); } return error instanceof Error ? error : new Error(String(error)); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jcr82/bruno-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server