Skip to content

Commit 9b96f2e

Browse files
hkiratclaude
andcommitted
feat: use empty sandboxes and add migration commands to setup
Remove per-project template building — sandboxes are now created empty and packages are installed at runtime. Setup configuration focuses on env vars, startup commands, and migration commands (auto-detected). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 465e6ae commit 9b96f2e

8 files changed

Lines changed: 80 additions & 66 deletions

File tree

apps/server/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ model Project {
111111
envVarsIv String?
112112
contextInstructions String? @db.Text
113113
startupCommands String[]
114+
migrationCommands String[]
114115
requiredServices String[]
115116
allowedFilePatterns String[]
116117
devServerPort Int @default(3000)

apps/server/src/routes/session.routes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,11 @@ router.post("/", async (req: Request, res: Response) => {
9999
.json({ error: "Not a member of this organization" });
100100
}
101101

102-
// Verify project template is READY
102+
// Verify project is configured
103103
if (project.templateStatus !== "READY") {
104104
return res
105105
.status(400)
106-
.json({ error: `Project template is not ready. Current status: ${project.templateStatus}` });
106+
.json({ error: `Project is not configured yet. Please complete setup first.` });
107107
}
108108

109109
// Start the session via service

apps/server/src/services/sandbox.service.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ export async function startSessionSandbox(
1818
include: { org: true },
1919
});
2020

21-
if (project.templateStatus !== "READY" || !project.e2bTemplateId) {
22-
throw new Error("Project template is not ready");
21+
if (project.templateStatus !== "READY") {
22+
throw new Error("Project is not configured yet");
2323
}
2424

2525
// Get user's GitHub token for repo cloning
@@ -34,8 +34,8 @@ export async function startSessionSandbox(
3434
const [ghEncrypted, ghIv] = githubAccount.accessToken.split("|");
3535
const githubToken = decrypt(ghEncrypted, ghIv);
3636

37-
// Create sandbox from the project's built template
38-
const sandbox = await Sandbox.create(project.e2bTemplateId, {
37+
// Create empty sandbox (no per-project template)
38+
const sandbox = await Sandbox.create({
3939
timeoutMs: project.maxSessionDurationMin * 60 * 1000,
4040
cpuCount: 8,
4141
memoryMB: 8192,
@@ -55,12 +55,27 @@ export async function startSessionSandbox(
5555
{ requestTimeoutMs: 5_000 }
5656
);
5757

58-
// Clone the repo (use env var for token to avoid leaking in process list)
59-
const cloneResult = await sandbox.commands.run(
58+
// Install required system packages
59+
const servicePkgs: string[] = [];
60+
for (const svc of project.requiredServices) {
61+
if (svc === "postgres") servicePkgs.push("postgresql", "postgresql-client");
62+
else if (svc === "redis") servicePkgs.push("redis-server");
63+
else if (svc === "mysql") servicePkgs.push("mysql-server");
64+
}
65+
const allPkgs = ["git", "curl", "xvfb", "x11vnc", "python3-pip", "chromium", ...servicePkgs].join(" ");
66+
await sandbox.commands.run(
67+
`sudo apt-get update && sudo apt-get install -y ${allPkgs} && pip3 install websockify --break-system-packages && sudo apt-get clean`,
68+
{ requestTimeoutMs: 180_000 }
69+
);
70+
71+
// Install global npm tools
72+
await sandbox.commands.run("npm install -g bun @openai/codex", { requestTimeoutMs: 60_000 });
73+
74+
// Clone the repo
75+
await sandbox.commands.run(
6076
`GIT_TOKEN="${githubToken}" git clone https://x-access-token:${githubToken}@github.com/${project.githubRepoFullName}.git /workspace 2>&1`,
6177
{ requestTimeoutMs: 120_000 }
6278
).catch((e: any) => {
63-
// CommandExitError has .result with stdout/stderr
6479
throw new Error(`Git clone failed: ${e.result?.stdout || e.message}`);
6580
});
6681

@@ -80,20 +95,29 @@ export async function startSessionSandbox(
8095
await sandbox.files.write("/workspace/.env", envContent);
8196
}
8297

83-
// Start services that are baked into the template
98+
// Start required services
8499
for (const service of project.requiredServices) {
85100
try {
86101
if (service === "postgres") {
87-
await sandbox.commands.run("sudo service postgresql start 2>/dev/null || service postgresql start 2>/dev/null || true", {
102+
await sandbox.commands.run("sudo service postgresql start 2>/dev/null || true", {
88103
requestTimeoutMs: 30_000,
89104
});
90105
} else if (service === "redis") {
91-
await sandbox.commands.run("redis-server --daemonize yes 2>/dev/null || sudo service redis-server start 2>/dev/null || true", {
106+
await sandbox.commands.run("redis-server --daemonize yes 2>/dev/null || true", {
92107
requestTimeoutMs: 10_000,
93108
});
94109
}
95110
} catch {
96-
// Services may already be running from template init
111+
// Best-effort service start
112+
}
113+
}
114+
115+
// Run migration commands
116+
for (const cmd of project.migrationCommands) {
117+
try {
118+
await sandbox.commands.run(`cd /workspace && ${cmd}`, { requestTimeoutMs: 60_000 });
119+
} catch {
120+
// Migration may fail if deps aren't installed yet — agent will handle it
97121
}
98122
}
99123

apps/server/src/services/session.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export async function startSession(projectId: string, userId: string) {
1010
});
1111

1212
if (project.templateStatus !== "READY") {
13-
throw new Error("Project template is not ready");
13+
throw new Error("Project is not configured yet");
1414
}
1515

1616
// Check for active sessions (conflict detection)
@@ -131,6 +131,7 @@ function buildSystemPrompt(project: {
131131
allowedFilePatterns: string[];
132132
contextInstructions: string | null;
133133
startupCommands: string[];
134+
migrationCommands: string[];
134135
requiredServices: string[];
135136
devServerPort: number;
136137
}): string {
@@ -143,6 +144,7 @@ function buildSystemPrompt(project: {
143144
`- Dev server port: ${project.devServerPort}`,
144145
`- Required services: ${project.requiredServices.length > 0 ? project.requiredServices.join(", ") : "none"}`,
145146
`- Startup commands: ${project.startupCommands.length > 0 ? project.startupCommands.join(" && ") : "check package.json"}`,
147+
`- Migration commands: ${project.migrationCommands.length > 0 ? project.migrationCommands.join(" && ") : "none"}`,
146148
"- You have FULL SYSTEM ACCESS including sudo. Use it freely to:",
147149
" - Start/stop/restart services (PostgreSQL, Redis, MySQL, etc.)",
148150
" - Install system packages with apt-get",

apps/server/src/services/setup.service.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Prisma } from "@prisma/client";
33
import { prisma } from "../lib/prisma";
44
import { decrypt, encrypt } from "../lib/crypto";
55
import { env } from "../config/env";
6-
import { buildProjectTemplate } from "./template.service";
76

87
function getLLMConfig() {
98
if (env.LLM_PROVIDER === "vercel") {
@@ -70,19 +69,22 @@ async function getSandbox(projectId: string): Promise<Sandbox> {
7069
return sandbox;
7170
}
7271

73-
const SETUP_SYSTEM_PROMPT = `You are a project setup assistant for Vendi. Your job is to analyze a project's codebase and automatically configure it for running in a sandbox environment.
72+
const SETUP_SYSTEM_PROMPT = `You are a project setup assistant for Vendi. Your job is to analyze a project's codebase and detect the configuration needed to run it.
7473
7574
The project is cloned at /workspace. ALWAYS use absolute paths starting with /workspace/ when reading files (e.g. /workspace/package.json, /workspace/docker-compose.yml).
7675
7776
YOUR GOAL:
78-
Automatically detect the project configuration by reading files. Then ask the developer ONLY for their environment variable values (the .env file contents). Do NOT ask about code changes, architecture decisions, or how the project should be modified.
79-
80-
WHAT TO AUTO-DETECT (do NOT ask the user about these — figure them out yourself):
81-
1. Required services (PostgreSQL, Redis, MySQL, etc.) — detect from docker-compose.yml, package.json dependencies, prisma/schema.prisma, etc.
82-
2. Startup commands — detect from package.json scripts (look for "dev" script), Makefile, README instructions
83-
3. Dev server port — detect from vite.config.ts, next.config.js, .env.example, or package.json scripts
84-
4. Allowed file patterns — infer from project structure (e.g. src/**, app/**, pages/**)
85-
5. Context instructions — write a brief description of the project based on what you find
77+
1. Auto-detect startup commands and database migration commands from the codebase.
78+
2. Ask the developer ONLY for their environment variable values (.env file contents).
79+
3. Output the configuration.
80+
81+
WHAT TO AUTO-DETECT (do NOT ask the user — figure them out yourself):
82+
1. Startup commands — from package.json scripts (look for "dev" script), Makefile, README instructions
83+
2. Migration commands — from prisma (npx prisma migrate deploy / npx prisma db push), drizzle, knex, sequelize, TypeORM, or whatever ORM/migration tool the project uses. If none found, leave empty.
84+
3. Required services (PostgreSQL, Redis, MySQL, etc.) — from docker-compose.yml, package.json deps, prisma/schema.prisma, etc.
85+
4. Dev server port — from vite.config.ts, next.config.js, .env.example, or package.json scripts
86+
5. Allowed file patterns — infer from project structure (e.g. src/**, app/**, pages/**)
87+
6. Context instructions — a brief description of the project
8688
8789
WHAT TO ASK THE USER:
8890
- Their .env file contents (environment variables). Reassure them values will be stored encrypted.
@@ -91,17 +93,18 @@ WHAT TO ASK THE USER:
9193
HOW TO WORK:
9294
1. Use your tools to read: package.json, .env.example or .env.sample, docker-compose.yml, README.md, prisma/schema.prisma, vite.config.ts or next.config.js, turbo.json or pnpm-workspace.yaml — read as many as exist
9395
2. Auto-detect ALL configuration from what you find
94-
3. Present a SHORT summary of what you detected (services, startup commands, port)
96+
3. Present a SHORT summary of what you detected (startup commands, migration commands, services)
9597
4. Ask the user to paste their .env file (or the values for the variables you found in .env.example)
9698
5. Once you have the .env values, IMMEDIATELY output the [SETUP_COMPLETE] block
9799
98100
OUTPUT FORMAT — when you have everything:
99101
100102
[SETUP_COMPLETE]
101103
{
102-
"requiredServices": ["postgres"],
103104
"startupCommands": ["npm install", "npm run dev"],
105+
"migrationCommands": ["npx prisma migrate deploy"],
104106
"envVars": {"DATABASE_URL": "postgresql://...", "PORT": "3000"},
107+
"requiredServices": ["postgres"],
105108
"devServerPort": 3000,
106109
"allowedFilePatterns": ["src/**", "public/**"],
107110
"contextInstructions": "Brief description of the project..."
@@ -111,13 +114,14 @@ OUTPUT FORMAT — when you have everything:
111114
RULES:
112115
- IMPORTANT: Batch multiple tool calls in a single response whenever possible. For example, read package.json, .env.example, and docker-compose.yml all in one response instead of one at a time. This saves time and cost.
113116
- ALWAYS read the codebase FIRST before saying anything to the user
114-
- Do NOT ask the user about services, ports, startup commands, or file patterns — detect them yourself
117+
- Do NOT ask the user about services, ports, startup commands, migration commands, or file patterns — detect them yourself
115118
- Do NOT ask about or suggest code changes — this is setup, not development
116119
- Parse the .env content the developer pastes and include ALL variables in envVars
117120
- Keep messages short — one message to summarize findings, one to ask for .env values
118121
- Do NOT output [SETUP_COMPLETE] until you have the env vars from the user
119122
- Keep the [SETUP_COMPLETE] JSON compact — no extra whitespace or commentary inside the block
120123
- Do NOT echo back or summarize all the env vars before the [SETUP_COMPLETE] block — go straight to the JSON output
124+
- If no migration commands are detected, set "migrationCommands" to an empty array []
121125
`;
122126

123127
const tools = [
@@ -496,7 +500,7 @@ async function handlePossibleCompletion(projectId: string, chatMessages: ChatMsg
496500
chatMessages.push({
497501
id: crypto.randomUUID(),
498502
role: "SYSTEM",
499-
content: "Project configured successfully! Building template...",
503+
content: "Project configured successfully! You can now start sessions.",
500504
createdAt: new Date().toISOString(),
501505
});
502506
await cleanupSandbox(projectId);
@@ -551,6 +555,7 @@ export async function resetSetup(projectId: string): Promise<void> {
551555
envVarsIv: null,
552556
contextInstructions: null,
553557
startupCommands: [],
558+
migrationCommands: [],
554559
requiredServices: [],
555560
allowedFilePatterns: [],
556561
devServerPort: 3000,
@@ -562,9 +567,10 @@ export async function resetSetup(projectId: string): Promise<void> {
562567
}
563568

564569
async function applySetupConfig(projectId: string, config: any) {
565-
const data: any = {};
570+
const data: any = { templateStatus: "READY" };
566571
if (config.requiredServices) data.requiredServices = config.requiredServices;
567572
if (config.startupCommands) data.startupCommands = config.startupCommands;
573+
if (config.migrationCommands) data.migrationCommands = config.migrationCommands;
568574
if (config.devServerPort) data.devServerPort = config.devServerPort;
569575
if (config.allowedFilePatterns) data.allowedFilePatterns = config.allowedFilePatterns;
570576
if (config.contextInstructions) data.contextInstructions = config.contextInstructions;
@@ -576,7 +582,6 @@ async function applySetupConfig(projectId: string, config: any) {
576582
data.envVarsIv = iv;
577583
}
578584
await prisma.project.update({ where: { id: projectId }, data });
579-
buildProjectTemplate(projectId).catch(console.error);
580585
}
581586

582587
async function cleanupSandbox(projectId: string) {

apps/web/src/pages/dashboard/Dashboard.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ import {
2222

2323
function TemplateStatusBadge({ status }: { status: Project["templateStatus"] }) {
2424
const config = {
25-
PENDING: { label: "Pending", className: "bg-gray-100 text-gray-600" },
26-
BUILDING: { label: "Building", className: "bg-yellow-100 text-yellow-700" },
27-
READY: { label: "Ready", className: "bg-green-100 text-green-700" },
28-
FAILED: { label: "Failed", className: "bg-red-100 text-red-700" },
25+
PENDING: { label: "Not Configured", className: "bg-gray-100 text-gray-600" },
26+
BUILDING: { label: "Not Configured", className: "bg-gray-100 text-gray-600" },
27+
READY: { label: "Configured", className: "bg-green-100 text-green-700" },
28+
FAILED: { label: "Not Configured", className: "bg-gray-100 text-gray-600" },
2929
};
3030
const { label, className } = config[status];
3131
return (
3232
<span className={cn("inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium", className)}>
33-
{status === "BUILDING" && <Loader2 className="mr-1 h-3 w-3 animate-spin" />}
3433
{label}
3534
</span>
3635
);

apps/web/src/pages/project/ProjectSetup.tsx

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,6 @@ export function ProjectSetup() {
200200
}
201201
}, [orgId, projectId]);
202202

203-
const templateStatus = project?.templateStatus;
204-
205203
return (
206204
<div className="flex h-[calc(100vh-3rem)] flex-col max-w-3xl mx-auto">
207205
{/* Header */}
@@ -344,35 +342,19 @@ export function ProjectSetup() {
344342
<div ref={messagesEndRef} />
345343
</div>
346344

347-
{/* Template build status */}
348-
{setupComplete && templateStatus && (
349-
<div className={cn(
350-
"px-4 py-2 text-sm border-t",
351-
templateStatus === "BUILDING" && "bg-yellow-50 text-yellow-800 border-yellow-200",
352-
templateStatus === "READY" && "bg-green-50 text-green-800 border-green-200",
353-
templateStatus === "FAILED" && "bg-red-50 text-red-800 border-red-200"
354-
)}>
355-
{templateStatus === "BUILDING" && (
356-
<span className="flex items-center gap-2">
357-
<Loader2 className="h-4 w-4 animate-spin" />
358-
Building template... This may take a few minutes.
359-
</span>
360-
)}
361-
{templateStatus === "READY" && (
362-
<span className="flex items-center gap-2">
363-
<CheckCircle2 className="h-4 w-4" />
364-
Template ready! You can now start sessions.
365-
<button
366-
onClick={() => navigate(`/orgs/${orgId}`)}
367-
className="ml-auto rounded-lg bg-green-700 px-3 py-1 text-xs font-medium text-white hover:bg-green-800"
368-
>
369-
Go to Dashboard
370-
</button>
371-
</span>
372-
)}
373-
{templateStatus === "FAILED" && (
374-
<span>Template build failed. Check the build log.</span>
375-
)}
345+
{/* Setup complete status */}
346+
{setupComplete && (
347+
<div className="px-4 py-2 text-sm border-t bg-green-50 text-green-800 border-green-200">
348+
<span className="flex items-center gap-2">
349+
<CheckCircle2 className="h-4 w-4" />
350+
Project configured! You can now start sessions.
351+
<button
352+
onClick={() => navigate(`/orgs/${orgId}`)}
353+
className="ml-auto rounded-lg bg-green-700 px-3 py-1 text-xs font-medium text-white hover:bg-green-800"
354+
>
355+
Go to Dashboard
356+
</button>
357+
</span>
376358
</div>
377359
)}
378360

packages/shared/src/types/project.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface Project {
99
defaultBranch: string;
1010
contextInstructions: string | null;
1111
startupCommands: string[];
12+
migrationCommands: string[];
1213
requiredServices: string[];
1314
allowedFilePatterns: string[];
1415
devServerPort: number;

0 commit comments

Comments
 (0)