feat(phase-2): Theia container bridge via docker exec
- Dockerfile: install docker-ce-cli, run as root for socket access - theia-exec.ts: container discovery (env > label > name), theiaExec(), syncToTheia() via docker cp - agent-session-runner: execute_command → docker exec into Theia (fallback to local) - agent-session-runner: syncToTheia() before auto-commit so "Open in Theia" shows agent files immediately after session completes - server.ts: compute theiaWorkspaceSubdir from giteaRepo slug, log bridge status at startup - custom_docker_run_options already set to mount /var/run/docker.sock Made-with: Cursor
This commit is contained in:
143
src/theia-exec.ts
Normal file
143
src/theia-exec.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* theia-exec.ts
|
||||
*
|
||||
* Bridge between vibn-agent-runner and the Theia container.
|
||||
*
|
||||
* The agent runner and Theia run as separate Docker containers on the same
|
||||
* host. With /var/run/docker.sock mounted in the runner, this module:
|
||||
*
|
||||
* 1. Discovers the Theia container at startup (by label or explicit name)
|
||||
* 2. theiaExec(cmd, workdir) — runs a command inside Theia via docker exec
|
||||
* 3. syncToTheia(localDir, theiaWorkspacePath) — copies files from the
|
||||
* runner's local workspace into Theia so "Open in Theia" shows them
|
||||
*
|
||||
* Usage:
|
||||
* import { theiaExec, syncToTheia, getTheiaContainer } from './theia-exec';
|
||||
*
|
||||
* Environment variables:
|
||||
* THEIA_CONTAINER — explicit container name/id (fastest)
|
||||
* THEIA_COOLIFY_UUID — Coolify app UUID to search by label (fallback)
|
||||
* default: b8wswk0ggcg88k0c8osk4c4c
|
||||
* THEIA_WORKSPACE — workspace path inside Theia (default: /home/node/workspace)
|
||||
*/
|
||||
|
||||
import { execSync, execFileSync } from 'child_process';
|
||||
|
||||
const THEIA_WORKSPACE = process.env.THEIA_WORKSPACE ?? '/home/node/workspace';
|
||||
const THEIA_COOLIFY_UUID = process.env.THEIA_COOLIFY_UUID ?? 'b8wswk0ggcg88k0c8osk4c4c';
|
||||
|
||||
let _cachedContainer: string | null = null;
|
||||
|
||||
/**
|
||||
* Find the running Theia container. Returns the container name/id or null.
|
||||
* Result is cached after first successful lookup.
|
||||
*/
|
||||
export function getTheiaContainer(): string | null {
|
||||
if (_cachedContainer) return _cachedContainer;
|
||||
|
||||
// 1. Explicit env var — fastest
|
||||
if (process.env.THEIA_CONTAINER) {
|
||||
_cachedContainer = process.env.THEIA_CONTAINER.trim();
|
||||
return _cachedContainer;
|
||||
}
|
||||
|
||||
// 2. Search by Coolify application UUID label
|
||||
try {
|
||||
const byAppId = execSync(
|
||||
`docker ps --filter "label=coolify.applicationId=${THEIA_COOLIFY_UUID}" --format "{{.Names}}" 2>/dev/null`,
|
||||
{ timeout: 5000 }
|
||||
).toString().trim();
|
||||
if (byAppId) { _cachedContainer = byAppId.split('\n')[0].trim(); return _cachedContainer; }
|
||||
} catch { /* docker not available or socket not mounted */ }
|
||||
|
||||
// 3. Search by partial name match
|
||||
try {
|
||||
const byName = execSync(
|
||||
`docker ps --filter "name=theia" --format "{{.Names}}" 2>/dev/null`,
|
||||
{ timeout: 5000 }
|
||||
).toString().trim();
|
||||
if (byName) { _cachedContainer = byName.split('\n')[0].trim(); return _cachedContainer; }
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns true if docker exec into Theia is available */
|
||||
export function isTheiaAvailable(): boolean {
|
||||
return getTheiaContainer() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a shell command inside the Theia container.
|
||||
* Returns { exitCode, stdout, stderr }.
|
||||
*/
|
||||
export async function theiaExec(
|
||||
command: string,
|
||||
workdir?: string
|
||||
): Promise<{ exitCode: number; stdout: string; stderr: string; error?: string }> {
|
||||
const container = getTheiaContainer();
|
||||
if (!container) {
|
||||
return { exitCode: 1, stdout: '', stderr: '', error: 'Theia container not available — running locally' };
|
||||
}
|
||||
|
||||
const cwd = workdir ?? THEIA_WORKSPACE;
|
||||
// Wrap in bash so the command runs in a shell with proper cd
|
||||
const wrapped = `cd ${JSON.stringify(cwd)} && ${command}`;
|
||||
|
||||
return new Promise(resolve => {
|
||||
try {
|
||||
const { execFile } = require('child_process') as typeof import('child_process');
|
||||
execFile(
|
||||
'docker', ['exec', container, 'bash', '-c', wrapped],
|
||||
{ timeout: 120_000, maxBuffer: 4 * 1024 * 1024 },
|
||||
(err, stdout, stderr) => {
|
||||
resolve({
|
||||
exitCode: typeof err?.code === 'number' ? err.code : 0,
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
error: err ? err.message : undefined,
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (err: any) {
|
||||
resolve({ exitCode: 1, stdout: '', stderr: '', error: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy files from the runner's local workspace directory into Theia's workspace.
|
||||
* Creates the destination directory if it doesn't exist.
|
||||
*
|
||||
* @param localDir Absolute path in the runner container, e.g. /workspaces/mark_sportsy
|
||||
* @param theiaPath Relative path inside THEIA_WORKSPACE, e.g. "mark_sportsy"
|
||||
* or pass null to copy into the workspace root
|
||||
*/
|
||||
export async function syncToTheia(
|
||||
localDir: string,
|
||||
theiaPath: string | null
|
||||
): Promise<{ ok: boolean; error?: string }> {
|
||||
const container = getTheiaContainer();
|
||||
if (!container) {
|
||||
return { ok: false, error: 'Theia container not available — sync skipped' };
|
||||
}
|
||||
|
||||
const destDir = theiaPath
|
||||
? `${THEIA_WORKSPACE}/${theiaPath}`
|
||||
: THEIA_WORKSPACE;
|
||||
|
||||
try {
|
||||
// Ensure destination directory exists
|
||||
execSync(`docker exec ${container} mkdir -p ${JSON.stringify(destDir)}`, { timeout: 10_000 });
|
||||
|
||||
// docker cp copies the contents of localDir into destDir
|
||||
// Trailing /. means "copy contents, not the directory itself"
|
||||
execSync(`docker cp "${localDir}/." "${container}:${destDir}/"`, { timeout: 60_000 });
|
||||
|
||||
console.log(`[theia-exec] Synced ${localDir} → ${container}:${destDir}`);
|
||||
return { ok: true };
|
||||
} catch (err: any) {
|
||||
console.error(`[theia-exec] Sync failed:`, err.message);
|
||||
return { ok: false, error: err.message };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user