import { exec } from 'child_process';
import { promisify } from 'util';
import { readFile } from 'fs/promises';
import { join, resolve } from 'path';
import * as yaml from 'js-yaml';
const execAsync = promisify(exec);
export interface ComposeUpOptions {
build?: boolean;
detach?: boolean;
forceRecreate?: boolean;
noBuild?: boolean;
noDeps?: boolean;
pull?: 'always' | 'missing' | 'never';
removeOrphans?: boolean;
scale?: Record<string, number>;
services?: string[];
}
export interface ComposeDownOptions {
removeOrphans?: boolean;
volumes?: boolean;
images?: 'all' | 'local';
timeout?: number;
}
export interface ComposeLogsOptions {
follow?: boolean;
tail?: number;
since?: string;
until?: string;
timestamps?: boolean;
services?: string[];
}
export class ComposeManager {
async composeUp(projectDir: string, options: ComposeUpOptions = {}): Promise<string> {
const args: string[] = ['up'];
if (options.build) args.push('--build');
if (options.detach) args.push('-d');
if (options.forceRecreate) args.push('--force-recreate');
if (options.noBuild) args.push('--no-build');
if (options.noDeps) args.push('--no-deps');
if (options.pull) args.push('--pull', options.pull);
if (options.removeOrphans) args.push('--remove-orphans');
if (options.scale) {
for (const [service, count] of Object.entries(options.scale)) {
args.push('--scale', `${service}=${count}`);
}
}
if (options.services) {
args.push(...options.services);
}
const command = `docker compose ${args.join(' ')}`;
const { stdout, stderr } = await execAsync(command, {
cwd: resolve(projectDir),
env: process.env,
});
if (stderr && !stderr.includes('Creating') && !stderr.includes('Starting')) {
throw new Error(`Docker Compose up failed: ${stderr}`);
}
return stdout || stderr || 'Compose services started successfully';
}
async composeDown(projectDir: string, options: ComposeDownOptions = {}): Promise<string> {
const args: string[] = ['down'];
if (options.removeOrphans) args.push('--remove-orphans');
if (options.volumes) args.push('--volumes');
if (options.images) args.push('--rmi', options.images);
if (options.timeout) args.push('--timeout', options.timeout.toString());
const command = `docker compose ${args.join(' ')}`;
const { stdout, stderr } = await execAsync(command, {
cwd: resolve(projectDir),
env: process.env,
});
if (stderr && !stderr.includes('Stopping') && !stderr.includes('Removing')) {
throw new Error(`Docker Compose down failed: ${stderr}`);
}
return stdout || stderr || 'Compose services stopped successfully';
}
async composePs(projectDir: string, options: { all?: boolean; services?: string[] } = {}): Promise<string> {
const args: string[] = ['ps'];
if (options.all) args.push('--all');
if (options.services) {
args.push(...options.services);
}
const command = `docker compose ${args.join(' ')}`;
const { stdout, stderr } = await execAsync(command, {
cwd: resolve(projectDir),
env: process.env,
});
if (stderr && !stdout) {
throw new Error(`Docker Compose ps failed: ${stderr}`);
}
return stdout || 'No services running';
}
async composeLogs(
projectDir: string,
options: ComposeLogsOptions = {}
): Promise<string> {
const args: string[] = ['logs'];
if (options.follow) args.push('--follow');
if (options.tail) args.push('--tail', options.tail.toString());
if (options.since) args.push('--since', options.since);
if (options.until) args.push('--until', options.until);
if (options.timestamps) args.push('--timestamps');
if (options.services) {
args.push(...options.services);
}
const command = `docker compose ${args.join(' ')}`;
const { stdout, stderr } = await execAsync(command, {
cwd: resolve(projectDir),
env: process.env,
});
if (stderr && !stdout) {
throw new Error(`Docker Compose logs failed: ${stderr}`);
}
return stdout || stderr || 'No logs available';
}
async composeConfig(projectDir: string): Promise<any> {
try {
const composeFile = join(resolve(projectDir), 'docker-compose.yml');
const content = await readFile(composeFile, 'utf-8');
return yaml.load(content);
} catch (error: any) {
throw new Error(`Failed to read docker-compose.yml: ${error.message}`);
}
}
async composeBuild(projectDir: string, options: { services?: string[]; noCache?: boolean; pull?: boolean } = {}): Promise<string> {
const args: string[] = ['build'];
if (options.noCache) args.push('--no-cache');
if (options.pull) args.push('--pull');
if (options.services) {
args.push(...options.services);
}
const command = `docker compose ${args.join(' ')}`;
const { stdout, stderr } = await execAsync(command, {
cwd: resolve(projectDir),
env: process.env,
});
if (stderr && !stderr.includes('Building') && !stderr.includes('Successfully')) {
throw new Error(`Docker Compose build failed: ${stderr}`);
}
return stdout || stderr || 'Build completed successfully';
}
async composeRestart(projectDir: string, options: { services?: string[]; timeout?: number } = {}): Promise<string> {
const args: string[] = ['restart'];
if (options.timeout) args.push('--timeout', options.timeout.toString());
if (options.services) {
args.push(...options.services);
}
const command = `docker compose ${args.join(' ')}`;
const { stdout, stderr } = await execAsync(command, {
cwd: resolve(projectDir),
env: process.env,
});
if (stderr && !stderr.includes('Restarting')) {
throw new Error(`Docker Compose restart failed: ${stderr}`);
}
return stdout || stderr || 'Services restarted successfully';
}
async composeStop(projectDir: string, options: { services?: string[]; timeout?: number } = {}): Promise<string> {
const args: string[] = ['stop'];
if (options.timeout) args.push('--timeout', options.timeout.toString());
if (options.services) {
args.push(...options.services);
}
const command = `docker compose ${args.join(' ')}`;
const { stdout, stderr } = await execAsync(command, {
cwd: resolve(projectDir),
env: process.env,
});
if (stderr && !stderr.includes('Stopping')) {
throw new Error(`Docker Compose stop failed: ${stderr}`);
}
return stdout || stderr || 'Services stopped successfully';
}
async composeStart(projectDir: string, options: { services?: string[] } = {}): Promise<string> {
const args: string[] = ['start'];
if (options.services) {
args.push(...options.services);
}
const command = `docker compose ${args.join(' ')}`;
const { stdout, stderr } = await execAsync(command, {
cwd: resolve(projectDir),
env: process.env,
});
if (stderr && !stderr.includes('Starting')) {
throw new Error(`Docker Compose start failed: ${stderr}`);
}
return stdout || stderr || 'Services started successfully';
}
}