Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion app/api/playground/copy-repo/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from "next/server"

import { copyRepoToExistingContainer } from "@/lib/utils/container"
import {
copyRepoToExistingContainer,
startDevServerInContainer,
} from "@/lib/utils/container"
import { setupLocalRepository } from "@/lib/utils/utils-server"

export async function POST(req: NextRequest) {
Expand Down Expand Up @@ -39,6 +42,17 @@ export async function POST(req: NextRequest) {
)
}

// Start a dev server in the background inside the container
try {
await startDevServerInContainer({ containerName, mountPath })
} catch (err) {
// Do not fail the whole request if dev server fails to start; surface as warning
return NextResponse.json(
{ error: `Copied repo but failed to start dev server: ${err}` },
{ status: 202 }
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Exit code ignored, failures return success incorrectly

The try-catch wrapping startDevServerInContainer won't catch script execution failures because execInContainerWithDockerode returns { stdout, stderr, exitCode } instead of throwing exceptions on non-zero exit codes. The PR description states this endpoint should return HTTP 202 when dev server startup fails, but the returned exitCode is never checked. The endpoint will return HTTP 200 with { success: true } even when the shell script fails. Other callers of execInContainerWithDockerode in the codebase properly check exitCode === 0 to determine success.

Fix in Cursor Fix in Web


return NextResponse.json({ success: true })
} catch (err) {
return NextResponse.json(
Expand Down
39 changes: 39 additions & 0 deletions lib/utils/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,42 @@ export async function copyRepoToExistingContainer({
throw new Error(`Failed to copy repository to container: ${e}`)
}
}

/**
* Start a dev server inside an existing container for the app in `mountPath`.
* This determines the appropriate package manager based on lockfiles and then
* runs install and dev in the background using nohup so this call can return
* immediately. Logs are written to .dev.log and PID to .dev.pid under mountPath.
*/
export async function startDevServerInContainer({
containerName,
mountPath = "/workspace",
}: {
containerName: string
mountPath?: string
}): Promise<{ stdout: string; stderr: string; exitCode: number }> {
// Build a shell script that:
// - chooses package manager (pnpm > yarn > npm)
// - runs install (preferring frozen/ci if lockfile present)
// - starts dev server
// - backgrounds the process with nohup and stores logs/pid
const script = [
"set -e",
`cd ${mountPath}`,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unquoted shell path breaks on paths with spaces

The mountPath parameter is interpolated directly into the shell command without quotes in cd ${mountPath}. While currently hardcoded to /workspace, the function interface accepts any string. If mountPath contained spaces (e.g., /my workspace), the cd command would receive multiple arguments and fail. Paths containing shell metacharacters like semicolons could also cause unexpected behavior. The path should be quoted like cd "${mountPath}" to handle paths safely.

Additional Locations (1)

Fix in Cursor Fix in Web

// Pick package manager
"if [ -f pnpm-lock.yaml ]; then PM=pnpm; elif [ -f yarn.lock ]; then PM=yarn; else PM=npm; fi",
// Construct install command
'if [ "$PM" = "pnpm" ]; then INSTALL="pnpm install --frozen-lockfile || pnpm install"; ' +
'elif [ "$PM" = "yarn" ]; then INSTALL="yarn install --frozen-lockfile || yarn install"; ' +
'else INSTALL="if [ -f package-lock.json ]; then npm ci || npm i; else npm i; fi"; fi',
// Start dev in background after install completes
'nohup sh -c "$INSTALL && "$PM" run -s dev" > .dev.log 2>&1 & echo $! > .dev.pid',
'echo "Dev server starting with $PM. Logs: .dev.log, PID: $(cat .dev.pid)"',
].join(" && ")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Broken quoting prevents dev server start

The nohup sh -c ... command string has mismatched quotes around $INSTALL and $PM, which makes the shell command syntactically invalid. This likely causes the nohup launch to fail, so the dev server never starts and .dev.pid/.dev.log may not reflect a running process.

Additional Locations (1)

Fix in Cursor Fix in Web


return await execInContainerWithDockerode({
name: containerName,
command: script,
cwd: mountPath,
})
}
39 changes: 39 additions & 0 deletions shared/src/lib/utils/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,42 @@ export async function copyRepoToExistingContainer({
await chownExec.start({})
}
}

/**
* Start a dev server inside an existing container for the app in `mountPath`.
* This determines the appropriate package manager based on lockfiles and then
* runs install and dev in the background using nohup so this call can return
* immediately. Logs are written to .dev.log and PID to .dev.pid under mountPath.
*/
export async function startDevServerInContainer({
containerName,
mountPath = "/workspace",
}: {
containerName: string
mountPath?: string
}): Promise<{ stdout: string; stderr: string; exitCode: number }> {
// Build a shell script that:
// - chooses package manager (pnpm > yarn > npm)
// - runs install (preferring frozen/ci if lockfile present)
// - starts dev server
// - backgrounds the process with nohup and stores logs/pid
const script = [
"set -e",
`cd ${mountPath}`,
// Pick package manager
"if [ -f pnpm-lock.yaml ]; then PM=pnpm; elif [ -f yarn.lock ]; then PM=yarn; else PM=npm; fi",
// Construct install command
'if [ "$PM" = "pnpm" ]; then INSTALL="pnpm install --frozen-lockfile || pnpm install"; ' +
'elif [ "$PM" = "yarn" ]; then INSTALL="yarn install --frozen-lockfile || yarn install"; ' +
'else INSTALL="if [ -f package-lock.json ]; then npm ci || npm i; else npm i; fi"; fi',
// Start dev in background after install completes
'nohup sh -c "$INSTALL && "$PM" run -s dev" > .dev.log 2>&1 & echo $! > .dev.pid',
'echo "Dev server starting with $PM. Logs: .dev.log, PID: $(cat .dev.pid)"',
].join(" && ")

return await execInContainerWithDockerode({
name: containerName,
command: script,
cwd: mountPath,
})
}