import type { ContainerInfo } from './docker.service'; /** * Abstraction layer for instance operations. * LocalDriver wraps docker.service.ts + filesystem. * RemoteDriver makes HTTPS calls to the remote agent. */ export interface ExecutionDriver { // ─── Docker Compose Operations ────────────────────────────── composeUp(projectDir: string, project: string, services?: string[]): Promise; composeDown(projectDir: string, project: string, removeVolumes?: boolean): Promise; composeStop(projectDir: string, project: string): Promise; composeRestart(projectDir: string, project: string, service?: string): Promise; composePull(projectDir: string, project: string): Promise; composeBuild(projectDir: string, project: string): Promise; composePs(projectDir: string, project: string): Promise; composeLogs(projectDir: string, project: string, service?: string, tail?: number, since?: string): Promise; composeExec(projectDir: string, project: string, service: string, command: string, timeoutMs?: number, envVars?: Record): Promise; // ─── Container Health ─────────────────────────────────────── waitForHealthy(containerName: string, timeoutMs?: number, pollIntervalMs?: number): Promise; waitForHttp(url: string, timeoutMs?: number, pollIntervalMs?: number): Promise; // ─── Filesystem Operations ────────────────────────────────── readEnvFile(basePath: string): Promise | null>; writeFiles(basePath: string, files: Array<{ relativePath: string; content: string }>): Promise; // Approach C pre-flight: diff proposed file contents against on-disk current. // Returns per-file status (unchanged | modified | created) + unified diff for modified. // Read-only. diffFiles( basePath: string, files: Array<{ relativePath: string; content: string }> ): Promise>; mkdir(basePath: string, relativePath: string): Promise; fileExists(basePath: string, relativePath: string): Promise; deleteDirectory(dirPath: string): Promise; cloneSource(basePath: string, gitRepo: string, gitBranch: string, excludes?: string[]): Promise; } /** * Error thrown when a remote agent is unreachable. */ export class AgentUnreachableError extends Error { constructor(public agentUrl: string, cause?: Error) { super(`Remote agent at ${agentUrl} is not reachable`); this.name = 'AgentUnreachableError'; if (cause) this.cause = cause; } } /** * Minimal instance shape needed to resolve a driver. */ export interface DriverInstance { id: string; slug: string; isRemote: boolean; agentUrl: string | null; } /** * Resolve the correct execution driver for an instance. * Returns LocalDriver for local instances, RemoteDriver for remote ones. */ export async function getDriverForInstance(instance: DriverInstance): Promise { if (!instance.isRemote) { const { getLocalDriver } = await import('./local-driver'); return getLocalDriver(); } return getRemoteDriverForInstance(instance); } /** * Resolve a RemoteDriver for a remote instance. Throws if the instance is * local, missing an agent URL, or has no valid mTLS certificate. * * Use this when you need to call RemoteDriver-specific methods like * createBackup() that don't exist on the ExecutionDriver interface. */ export async function getRemoteDriverForInstance(instance: DriverInstance) { if (!instance.isRemote) { throw new Error(`Instance ${instance.slug} is not remote`); } if (!instance.agentUrl) { throw new Error(`Remote instance ${instance.slug} has no agent URL configured`); } const { getAgentClientMaterials } = await import('./certificate.service'); const materials = await getAgentClientMaterials(instance.id); if (!materials) { throw new Error(`No valid certificate found for remote instance ${instance.slug}`); } const { RemoteDriver } = await import('./remote-driver'); return new RemoteDriver( instance.agentUrl, instance.slug, Buffer.from(materials.agentCertPem), Buffer.from(materials.agentKeyPem), Buffer.from(materials.caCertPem) ); }