Skip to main content
Glama
utils.ts20.1 kB
/** * Utility functions for devpipe MCP server */ import { exec } from 'child_process'; import { promisify } from 'util'; import { readFile, access, readdir, stat } from 'fs/promises'; import { join, dirname } from 'path'; import * as TOML from '@iarna/toml'; import type { DevpipeConfig, SummaryData, RunMetadata } from './types.js'; const execAsync = promisify(exec); /** * Check if devpipe is installed and accessible */ export async function checkDevpipeInstalled(): Promise<{ installed: boolean; version?: string; error?: string }> { try { const { stdout } = await execAsync('devpipe --version'); return { installed: true, version: stdout.trim() }; } catch (error) { return { installed: false, error: 'devpipe not found. Install it with: brew install drewkhoury/tap/devpipe' }; } } /** * Find config.toml file in current directory or parent directories */ export async function findConfigFile(startDir: string = process.cwd()): Promise<string | null> { let currentDir = startDir; const root = '/'; while (currentDir !== root) { const configPath = join(currentDir, 'config.toml'); try { await access(configPath); return configPath; } catch { // File doesn't exist, try parent directory currentDir = dirname(currentDir); } } return null; } /** * Parse TOML configuration file */ export async function parseConfig(configPath: string): Promise<DevpipeConfig> { try { const content = await readFile(configPath, 'utf-8'); const parsed = TOML.parse(content) as unknown as DevpipeConfig; return parsed; } catch (error) { throw new Error(`Failed to parse config file: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get the output directory path */ export function getOutputDir(configPath: string, config?: DevpipeConfig): string { const baseDir = dirname(configPath); const outputRoot = config?.defaults?.outputRoot || '.devpipe'; return join(baseDir, outputRoot); } /** * Get the most recent run directory */ export async function getLastRunDir(outputDir: string): Promise<string | null> { try { const runsDir = join(outputDir, 'runs'); const entries = await readdir(runsDir); // Filter for directories and sort by name (timestamp-based) const runDirs = []; for (const entry of entries) { const fullPath = join(runsDir, entry); const stats = await stat(fullPath); if (stats.isDirectory()) { runDirs.push({ name: entry, path: fullPath }); } } if (runDirs.length === 0) { return null; } // Sort descending (most recent first) runDirs.sort((a, b) => b.name.localeCompare(a.name)); return runDirs[0].path; } catch (error) { return null; } } /** * Read summary.json file */ export async function readSummary(outputDir: string): Promise<SummaryData | null> { try { const summaryPath = join(outputDir, 'summary.json'); const content = await readFile(summaryPath, 'utf-8'); return JSON.parse(content); } catch (error) { return null; } } /** * Read run.json metadata from a specific run */ export async function readRunMetadata(runDir: string): Promise<RunMetadata | null> { try { const runJsonPath = join(runDir, 'run.json'); const content = await readFile(runJsonPath, 'utf-8'); return JSON.parse(content); } catch (error) { return null; } } /** * Read task log file */ export async function readTaskLog(runDir: string, taskId: string): Promise<string | null> { try { const logPath = join(runDir, 'logs', `${taskId}.log`); return await readFile(logPath, 'utf-8'); } catch (error) { return null; } } /** * Read pipeline.log file */ export async function readPipelineLog(runDir: string): Promise<string | null> { try { const logPath = join(runDir, 'pipeline.log'); return await readFile(logPath, 'utf-8'); } catch (error) { return null; } } /** * Parse JUnit XML metrics */ export async function parseJUnitMetrics(metricsPath: string): Promise<any> { try { const content = await readFile(metricsPath, 'utf-8'); // Simple parsing - in production you'd use a proper XML parser return { raw: content, note: 'JUnit parsing requires XML parser' }; } catch (error) { throw new Error(`Failed to read JUnit metrics: ${error instanceof Error ? error.message : String(error)}`); } } /** * Parse SARIF JSON metrics */ export async function parseSARIFMetrics(metricsPath: string): Promise<any> { try { const content = await readFile(metricsPath, 'utf-8'); return JSON.parse(content); } catch (error) { throw new Error(`Failed to parse SARIF metrics: ${error instanceof Error ? error.message : String(error)}`); } } /** * Build devpipe command with arguments */ export function buildDevpipeCommand(args: { config?: string; only?: string[]; skip?: string[]; since?: string; fixType?: string; ui?: string; dashboard?: boolean; failFast?: boolean; fast?: boolean; dryRun?: boolean; verbose?: boolean; noColor?: boolean; }): string { const parts = ['devpipe']; if (args.config) parts.push(`--config "${args.config}"`); if (args.only && args.only.length > 0) { args.only.forEach(task => parts.push(`--only ${task}`)); } if (args.skip && args.skip.length > 0) { args.skip.forEach(task => parts.push(`--skip ${task}`)); } if (args.since) parts.push(`--since ${args.since}`); if (args.fixType) parts.push(`--fix-type ${args.fixType}`); if (args.ui) parts.push(`--ui ${args.ui}`); if (args.dashboard) parts.push('--dashboard'); if (args.failFast) parts.push('--fail-fast'); if (args.fast) parts.push('--fast'); if (args.dryRun) parts.push('--dry-run'); if (args.verbose) parts.push('--verbose'); if (args.noColor) parts.push('--no-color'); return parts.join(' '); } /** * Execute devpipe command */ export async function executeDevpipe(command: string, cwd?: string): Promise<{ stdout: string; stderr: string; exitCode: number }> { try { const { stdout, stderr } = await execAsync(command, { cwd: cwd || process.cwd(), maxBuffer: 10 * 1024 * 1024 // 10MB buffer }); return { stdout, stderr, exitCode: 0 }; } catch (error: any) { return { stdout: error.stdout || '', stderr: error.stderr || error.message || '', exitCode: error.code || 1 }; } } /** * List all available tasks from config */ export function listTasksFromConfig(config: DevpipeConfig): Array<{ id: string; name: string; description: string; type: string; command: string; enabled: boolean; isPhaseHeader: boolean; }> { const tasks = []; for (const [taskId, task] of Object.entries(config.tasks)) { const isPhaseHeader = taskId.startsWith('phase-'); tasks.push({ id: taskId, name: task.name || taskId, description: task.desc || '', type: task.type || 'check', command: task.command || '', enabled: task.enabled !== false, isPhaseHeader }); } return tasks; } /** * List tasks using devpipe list --verbose command */ export async function listTasksVerbose(configPath?: string): Promise<{ stdout: string; parsed: any }> { const command = configPath ? `devpipe list --verbose --config "${configPath}"` : 'devpipe list --verbose'; const result = await executeDevpipe(command); return { stdout: result.stdout, parsed: { raw: result.stdout, exitCode: result.exitCode, note: 'Parse the table format output for structured data' } }; } /** * Analyze project directory to detect technologies and suggest tasks */ export async function analyzeProject(projectPath: string = process.cwd()): Promise<{ detectedTechnologies: string[]; suggestedTasks: Array<{ technology: string; taskType: string; reason: string }>; existingFiles: { [key: string]: boolean }; }> { const detectedTechnologies: string[] = []; const suggestedTasks: Array<{ technology: string; taskType: string; reason: string }> = []; const existingFiles: { [key: string]: boolean } = {}; try { const files = await readdir(projectPath); // Check for various technology indicators for (const file of files) { existingFiles[file] = true; } // Go detection if (existingFiles['go.mod'] || existingFiles['go.sum']) { detectedTechnologies.push('Go'); suggestedTasks.push( { technology: 'Go', taskType: 'check-format', reason: 'go fmt for formatting' }, { technology: 'Go', taskType: 'check-lint', reason: 'golangci-lint for linting' }, { technology: 'Go', taskType: 'check-static', reason: 'go vet for static analysis' }, { technology: 'Go', taskType: 'test-unit', reason: 'go test for unit tests' }, { technology: 'Go', taskType: 'build', reason: 'go build for compilation' } ); } // Python detection if (existingFiles['requirements.txt'] || existingFiles['pyproject.toml'] || existingFiles['setup.py']) { detectedTechnologies.push('Python'); suggestedTasks.push( { technology: 'Python', taskType: 'check-format', reason: 'black or ruff for formatting' }, { technology: 'Python', taskType: 'check-lint', reason: 'pylint or ruff for linting' }, { technology: 'Python', taskType: 'check-types', reason: 'mypy for type checking' }, { technology: 'Python', taskType: 'test-unit', reason: 'pytest for unit tests' } ); } // Node.js/TypeScript detection if (existingFiles['package.json']) { detectedTechnologies.push('Node.js'); suggestedTasks.push( { technology: 'Node.js', taskType: 'check-lint', reason: 'eslint for linting' }, { technology: 'Node.js', taskType: 'test-unit', reason: 'npm test or jest' }, { technology: 'Node.js', taskType: 'build', reason: 'npm run build' } ); } if (existingFiles['tsconfig.json']) { detectedTechnologies.push('TypeScript'); suggestedTasks.push( { technology: 'TypeScript', taskType: 'check-types', reason: 'tsc for type checking' } ); } // Rust detection if (existingFiles['Cargo.toml']) { detectedTechnologies.push('Rust'); suggestedTasks.push( { technology: 'Rust', taskType: 'check-format', reason: 'cargo fmt for formatting' }, { technology: 'Rust', taskType: 'check-lint', reason: 'cargo clippy for linting' }, { technology: 'Rust', taskType: 'test-unit', reason: 'cargo test for tests' }, { technology: 'Rust', taskType: 'build', reason: 'cargo build' } ); } // Docker detection if (existingFiles['Dockerfile'] || existingFiles['docker-compose.yml']) { detectedTechnologies.push('Docker'); suggestedTasks.push( { technology: 'Docker', taskType: 'check-lint', reason: 'hadolint for Dockerfile linting' } ); } // Makefile detection if (existingFiles['Makefile']) { detectedTechnologies.push('Make'); } return { detectedTechnologies, suggestedTasks, existingFiles }; } catch (error) { throw new Error(`Failed to analyze project: ${error instanceof Error ? error.message : String(error)}`); } } /** * Generate a phase header task * Note: Phase headers have NO required fields - they're just organizational markers * Common practice: include name OR desc, but neither is required */ export function generatePhaseHeader(phaseName?: string, description?: string): string { const phaseId = phaseName ? `phase-${phaseName.toLowerCase()}` : 'phase-unnamed'; let toml = `[tasks.${phaseId}]\n`; if (phaseName) { toml += `name = "${phaseName}"\n`; } if (description) { toml += `desc = "${description}"\n`; } return toml; } /** * Generate task configuration from template */ export function generateTaskConfig(technology: string, taskType: string, taskId?: string): string { const id = taskId || `${technology.toLowerCase()}-${taskType}`; // Special handling for phase headers if (technology.toLowerCase() === 'phase') { return generatePhaseHeader(taskType, taskId); } const templates: { [key: string]: { [key: string]: any } } = { 'Go': { 'check-format': { name: 'Go Format', desc: 'Verifies that Go code is properly formatted', type: 'check', command: 'gofmt -l .', fixType: 'helper', fixCommand: 'gofmt -w .' }, 'check-lint': { name: 'Golang CI Lint', desc: 'Runs comprehensive linting on Go code', type: 'check', command: 'golangci-lint run', fixType: 'auto', fixCommand: 'golangci-lint run --fix' }, 'check-static': { name: 'Go Vet', desc: 'Examines Go code for suspicious constructs', type: 'check', command: 'go vet ./...' }, 'test-unit': { name: 'Unit Tests', desc: 'Run all unit tests', type: 'test', command: 'go test -v ./...', metricsFormat: 'junit', metricsPath: 'test-results.xml' }, 'build': { name: 'Build Binary', desc: 'Compile Go application', type: 'build', command: 'go build -o bin/app .' } }, 'Python': { 'check-format': { name: 'Python Format Check', desc: 'Check Python code formatting with black', type: 'check', command: 'black --check .', fixType: 'auto', fixCommand: 'black .' }, 'check-lint': { name: 'Python Lint', desc: 'Lint Python code with ruff', type: 'check', command: 'ruff check .', fixType: 'auto', fixCommand: 'ruff check --fix .' }, 'check-types': { name: 'Type Check', desc: 'Check types with mypy', type: 'check', command: 'mypy .' }, 'test-unit': { name: 'Unit Tests', desc: 'Run pytest unit tests', type: 'test', command: 'pytest', metricsFormat: 'junit', metricsPath: 'test-results.xml' } }, 'Node.js': { 'check-lint': { name: 'ESLint', desc: 'Lint JavaScript/TypeScript with ESLint', type: 'check', command: 'npm run lint', fixType: 'auto', fixCommand: 'npm run lint -- --fix' }, 'test-unit': { name: 'Unit Tests', desc: 'Run unit tests', type: 'test', command: 'npm test' }, 'build': { name: 'Build', desc: 'Build the project', type: 'build', command: 'npm run build' } }, 'TypeScript': { 'check-types': { name: 'Type Check', desc: 'Check TypeScript types', type: 'check', command: 'tsc --noEmit' } } }; const techTemplates = templates[technology]; if (!techTemplates) { return `# No template available for ${technology}\n# Please create a custom task`; } const template = techTemplates[taskType]; if (!template) { return `# No template available for ${technology} ${taskType}\n# Available types: ${Object.keys(techTemplates).join(', ')}`; } // Generate TOML let toml = `[tasks.${id}]\n`; toml += `name = "${template.name}"\n`; toml += `desc = "${template.desc}"\n`; toml += `type = "${template.type}"\n`; toml += `command = "${template.command}"\n`; if (template.fixType) { toml += `fixType = "${template.fixType}"\n`; } if (template.fixCommand) { toml += `fixCommand = "${template.fixCommand}"\n`; } if (template.metricsFormat) { toml += `metricsFormat = "${template.metricsFormat}"\n`; } if (template.metricsPath) { toml += `metricsPath = "${template.metricsPath}"\n`; } return toml; } /** * Create a complete config.toml file from scratch */ export async function createConfig(projectPath: string = process.cwd(), options?: { includeDefaults?: boolean; autoDetect?: boolean; }): Promise<string> { const includeDefaults = options?.includeDefaults !== false; const autoDetect = options?.autoDetect !== false; let config = ''; // Add defaults section if (includeDefaults) { config += `# Devpipe Configuration # https://github.com/drewkhoury/devpipe [defaults] outputRoot = ".devpipe" fastThreshold = 300 # Tasks over 300s are skipped with --fast uiMode = "basic" # Options: basic, full animationRefreshMs = 500 animatedGroupBy = "phase" # Options: phase, type [defaults.git] mode = "staged_unstaged" # Options: staged, staged_unstaged, ref # ref = "HEAD" # Uncomment to compare against a specific ref [task_defaults] enabled = true workdir = "." # fixType = "helper" # Options: auto, helper, none `; } // Auto-detect and add tasks if (autoDetect) { const analysis = await analyzeProject(projectPath); if (analysis.detectedTechnologies.length > 0) { config += `# Detected technologies: ${analysis.detectedTechnologies.join(', ')}\n\n`; // Group tasks by phase const phases = new Map<string, Array<{ technology: string; taskType: string; reason: string }>>(); for (const task of analysis.suggestedTasks) { let phase = 'validate'; if (task.taskType.includes('build')) phase = 'build'; else if (task.taskType.includes('test')) phase = 'test'; if (!phases.has(phase)) phases.set(phase, []); phases.get(phase)!.push(task); } // Add phase headers and tasks for (const [phase, tasks] of phases) { const phaseName = phase.charAt(0).toUpperCase() + phase.slice(1); config += `# ${phaseName} Phase\n`; config += `[tasks.phase-${phase}]\n`; config += `name = "${phaseName}"\n`; config += `desc = "Tasks for ${phase} stage"\n\n`; for (const task of tasks) { const taskId = `${task.technology.toLowerCase().replace(/\./g, '-')}-${task.taskType}`; const taskConfig = generateTaskConfig(task.technology, task.taskType, taskId); config += taskConfig + '\n'; } } } else { // No technologies detected, add example tasks config += `# Example tasks - customize for your project\n\n`; config += `[tasks.example-check]\n`; config += `name = "Example Check"\n`; config += `desc = "Replace with your actual check command"\n`; config += `type = "check"\n`; config += `command = "echo 'Add your check command here'"\n\n`; } } return config; } /** * Generate CI/CD configuration from devpipe config */ export function generateCIConfig(config: DevpipeConfig, platform: 'github' | 'gitlab'): string { const tasks = listTasksFromConfig(config); const enabledTasks = tasks.filter(t => t.enabled && !t.isPhaseHeader); if (platform === 'github') { return `name: CI Pipeline on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: devpipe: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install devpipe run: | curl -L https://github.com/drewkhoury/devpipe/releases/latest/download/devpipe-linux-amd64 -o devpipe chmod +x devpipe sudo mv devpipe /usr/local/bin/ - name: Run devpipe run: devpipe --fail-fast - name: Upload results if: always() uses: actions/upload-artifact@v4 with: name: devpipe-results path: .devpipe/ `; } else if (platform === 'gitlab') { return `stages: - validate - test - build devpipe: stage: test image: golang:latest before_script: - curl -L https://github.com/drewkhoury/devpipe/releases/latest/download/devpipe-linux-amd64 -o devpipe - chmod +x devpipe - mv devpipe /usr/local/bin/ script: - devpipe --fail-fast artifacts: when: always paths: - .devpipe/ reports: junit: .devpipe/runs/*/metrics/*.xml `; } return ''; }

Implementation Reference

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/drewkhoury/devpipe-mcp'

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