From ad8c6636297f15a7304f44924b9d40617f22fca4 Mon Sep 17 00:00:00 2001 From: SaelixCode Date: Fri, 20 Feb 2026 22:20:34 -0500 Subject: [PATCH] refactor: update ComposeService and DockerController for improved path handling and environment variable management; enhance EditorLayout for dynamic button rendering based on stack state --- backend/src/index.ts | 1 + backend/src/services/ComposeService.ts | 17 +++++-- backend/src/services/DockerController.ts | 61 +++++++++++++++++++----- frontend/src/components/EditorLayout.tsx | 50 +++++++++++-------- 4 files changed, 93 insertions(+), 36 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index a30e66ed..d3b21b0b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -7,6 +7,7 @@ import DockerController from './services/DockerController'; import { FileSystemService } from './services/FileSystemService'; import { ComposeService } from './services/ComposeService'; import { ConfigService } from './services/ConfigService'; +// @ts-ignore - composerize lacks proper type definitions import composerize from 'composerize'; import si from 'systeminformation'; import http from 'http'; diff --git a/backend/src/services/ComposeService.ts b/backend/src/services/ComposeService.ts index 4d9c70b9..594b0cf5 100644 --- a/backend/src/services/ComposeService.ts +++ b/backend/src/services/ComposeService.ts @@ -6,7 +6,7 @@ export class ComposeService { private baseDir: string; constructor() { - this.baseDir = process.env.COMPOSE_DIR || path.join(process.cwd(), '..', 'mock_data', 'docker', 'compose'); + this.baseDir = process.env.COMPOSE_DIR || path.join(process.cwd(), '..', 'docker', 'compose'); } /** @@ -25,7 +25,10 @@ export class ComposeService { const child = spawn('docker', args, { cwd: stackDir, // CRITICAL: Set working directory to stack folder - shell: true + env: { + ...process.env, + PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + } }); if (ws) { @@ -65,7 +68,10 @@ export class ComposeService { await new Promise((resolve, reject) => { const pullProcess = spawn('docker', ['compose', 'pull'], { cwd: stackDir, - shell: true + env: { + ...process.env, + PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + } }); pullProcess.stdout.on('data', (data: Buffer) => { @@ -97,7 +103,10 @@ export class ComposeService { await new Promise((resolve, reject) => { const upProcess = spawn('docker', ['compose', 'up', '-d'], { cwd: stackDir, - shell: true + env: { + ...process.env, + PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + } }); upProcess.stdout.on('data', (data: Buffer) => { diff --git a/backend/src/services/DockerController.ts b/backend/src/services/DockerController.ts index 0e7215f0..e788f1a0 100644 --- a/backend/src/services/DockerController.ts +++ b/backend/src/services/DockerController.ts @@ -1,6 +1,12 @@ import Docker from 'dockerode'; import WebSocket from 'ws'; import { Duplex } from 'stream'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import path from 'path'; + +const execAsync = promisify(exec); +const COMPOSE_DIR = process.env.COMPOSE_DIR || '/app/compose'; class DockerController { private static instance: DockerController; @@ -30,19 +36,50 @@ class DockerController { } public async getContainersByStack(stackName: string) { - const containers = await this.docker.listContainers({ all: true }); - // Normalize the stack name: remove all non-alphanumeric characters and lowercase - // Docker Compose strips hyphens and underscores from project names - const normalizedStackName = stackName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); - - return containers.filter(container => { - if (!container.Labels || !container.Labels['com.docker.compose.project']) { - return false; + try { + const stackDir = path.join(COMPOSE_DIR, stackName); + const { stdout } = await execAsync('docker compose ps --format json -a', { + cwd: stackDir, + env: { + ...process.env, + PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + } + }); + + // Robust JSON parsing - handle both JSON array and newline-separated JSON objects + // Docker Compose v2 may return either format depending on version + interface ComposeContainer { + ID?: string; + Name?: string; + State?: string; + Status?: string; } - // Normalize the Docker label for comparison - const projectLabel = container.Labels['com.docker.compose.project'].replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); - return projectLabel === normalizedStackName; - }); + + let containers: ComposeContainer[]; + try { + // Try parsing as a standard JSON array + const parsed = JSON.parse(stdout); + containers = Array.isArray(parsed) ? parsed : [parsed]; + } catch { + // Fallback: parse newline-separated JSON objects + const lines = stdout.trim().split('\n'); + containers = lines.map(line => JSON.parse(line) as ComposeContainer); + } + + // Map to frontend's expected interface + // Note: docker compose ps returns Name (singular), but frontend expects Names (array) + // Dockerode returns Names with leading slash, so we add it for compatibility + return containers.map((c) => ({ + Id: c.ID || '', + Names: ['/' + (c.Name || '')], // Add leading slash to match Dockerode format + State: c.State || 'unknown', + Status: c.Status || '' + })); + } catch (error) { + // If command fails (e.g., stack not deployed), return empty array + console.error('Failed to get containers for stack:', stackName, error); + return []; + } } public async startContainer(containerId: string) { diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 73a77028..44d384bf 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -388,6 +388,10 @@ export default function EditorLayout() { // Safe content strings with fallback const safeContent = content || ''; const safeEnvContent = envContent || ''; + + // Stack state booleans for dynamic button rendering + const isDeployed = safeContainers && safeContainers.length > 0; + const isRunning = safeContainers?.some(c => c.State === 'running'); // Stack name is now the same as selectedFile (no extension to strip) const stackName = selectedFile || ''; @@ -547,9 +551,9 @@ export default function EditorLayout() {
{!isLoading && selectedFile ? ( -
+
{/* Left Column (Command Center & Terminal) */} -
+
{/* Command Center Card */} @@ -558,22 +562,28 @@ export default function EditorLayout() { {stackName} {/* Action Bar */}
- - - - + {!isDeployed && ( + + )} + {isDeployed && ( + <> + + + + + )}