/** * POST /api/workspaces/[slug]/apps/[uuid]/exec * * Run a one-shot command inside an app container via `docker exec` * on the Coolify host (over SSH). The companion of `/logs` for the * write path. * * Body (JSON): * command string required. Passed through `sh -lc`. * service string? compose service to target (required when * the app has >1 container). * user string? docker exec --user * workdir string? docker exec --workdir * timeout_ms number? default 60_000, max 600_000 * max_bytes number? default 1_000_000, max 5_000_000 */ import { NextResponse } from 'next/server'; import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth'; import { getApplicationInProject, TenantError } from '@/lib/coolify'; import { execInCoolifyApp } from '@/lib/coolify-exec'; import { isCoolifySshConfigured } from '@/lib/coolify-ssh'; export async function POST( request: Request, { params }: { params: Promise<{ slug: string; uuid: string }> } ) { const { slug, uuid } = await params; const principal = await requireWorkspacePrincipal(request, { targetSlug: slug }); if (principal instanceof NextResponse) return principal; const ws = principal.workspace; if (!ws.coolify_project_uuid) { return NextResponse.json({ error: 'Workspace has no Coolify project yet' }, { status: 503 }); } if (!isCoolifySshConfigured()) { return NextResponse.json( { error: 'apps.exec requires SSH to the Coolify host, not configured on this deployment.' }, { status: 501 }, ); } let body: Record; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Body must be JSON' }, { status: 400 }); } const command = typeof body.command === 'string' ? body.command : ''; if (!command) { return NextResponse.json({ error: 'command is required' }, { status: 400 }); } const service = typeof body.service === 'string' && body.service.trim() ? body.service.trim() : undefined; const user = typeof body.user === 'string' && body.user.trim() ? body.user.trim() : undefined; const workdir = typeof body.workdir === 'string' && body.workdir.trim() ? body.workdir.trim() : undefined; const timeoutMs = Number.isFinite(Number(body.timeout_ms)) ? Number(body.timeout_ms) : undefined; const maxBytes = Number.isFinite(Number(body.max_bytes)) ? Number(body.max_bytes) : undefined; try { await getApplicationInProject(uuid, ws.coolify_project_uuid); const result = await execInCoolifyApp({ appUuid: uuid, command, service, user, workdir, timeoutMs, maxBytes, }); return NextResponse.json(result); } catch (err) { if (err instanceof TenantError) { return NextResponse.json({ error: err.message }, { status: 403 }); } return NextResponse.json( { error: 'Failed to exec in container', details: err instanceof Error ? err.message : String(err) }, { status: 502 }, ); } }