refactor: update ComposeService and DockerController for improved path handling and environment variable management; enhance EditorLayout for dynamic button rendering based on stack state

This commit is contained in:
SaelixCode
2026-02-20 22:20:34 -05:00
parent 888283d515
commit ad8c663629
4 changed files with 93 additions and 36 deletions
+1
View File
@@ -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';
+13 -4
View File
@@ -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<void>((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<void>((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) => {
+49 -12
View File
@@ -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) {
+30 -20
View File
@@ -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() {
<div className="flex-1 overflow-y-auto p-6">
{!isLoading && selectedFile ? (
<ErrorBoundary>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-stretch">
{/* Left Column (Command Center & Terminal) */}
<div className="flex flex-col gap-6">
<div className="flex flex-col h-full gap-6">
{/* Command Center Card */}
<Card className="rounded-xl border-muted bg-card">
<CardHeader className="p-4 pb-2">
@@ -558,22 +562,28 @@ export default function EditorLayout() {
<CardTitle className="text-2xl font-bold">{stackName}</CardTitle>
{/* Action Bar */}
<div className="flex items-center gap-2 flex-wrap">
<Button size="sm" className="rounded-lg" onClick={deployStack}>
<Play className="w-4 h-4 mr-2" />
Deploy
</Button>
<Button size="sm" variant="outline" className="rounded-lg" onClick={restartStack}>
<RotateCw className="w-4 h-4 mr-2" />
Restart
</Button>
<Button size="sm" variant="outline" className="rounded-lg" onClick={updateStack}>
<CloudDownload className="w-4 h-4 mr-2" />
Update
</Button>
<Button size="sm" variant="outline" className="rounded-lg" onClick={stopStack}>
<Square className="w-4 h-4 mr-2" />
Stop
</Button>
{!isDeployed && (
<Button size="sm" className="rounded-lg" onClick={deployStack}>
<Play className="w-4 h-4 mr-2" />
Deploy
</Button>
)}
{isDeployed && (
<>
<Button size="sm" variant="outline" className="rounded-lg" onClick={restartStack}>
<RotateCw className="w-4 h-4 mr-2" />
Restart
</Button>
<Button size="sm" variant="outline" className="rounded-lg" onClick={updateStack}>
<CloudDownload className="w-4 h-4 mr-2" />
Update
</Button>
<Button size="sm" variant="outline" className="rounded-lg" onClick={stopStack}>
<Square className="w-4 h-4 mr-2" />
{isRunning ? 'Stop' : 'Down'}
</Button>
</>
)}
<Button
size="sm"
variant="destructive"
@@ -660,7 +670,7 @@ export default function EditorLayout() {
{/* Terminal Section */}
{showConsole && (
<div className="rounded-xl overflow-hidden border border-muted bg-black p-3 h-[400px]">
<div className="flex-1 rounded-xl overflow-hidden border border-muted bg-black p-3 min-h-[300px]">
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Terminal</h3>
<div className="h-[calc(100%-24px)]">
<ErrorBoundary>
@@ -672,7 +682,7 @@ export default function EditorLayout() {
</div>
{/* Right Column (The Editor) */}
<Card className="rounded-xl border-muted overflow-hidden flex flex-col h-[700px] bg-card">
<Card className="rounded-xl border-muted overflow-hidden flex flex-col h-full min-h-[600px] bg-card">
<div className="p-4 border-b border-muted flex items-center justify-between">
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as 'compose' | 'env')}>
<TabsList className="bg-muted">