diff --git a/apps/web/app/(app)/api/check-sandbox/route.ts b/apps/web/app/(app)/api/check-sandbox/route.ts index d1409ba2..3a4d9239 100644 --- a/apps/web/app/(app)/api/check-sandbox/route.ts +++ b/apps/web/app/(app)/api/check-sandbox/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server' -import { Sandbox } from '@e2b/code-interpreter' +import { getSandboxProvider } from '@react-native-vibe-code/sandbox/lib' export async function POST(request: Request) { try { @@ -11,14 +11,15 @@ export async function POST(request: Request) { console.log('[check-sandbox] Checking sandbox container:', sandboxId) - // Check if the sandbox container is alive using E2B SDK + // Check if the sandbox container is alive using the active provider try { - const sandbox = await Sandbox.connect(sandboxId) - + const sandbox = await getSandboxProvider().connect(sandboxId) console.log('[check-sandbox] Sandbox container is alive:', sandboxId) - // Note: E2B Sandbox doesn't need explicit closing for resume operations - // The sandbox will remain active and timeout according to E2B's policies + // Close connection if supported + if (sandbox.close) { + await sandbox.close().catch(() => {}) + } return NextResponse.json({ isAlive: true }) } catch (error) { diff --git a/apps/web/app/(app)/api/create-container/route.ts b/apps/web/app/(app)/api/create-container/route.ts index 79f5aa93..7ac65389 100644 --- a/apps/web/app/(app)/api/create-container/route.ts +++ b/apps/web/app/(app)/api/create-container/route.ts @@ -1,6 +1,6 @@ import { db } from '@/lib/db' import { projects, convexProjectCredentials } from '@react-native-vibe-code/database' -import { Sandbox } from '@e2b/code-interpreter' +import { getSandboxProvider, type ISandbox } from '@react-native-vibe-code/sandbox/lib' import { Octokit } from '@octokit/rest' import { GitHubService } from '@/lib/github-service' import { eq, and } from 'drizzle-orm' @@ -113,7 +113,7 @@ function sendConvexError(projectId: string, logData: string): void { async function setupConvex(params: { projectId: string userId: string - sandbox: Sandbox + sandbox: ISandbox appName: string }): Promise { // Check if Convex is configured @@ -210,7 +210,7 @@ async function setupConvex(params: { /** * Write EXPO_PUBLIC_CONVEX_URL to sandbox .env.local */ -async function writeConvexUrlToSandbox(sandbox: Sandbox, deploymentUrl: string): Promise { +async function writeConvexUrlToSandbox(sandbox: ISandbox, deploymentUrl: string): Promise { try { const { updateSandboxEnvFile } = await import('@/lib/convex/sandbox-utils') await updateSandboxEnvFile(sandbox, 'EXPO_PUBLIC_CONVEX_URL', deploymentUrl) @@ -225,7 +225,7 @@ async function writeConvexUrlToSandbox(sandbox: Sandbox, deploymentUrl: string): */ async function startConvexDevServer(params: { projectId: string - sandbox: Sandbox + sandbox: ISandbox adminKey: string deploymentUrl: string }): Promise { @@ -304,7 +304,8 @@ export async function POST(req: NextRequest) { // Check if project already exists with active sandbox let project = null - let sandbox: Sandbox | null = null + const sandboxProvider = getSandboxProvider() + let sandbox: ISandbox | null = null try { const existingProjects = await db @@ -328,7 +329,7 @@ export async function POST(req: NextRequest) { // Try to connect to the existing sandbox if (project.sandboxId) { try { - sandbox = await Sandbox.connect(project.sandboxId) + sandbox = await sandboxProvider.connect(project.sandboxId) console.log(`Connected to existing sandbox: ${sandbox.sandboxId}`) // Set API Base URL for the sandbox app @@ -387,15 +388,16 @@ export async function POST(req: NextRequest) { console.log(`Using template: ${templateSelection} (ID: ${templateId[templateSelection]})`) - sandbox = await Sandbox.create(templateId[templateSelection], { - + sandbox = await sandboxProvider.create({ + templateId: templateId[templateSelection], + image: process.env.DAYTONA_IMAGE, metadata: { template: templateId[templateSelection], userID: userID, teamID: teamID || '', projectId, }, - timeoutMs: parseInt(process.env.E2B_SANDBOX_TIMEOUT_MS || '3600000'), // Use env var, default to 1 hour + timeoutMs: parseInt(process.env.SANDBOX_TIMEOUT_MS || process.env.E2B_SANDBOX_TIMEOUT_MS || '3600000'), }) console.log(`Created new sandbox: ${sandbox.sandboxId}`) diff --git a/apps/web/app/(app)/api/sandbox/route.ts b/apps/web/app/(app)/api/sandbox/route.ts index fab6c6f1..caab8d5a 100644 --- a/apps/web/app/(app)/api/sandbox/route.ts +++ b/apps/web/app/(app)/api/sandbox/route.ts @@ -2,12 +2,12 @@ import { db } from '@/lib/db' import { projects } from '@react-native-vibe-code/database' import { FragmentSchema } from '@/lib/schema' import { ExecutionResultInterpreter, ExecutionResultWeb } from '@/lib/types' -import { Sandbox } from '@e2b/code-interpreter' +import { getSandboxProvider, type ISandbox } from '@react-native-vibe-code/sandbox/lib' import { eq, and } from 'drizzle-orm' import { globalFileWatcher } from '@/lib/sandbox-file-watcher' import { globalFileChangeStream } from '@/lib/file-change-stream' -const sandboxTimeout = parseInt(process.env.E2B_SANDBOX_TIMEOUT_MS || '3600000') // Use env var, default to 1 hour +const sandboxTimeout = parseInt(process.env.SANDBOX_TIMEOUT_MS || process.env.E2B_SANDBOX_TIMEOUT_MS || '3600000') // Use env var, default to 1 hour export const maxDuration = 120 @@ -122,7 +122,9 @@ export async function POST(req: Request) { } let project = null - let sbx = null + let sbx: ISandbox | null = null + + const sandboxProvider = getSandboxProvider() // Check if this is a follow-up message for an existing project if (projectId && !isFirstMessage) { @@ -148,21 +150,14 @@ export async function POST(req: Request) { // Try to connect to the existing sandbox if (project.sandboxId) { try { - sbx = await Sandbox.connect(project.sandboxId) + sbx = await sandboxProvider.connect(project.sandboxId) console.log(`Connected to sandbox: ${sbx.sandboxId}`) - } catch (error) { - console.log(`Failed to connect to sandbox ${project.sandboxId}:`, error) - // Try to connect to existing sandbox instead - try { - sbx = await Sandbox.connect(project.sandboxId) - console.log(`Connected to existing sandbox: ${sbx.sandboxId}`) - } catch (connectError) { - console.log( - `Failed to connect to sandbox ${project.sandboxId}:`, - connectError, - ) - // If both resume and connect fail, we'll create a new sandbox below - } + } catch (connectError) { + console.log( + `Failed to connect to sandbox ${project.sandboxId}:`, + connectError, + ) + // If connect fails, we'll create a new sandbox below } } } @@ -178,7 +173,9 @@ export async function POST(req: Request) { ? 'sm3r39vktkmu37lna0qa' : fragment.template - sbx = await Sandbox.create(templateId, { + sbx = await sandboxProvider.create({ + templateId, + image: process.env.DAYTONA_IMAGE, metadata: { template: templateId, userID: userID ?? '', @@ -310,18 +307,26 @@ export async function POST(req: Request) { console.log(`Still waiting for webBundled... ${waitTime}ms elapsed`) } - // Get the public URL using e2b's getHost method - const publicHost = sbx.getHost(8081) - publicUrl = `https://${publicHost}` + // Get the public URL - try async getPreviewUrl first, fall back to sync getHost + if (sbx.getPreviewUrl) { + publicUrl = await sbx.getPreviewUrl(8081) + } else { + publicUrl = `https://${sbx.getHost(8081)}` + } - console.log('E2B public URL:', publicUrl) + console.log('Sandbox public URL:', publicUrl) if (!webBundled) { console.warn('WebBundled not detected, but proceeding with URL') } } else { // For other templates, use the standard port - publicUrl = `https://${sbx?.getHost(fragment.port || 80)}` + const port = fragment.port || 80 + if (sbx?.getPreviewUrl) { + publicUrl = await sbx.getPreviewUrl(port) + } else { + publicUrl = `https://${sbx?.getHost(port)}` + } } // Install packages diff --git a/apps/web/app/(app)/api/start-server/route.ts b/apps/web/app/(app)/api/start-server/route.ts index 8d779e53..8ec5ce2b 100644 --- a/apps/web/app/(app)/api/start-server/route.ts +++ b/apps/web/app/(app)/api/start-server/route.ts @@ -1,7 +1,7 @@ import { db } from '@/lib/db' import { projects } from '@react-native-vibe-code/database' import { startExpoServer } from '@/lib/server-utils' -import { Sandbox } from '@e2b/code-interpreter' +import { getSandboxProvider } from '@react-native-vibe-code/sandbox/lib' import { eq, and } from 'drizzle-orm' import { NextRequest } from 'next/server' @@ -66,8 +66,8 @@ export async function POST(req: NextRequest) { // Verify the sandbox is still active before returning cached URLs try { console.log('Verifying sandbox is still active:', sandboxId) - const testSandbox = await Sandbox.connect(sandboxId) - await testSandbox.close() // Close immediately after verification + const testSandbox = await getSandboxProvider().connect(sandboxId) + if (testSandbox.close) await testSandbox.close() // Close immediately after verification console.log( 'Server already running and sandbox active, returning existing URLs:', @@ -95,10 +95,10 @@ export async function POST(req: NextRequest) { ) } - // Connect to sandbox - let sandbox: Sandbox + // Connect to sandbox using the active provider + let sandbox: Awaited['connect']> try { - sandbox = await Sandbox.connect(sandboxId) + sandbox = await getSandboxProvider().connect(sandboxId) console.log(`Connected to sandbox for server start: ${sandbox.sandboxId}`) } catch (error) { console.error(`Failed to resume sandbox ${sandboxId}:`, error) diff --git a/apps/web/lib/claude-code-handler.ts b/apps/web/lib/claude-code-handler.ts index 1cfbb10f..5922f355 100644 --- a/apps/web/lib/claude-code-handler.ts +++ b/apps/web/lib/claude-code-handler.ts @@ -2,7 +2,7 @@ import { ClaudeCodeService } from '@/lib/claude-code-service' import { db } from '@/lib/db' import { projects } from '@react-native-vibe-code/database' import { UsageTracker } from '@/lib/usage-tracking' -import { Sandbox } from '@e2b/code-interpreter' +import { getSandboxProvider, type ISandbox } from '@react-native-vibe-code/sandbox/lib' import { eq, and } from 'drizzle-orm' import { getSkillTemplate, getSkillFilePath } from '@/lib/skills/templates' import { validateSkillIds } from '@/lib/skills' @@ -62,7 +62,7 @@ export async function handleClaudeCodeGeneration( // Get existing project let project: any = null - let sandbox: Sandbox | null = null + let sandbox: ISandbox | null = null // Helper function to fetch project from database const fetchProject = async () => { @@ -133,8 +133,8 @@ export async function handleClaudeCodeGeneration( } } - // Connect to sandbox - sandbox = await Sandbox.connect(targetSandboxId) + // Connect to sandbox using the active provider + sandbox = await getSandboxProvider().connect(targetSandboxId) console.log(`[Claude Code Handler] Connected to sandbox: ${sandbox.sandboxId}`) } catch (error) { console.error('[Claude Code Handler] Error checking for existing project:', error) diff --git a/apps/web/lib/claude-code-service.ts b/apps/web/lib/claude-code-service.ts index b744fab6..d2cea6d9 100644 --- a/apps/web/lib/claude-code-service.ts +++ b/apps/web/lib/claude-code-service.ts @@ -1,5 +1,5 @@ import { pusherServer } from './pusher' -import { Sandbox} from '@e2b/code-interpreter' +import type { ISandbox } from '@react-native-vibe-code/sandbox/lib' import { prompt, getPromptWithCloudStatus } from '@react-native-vibe-code/prompt-engine' import { db } from '@/lib/db' import { projects } from '@react-native-vibe-code/database' @@ -59,7 +59,7 @@ export class ClaudeCodeService { async generateAppStreaming( request: AppGenerationRequest, - sandbox: Sandbox, + sandbox: ISandbox, callbacks: StreamingCallbacks, ): Promise { try { @@ -621,7 +621,7 @@ export class ClaudeCodeService { private async getConversationContext( request: AppGenerationRequest, - sandbox: Sandbox, + sandbox: ISandbox, ): Promise { // Load previous conversation history from database if exists const previousMessages = await this.loadConversationHistory( diff --git a/packages/convex/src/sandbox-utils.ts b/packages/convex/src/sandbox-utils.ts index 95792ac3..5596bb35 100644 --- a/packages/convex/src/sandbox-utils.ts +++ b/packages/convex/src/sandbox-utils.ts @@ -1,9 +1,23 @@ /** * Convex Sandbox Utilities - * Functions for managing Convex configuration in E2B sandboxes + * Functions for managing Convex configuration in sandboxes. + * Compatible with any sandbox provider (E2B, Daytona, etc.) */ -import type { Sandbox } from '@e2b/code-interpreter' +/** + * Minimal sandbox interface required by Convex sandbox utilities. + * This avoids a circular dependency with @react-native-vibe-code/sandbox. + */ +interface SandboxLike { + files: { + exists(path: string): Promise + read(path: string): Promise + write(path: string, content: string): Promise + } +} + +/** @deprecated Use SandboxLike directly. Kept for backwards compatibility. */ +type Sandbox = SandboxLike import { db, convexProjectCredentials, eq } from '@react-native-vibe-code/database' import { detectAndNotifyConvexError } from '@react-native-vibe-code/error-manager/server' diff --git a/packages/publish/package.json b/packages/publish/package.json index 1877323d..c853082e 100644 --- a/packages/publish/package.json +++ b/packages/publish/package.json @@ -35,6 +35,7 @@ "access": "public" }, "dependencies": { + "@react-native-vibe-code/sandbox": "workspace:*", "@e2b/code-interpreter": "2.1.0", "cloudflare": "^5.2.0", "nanoid": "^5.1.5" diff --git a/packages/publish/src/cloudflare/deploy.ts b/packages/publish/src/cloudflare/deploy.ts index 8df5f501..2b49be7f 100644 --- a/packages/publish/src/cloudflare/deploy.ts +++ b/packages/publish/src/cloudflare/deploy.ts @@ -11,7 +11,7 @@ * @see README.md for required environment variables */ -import { Sandbox } from '@e2b/code-interpreter' +import { getSandboxProvider } from '@react-native-vibe-code/sandbox/lib' import { customAlphabet } from 'nanoid' import { addCustomDomain, verifyCustomDomain } from './custom-domain' import type { CloudflareDeployOptions, CloudflareDeployResult } from '../types' @@ -99,8 +99,8 @@ export async function deployToCloudflare( console.log(`[Cloudflare Deploy] Sandbox ID received: ${sandboxId}`) console.log(`[Cloudflare Deploy] About to connect to sandbox...`) - // Connect to the sandbox - const sandbox = await Sandbox.connect(sandboxId) + // Connect to the sandbox using the active provider + const sandbox = await getSandboxProvider().connect(sandboxId) console.log(`[Cloudflare Deploy] Successfully connected to sandbox: ${sandbox.sandboxId}`) // Step 1: Build the web app diff --git a/packages/publish/src/config/app-config.ts b/packages/publish/src/config/app-config.ts index 5b398a0f..c560fc4f 100644 --- a/packages/publish/src/config/app-config.ts +++ b/packages/publish/src/config/app-config.ts @@ -7,7 +7,7 @@ * - wrangler.toml (Cloudflare Workers configuration) */ -import type { Sandbox } from '@e2b/code-interpreter' +import type { ISandbox as Sandbox } from '@react-native-vibe-code/sandbox/lib' /** * Update app.json in the sandbox with the fantasy name diff --git a/packages/remix/src/api/remix-handler.ts b/packages/remix/src/api/remix-handler.ts index 23e1c297..68cc3a02 100644 --- a/packages/remix/src/api/remix-handler.ts +++ b/packages/remix/src/api/remix-handler.ts @@ -1,4 +1,4 @@ -import { Sandbox } from '@e2b/code-interpreter' +import { getSandboxProvider } from '@react-native-vibe-code/sandbox/lib' import { Octokit } from '@octokit/rest' import { eq, sql } from 'drizzle-orm' import { v4 as uuidv4 } from 'uuid' @@ -82,7 +82,8 @@ export async function createRemix(params: { console.log(`[Remix] User ${userId} remixing project ${sourceProjectId} to ${newProjectId}`) // Create sandbox first - let sandbox: Sandbox | null = null + const sandboxProvider = getSandboxProvider() + let sandbox: Awaited> | null = null try { const templateId = { expo: 'sm3r39vktkmu37lna0qa', @@ -91,14 +92,16 @@ export async function createRemix(params: { const templateSelection: keyof typeof templateId = (process.env.TEMPLATE_SELECTION as keyof typeof templateId) || 'expo' - sandbox = await Sandbox.create(templateId[templateSelection], { + sandbox = await sandboxProvider.create({ + templateId: templateId[templateSelection], + image: process.env.DAYTONA_IMAGE, metadata: { template: sourceProject.template, userID: userId, projectId: newProjectId, forkedFrom: sourceProjectId, }, - timeoutMs: parseInt(process.env.E2B_SANDBOX_TIMEOUT_MS || '3600000'), + timeoutMs: parseInt(process.env.SANDBOX_TIMEOUT_MS || process.env.E2B_SANDBOX_TIMEOUT_MS || '3600000'), }) console.log(`[Remix] Created sandbox: ${sandbox.sandboxId}`) @@ -291,7 +294,7 @@ export async function createRemix(params: { } // Connect to source sandbox and push its code to GitHub - const sourceSandbox = await Sandbox.connect(sourceProject.sandboxId) + const sourceSandbox = await sandboxProvider.connect(sourceProject.sandboxId) console.log(`[Remix] Connected to source sandbox to push code`) const pushSourceScript = `#!/bin/bash diff --git a/packages/remix/src/lib/convex-helpers.ts b/packages/remix/src/lib/convex-helpers.ts index f64ddb91..40dc59d9 100644 --- a/packages/remix/src/lib/convex-helpers.ts +++ b/packages/remix/src/lib/convex-helpers.ts @@ -1,4 +1,4 @@ -import { Sandbox } from '@e2b/code-interpreter' +import type { ISandbox as Sandbox } from '@react-native-vibe-code/sandbox/lib' import { eq } from 'drizzle-orm' import { getPusherServer } from '@react-native-vibe-code/pusher' import { provisionManagedConvexProject } from '@react-native-vibe-code/convex' diff --git a/packages/restore/src/api/git-commits.ts b/packages/restore/src/api/git-commits.ts index 77a881fb..cc5abb61 100644 --- a/packages/restore/src/api/git-commits.ts +++ b/packages/restore/src/api/git-commits.ts @@ -1,5 +1,5 @@ import { db, projects, eq, and } from '@react-native-vibe-code/database' -import { Sandbox } from '@e2b/code-interpreter' +import { getSandboxProvider } from '@react-native-vibe-code/sandbox/lib' import type { GitCommitsRequest, GitCommitsResponse, Commit } from '../types' export const maxDuration = 30 @@ -52,9 +52,9 @@ export async function getGitCommits( } // Connect to sandbox - let sandbox: Sandbox + let sandbox: Awaited['connect']> try { - sandbox = await Sandbox.connect(sandboxId) + sandbox = await getSandboxProvider().connect(sandboxId) console.log(`[Git Commits] Connected to sandbox: ${sandbox.sandboxId}`) } catch (error) { console.error( diff --git a/packages/restore/src/api/git-restore.ts b/packages/restore/src/api/git-restore.ts index da3a2d7e..c24fe9d0 100644 --- a/packages/restore/src/api/git-restore.ts +++ b/packages/restore/src/api/git-restore.ts @@ -1,6 +1,5 @@ import { db, projects, message, eq, and, gte } from '@react-native-vibe-code/database' -import { startExpoServer } from '@react-native-vibe-code/sandbox' -import { Sandbox } from '@e2b/code-interpreter' +import { startExpoServer, getSandboxProvider } from '@react-native-vibe-code/sandbox/lib' import type { GitRestoreRequest, GitRestoreResponse } from '../types' export const maxDuration = 120 // 2 minutes for git operations + server restart @@ -141,10 +140,10 @@ export async function restoreGitCommit( const project = existingProjects[0] - // Connect to sandbox - let sandbox: Sandbox + // Connect to sandbox using the active provider + let sandbox: Awaited['connect']> try { - sandbox = await Sandbox.connect(sandboxId) + sandbox = await getSandboxProvider().connect(sandboxId) console.log(`[Git Restore] Connected to sandbox: ${sandbox.sandboxId}`) } catch (error) { console.error( diff --git a/packages/restore/src/api/github-commit.ts b/packages/restore/src/api/github-commit.ts index 3b0300bd..f70fb82c 100644 --- a/packages/restore/src/api/github-commit.ts +++ b/packages/restore/src/api/github-commit.ts @@ -1,5 +1,4 @@ -import { GitHubService } from '@react-native-vibe-code/sandbox' -import { Sandbox } from '@e2b/code-interpreter' +import { GitHubService, getSandboxProvider } from '@react-native-vibe-code/sandbox/lib' import type { GitHubCommitRequest, GitHubCommitResponse } from '../types' export const maxDuration = 300 // 5 minutes @@ -38,10 +37,10 @@ export async function commitToGitHub( } } - // Connect to sandbox - let sandbox: Sandbox + // Connect to sandbox using the active provider + let sandbox: Awaited['connect']> try { - sandbox = await Sandbox.connect(sandboxId) + sandbox = await getSandboxProvider().connect(sandboxId) console.log('[GitHub Commit API] Connected to sandbox:', sandboxId) } catch (error) { console.error('[GitHub Commit API] Failed to connect to sandbox:', error) diff --git a/packages/restore/src/lib/git-operations.ts b/packages/restore/src/lib/git-operations.ts index 0ac7fa3a..8a0b682c 100644 --- a/packages/restore/src/lib/git-operations.ts +++ b/packages/restore/src/lib/git-operations.ts @@ -1,4 +1,4 @@ -import type { Sandbox } from '@e2b/code-interpreter' +import type { ISandbox as Sandbox } from '@react-native-vibe-code/sandbox/lib' /** * Execute git status in sandbox diff --git a/packages/restore/src/types/index.ts b/packages/restore/src/types/index.ts index b8528aa4..4f236093 100644 --- a/packages/restore/src/types/index.ts +++ b/packages/restore/src/types/index.ts @@ -1,5 +1,3 @@ -import type { Sandbox } from '@e2b/code-interpreter' - // Commit types export interface Commit { hash: string diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index 4a5f3eee..d0e8b1f7 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -46,7 +46,9 @@ "type-check": "tsc --noEmit", "build:sandbox:expo": "e2b template build --config templates/expo-template/e2b.toml --name 'capsule-expo-sandbox'", "build:sandbox:tamagui": "e2b template build --config templates/tamagui-template/e2b.toml --name 'capsule-tamagui-sandbox'", - "build:sandbox:github": "e2b template build --config templates/github-template/e2b.toml --name 'capsule-github-sandbox'" + "build:sandbox:github": "e2b template build --config templates/github-template/e2b.toml --name 'capsule-github-sandbox'", + "build:sandbox:expo:daytona": "docker build -f templates/daytona-expo-template/Dockerfile -t react-native-vibe-code/expo-sandbox:latest .", + "build:sandbox:github:daytona": "docker build -f templates/daytona-github-template/Dockerfile -t react-native-vibe-code/github-sandbox:latest ." }, "publishConfig": { "access": "public" @@ -61,6 +63,7 @@ "@react-native-vibe-code/error-manager": "workspace:*", "@react-native-vibe-code/ui": "workspace:*", "@e2b/code-interpreter": "2.1.0", + "@daytonaio/sdk": "^0.1.0", "@anthropic-ai/claude-agent-sdk": "^0.1.9", "@octokit/rest": "^22.0.0", "@vercel/blob": "^2.0.0", diff --git a/packages/sandbox/src/lib/bundle-builder.ts b/packages/sandbox/src/lib/bundle-builder.ts index 0d94267c..0e6b2578 100644 --- a/packages/sandbox/src/lib/bundle-builder.ts +++ b/packages/sandbox/src/lib/bundle-builder.ts @@ -1,9 +1,10 @@ /** * Static Bundle Builder Service - * Builds Expo static bundles in E2B sandboxes and uploads to Vercel Blob + * Builds Expo static bundles in sandboxes and uploads to Vercel Blob. + * Compatible with any ISandbox provider (E2B, Daytona, etc.) */ -import { Sandbox } from '@e2b/code-interpreter' +import type { ISandbox } from './providers/types' import { put } from '@vercel/blob' import { generateManifest, validateManifest } from './generate-manifest' import { db, projects, commits, eq } from '@react-native-vibe-code/database' @@ -30,7 +31,7 @@ export async function buildStaticBundle( commitId: string, userMessage?: string ): Promise { - let sandbox: Sandbox | null = null + let sandbox: ISandbox | null = null try { console.log('[BundleBuilder] Starting bundle build', { @@ -39,8 +40,9 @@ export async function buildStaticBundle( commitId, }) - // Connect to existing sandbox - sandbox = await Sandbox.connect(sandboxId) + // Connect to existing sandbox using the active provider + const { getSandboxProvider } = await import('./providers') + sandbox = await getSandboxProvider().connect(sandboxId) // Step 1: Run expo export for iOS console.log('[BundleBuilder] Running expo export...') @@ -270,7 +272,7 @@ function getContentTypeForExtension(extension: string): string { /** * Get latest commit SHA from sandbox */ -export async function getLatestCommitSHA(sandbox: Sandbox): Promise { +export async function getLatestCommitSHA(sandbox: ISandbox): Promise { try { const result = await sandbox.commands.run( 'cd /home/user/app && git rev-parse HEAD' diff --git a/packages/sandbox/src/lib/github-service.ts b/packages/sandbox/src/lib/github-service.ts index b363e2fd..d387da9f 100644 --- a/packages/sandbox/src/lib/github-service.ts +++ b/packages/sandbox/src/lib/github-service.ts @@ -1,4 +1,4 @@ -import { Sandbox } from '@e2b/code-interpreter' +import type { ISandbox } from './providers/types' export interface GitHubConfig { owner: string @@ -16,7 +16,7 @@ export class GitHubService { * Initialize git repository and push initial code to GitHub */ async initializeRepository( - sandbox: Sandbox, + sandbox: ISandbox, projectId: string, repositoryName: string, commitMessage: string = 'Initial commit' @@ -80,7 +80,7 @@ fi * Commit and push changes to GitHub */ async commitAndPush( - sandbox: Sandbox, + sandbox: ISandbox, repositoryName: string, commitMessage: string, files?: string[] @@ -137,7 +137,7 @@ echo "Successfully pushed changes to GitHub" * Pull latest changes from GitHub */ async pullChanges( - sandbox: Sandbox, + sandbox: ISandbox, repositoryName: string ): Promise { try { @@ -173,7 +173,7 @@ echo "Successfully pulled latest changes" /** * Get git status */ - async getStatus(sandbox: Sandbox): Promise { + async getStatus(sandbox: ISandbox): Promise { try { const execution = await sandbox.commands.run('cd /home/user/app && git status --porcelain', { timeoutMs: 10000, @@ -189,7 +189,7 @@ echo "Successfully pulled latest changes" /** * Check if directory is a git repository */ - async isGitRepository(sandbox: Sandbox): Promise { + async isGitRepository(sandbox: ISandbox): Promise { try { const execution = await sandbox.commands.run('cd /home/user/app && git status', { timeoutMs: 5000, @@ -204,7 +204,7 @@ echo "Successfully pulled latest changes" /** * Create a zip archive of the project */ - async createProjectArchive(sandbox: Sandbox): Promise { + async createProjectArchive(sandbox: ISandbox): Promise { try { const zipScript = `#!/bin/bash set -e diff --git a/packages/sandbox/src/lib/index.ts b/packages/sandbox/src/lib/index.ts index ad6b3d73..0dc65218 100644 --- a/packages/sandbox/src/lib/index.ts +++ b/packages/sandbox/src/lib/index.ts @@ -1,5 +1,24 @@ // Sandbox library exports +// Provider abstraction layer +export { + getSandboxProvider, + resetSandboxProvider, + E2BProvider, + DaytonaProvider, + type ISandbox, + type ISandboxFiles, + type ISandboxCommands, + type ISandboxProvider, + type SandboxCreateConfig, + type CommandResult, + type CommandOptions, + type FileWatchEvent, + type WatchHandle, + type WatchOptions, + type SandboxProviderName, +} from './providers' + // File watcher export { SandboxFileWatcher, diff --git a/packages/sandbox/src/lib/providers/daytona.ts b/packages/sandbox/src/lib/providers/daytona.ts new file mode 100644 index 00000000..5aad02eb --- /dev/null +++ b/packages/sandbox/src/lib/providers/daytona.ts @@ -0,0 +1,422 @@ +/** + * Daytona Sandbox Provider + * + * Adapter that wraps @daytonaio/sdk Workspace to implement ISandbox. + * Enable by setting SANDBOX_PROVIDER=daytona in your .env file. + * + * Required environment variables: + * - DAYTONA_API_KEY: Your Daytona API key + * - DAYTONA_SERVER_URL: Your Daytona server URL (e.g., https://app.daytona.io) + * + * Optional environment variables: + * - DAYTONA_IMAGE: Default Docker image (e.g., ubuntu:22.04) + * - DAYTONA_TARGET: Target runner (e.g., 'local', 'us') + * + * Daytona docs: https://www.daytona.io/docs/ + */ + +import type { + ISandbox, + ISandboxFiles, + ISandboxCommands, + ISandboxProvider, + SandboxCreateConfig, + CommandOptions, + CommandResult, + FileWatchEvent, + WatchHandle, + WatchOptions, +} from './types' + +// Lazily import Daytona SDK to avoid errors when not using Daytona provider +let DaytonaSDK: any = null +async function getDaytonaSDK() { + if (!DaytonaSDK) { + try { + DaytonaSDK = await import('@daytonaio/sdk') + } catch { + throw new Error( + '[DaytonaProvider] @daytonaio/sdk is not installed. Run: pnpm add @daytonaio/sdk --filter @react-native-vibe-code/sandbox', + ) + } + } + return DaytonaSDK +} + +class DaytonaSandboxFiles implements ISandboxFiles { + constructor(private workspace: any) {} + + async write(path: string, content: string): Promise { + try { + const contentBuffer = Buffer.from(content, 'utf-8') + // Daytona filesystem.uploadFile expects a File object + const file = new File([contentBuffer], path.split('/').pop() ?? 'file', { + type: 'text/plain', + }) + await this.workspace.filesystem.uploadFile(file, path) + } catch (error) { + console.error(`[DaytonaSandboxFiles] Error writing file ${path}:`, error) + throw error + } + } + + async read(path: string): Promise { + try { + const content = await this.workspace.filesystem.downloadFile(path) + if (Buffer.isBuffer(content)) { + return content.toString('utf-8') + } + if (content instanceof Uint8Array) { + return Buffer.from(content).toString('utf-8') + } + return String(content) + } catch (error) { + console.error(`[DaytonaSandboxFiles] Error reading file ${path}:`, error) + throw error + } + } + + async exists(path: string): Promise { + try { + await this.workspace.filesystem.downloadFile(path) + return true + } catch { + // File or folder doesn't exist + return false + } + } + + async makeDir(path: string): Promise { + try { + await this.workspace.filesystem.createFolder(path, '0755') + } catch (error) { + // Ignore "already exists" errors + const msg = error instanceof Error ? error.message : String(error) + if (!msg.includes('exists') && !msg.includes('exist')) { + console.error(`[DaytonaSandboxFiles] Error creating directory ${path}:`, error) + throw error + } + } + } + + /** + * Daytona does not have native file watching. Returns undefined so that + * SandboxFileWatcher falls back to its polling implementation. + */ + watchDir?( + _path: string, + _callback: (event: FileWatchEvent) => void, + _options?: WatchOptions, + ): Promise { + // Return undefined to trigger polling fallback in SandboxFileWatcher + return undefined as any + } +} + +/** + * Daytona sandbox commands implementation. + * + * Background commands are emulated using Daytona process sessions. + * Output is streamed by polling session logs at a regular interval. + */ +class DaytonaSandboxCommands implements ISandboxCommands { + constructor(private workspace: any) {} + + async run(command: string, options?: CommandOptions): Promise { + const timeoutSec = options?.timeoutMs + ? Math.ceil(options.timeoutMs / 1000) + : undefined + + if (options?.background) { + return this._runBackground(command, options, timeoutSec) + } + + return this._runForeground(command, options, timeoutSec) + } + + private async _runForeground( + command: string, + options?: CommandOptions, + timeoutSec?: number, + ): Promise { + try { + const execOptions: Record = {} + if (timeoutSec !== undefined) { + execOptions.timeout = timeoutSec + } + if (options?.envs) { + execOptions.envVars = options.envs + } + + const response = await this.workspace.process.executeCommand( + command, + undefined, // sessionId + undefined, // executorName + execOptions, + ) + + const stdout = response.result ?? response.output ?? '' + const stderr = response.error ?? response.stderr ?? '' + const exitCode = response.exitCode ?? 0 + + if (stdout && options?.onStdout) { + options.onStdout(stdout) + } + if (stderr && options?.onStderr) { + options.onStderr(stderr) + } + + return { stdout, stderr, exitCode } + } catch (error) { + const msg = error instanceof Error ? error.message : String(error) + options?.onStderr?.(msg) + return { stdout: '', stderr: msg, exitCode: 1 } + } + } + + private async _runBackground( + command: string, + options?: CommandOptions, + timeoutSec?: number, + ): Promise { + const sessionId = `bg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + let stopped = false + let pollTimer: NodeJS.Timeout | null = null + + try { + // Create a dedicated process session + await this.workspace.process.createSession(sessionId) + + const execOptions: Record = { + timeout: timeoutSec ?? 0, // 0 = no timeout + } + if (options?.envs) { + execOptions.envVars = options.envs + } + + // Start the command in the session + const execResult = await this.workspace.process.executeSessionCommand( + sessionId, + { command, ...execOptions }, + ) + + const commandId = execResult?.commandId ?? execResult?.id + + // Poll for output to simulate streaming + let lastOffset = 0 + const poll = async () => { + if (stopped) return + try { + const logs = await this.workspace.process.getSessionCommandLogs( + sessionId, + commandId, + ) + const allOutput = logs?.output ?? logs?.result ?? logs ?? '' + if (typeof allOutput === 'string' && allOutput.length > lastOffset) { + const newData = allOutput.slice(lastOffset) + lastOffset = allOutput.length + if (newData && options?.onStdout) { + options.onStdout(newData) + } + } + } catch { + // Ignore polling errors + } + + if (!stopped) { + pollTimer = setTimeout(poll, 500) + } + } + + // Start polling output + poll() + + const stopFn = async () => { + stopped = true + if (pollTimer) { + clearTimeout(pollTimer) + pollTimer = null + } + try { + await this.workspace.process.deleteSession(sessionId) + } catch { + // Ignore cleanup errors + } + } + + return { + stdout: '', + stderr: '', + exitCode: 0, + stop: stopFn, + } + } catch (error) { + stopped = true + if (pollTimer) { + clearTimeout(pollTimer) + } + const msg = error instanceof Error ? error.message : String(error) + console.error('[DaytonaSandboxCommands] Error starting background command:', error) + options?.onStderr?.(msg) + return { stdout: '', stderr: msg, exitCode: 1 } + } + } +} + +/** + * Daytona sandbox instance that wraps a Daytona workspace. + */ +class DaytonaSandboxInstance implements ISandbox { + readonly files: DaytonaSandboxFiles + readonly commands: DaytonaSandboxCommands + private _previewHostCache: Map = new Map() + + constructor( + private workspace: any, + private serverUrl: string, + ) { + this.files = new DaytonaSandboxFiles(workspace) + this.commands = new DaytonaSandboxCommands(workspace) + } + + get sandboxId(): string { + return this.workspace.id + } + + /** + * Returns a hostname for the given port. + * + * Daytona preview URLs follow the pattern: + * {port}-{workspaceId}.{daytona-server-host} + * + * If cached from a prior async getPreviewUrl() call, that is used. + * Otherwise, constructs the URL from the workspace ID and server host. + */ + getHost(port: number): string { + if (this._previewHostCache.has(port)) { + return this._previewHostCache.get(port)! + } + + // Construct a Daytona preview URL from workspace ID and server host + const serverHost = this.serverUrl + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') + + return `${port}-${this.workspace.id}.${serverHost}` + } + + /** + * Async version of getHost - uses Daytona's native getPreviewLink API + * when available, caching the result for future sync getHost() calls. + */ + async getPreviewUrl(port: number): Promise { + try { + const previewInfo = await this.workspace.getPreviewLink(port) + const url = previewInfo?.url ?? previewInfo + if (url) { + // Cache the hostname for sync getHost() calls + const host = String(url).replace(/^https?:\/\//, '').split('/')[0] + this._previewHostCache.set(port, host) + return String(url).startsWith('http') ? String(url) : `https://${url}` + } + } catch (error) { + console.warn( + `[DaytonaSandboxInstance] getPreviewLink failed for port ${port}, using constructed URL:`, + error, + ) + } + return `https://${this.getHost(port)}` + } + + async setTimeout(ms: number): Promise { + // Daytona uses auto-stop interval in minutes + const minutes = Math.ceil(ms / 60000) + try { + if (typeof this.workspace.setAutostopInterval === 'function') { + await this.workspace.setAutostopInterval(minutes) + console.log(`[DaytonaSandboxInstance] Set auto-stop interval to ${minutes} minutes`) + } + } catch (error) { + console.warn('[DaytonaSandboxInstance] Failed to set auto-stop interval:', error) + } + } + + async close(): Promise { + // Daytona workspaces are managed externally; nothing to close per-connection + } +} + +/** + * Daytona provider implementation. + */ +export class DaytonaProvider implements ISandboxProvider { + private clientPromise: Promise | null = null + + private async getClient(): Promise { + if (!this.clientPromise) { + this.clientPromise = (async () => { + const sdk = await getDaytonaSDK() + const { Daytona } = sdk + + const config: Record = {} + if (process.env.DAYTONA_API_KEY) { + config.apiKey = process.env.DAYTONA_API_KEY + } + if (process.env.DAYTONA_SERVER_URL) { + config.serverUrl = process.env.DAYTONA_SERVER_URL + } + + return new Daytona(Object.keys(config).length > 0 ? config : undefined) + })() + } + return this.clientPromise + } + + private getServerUrl(): string { + return (process.env.DAYTONA_SERVER_URL ?? 'https://app.daytona.io') + .replace(/\/$/, '') + } + + async create(config: SandboxCreateConfig): Promise { + const client = await this.getClient() + + const image = + config.image ?? + process.env.DAYTONA_IMAGE ?? + 'imbios/bun-node:20-slim' + + const createParams: Record = { + image, + envVars: config.envs ?? {}, + labels: config.metadata ?? {}, + resources: { + cpu: 4, + memory: 4096, + }, + } + + if (config.timeoutMs) { + createParams.autoStopInterval = Math.ceil(config.timeoutMs / 60000) + } + + if (process.env.DAYTONA_TARGET) { + createParams.target = process.env.DAYTONA_TARGET + } + + console.log('[DaytonaProvider] Creating workspace with image:', image) + const workspace = await client.create(createParams) + console.log('[DaytonaProvider] Created workspace:', workspace.id) + + return new DaytonaSandboxInstance(workspace, this.getServerUrl()) + } + + async connect(sandboxId: string): Promise { + const client = await this.getClient() + + console.log('[DaytonaProvider] Connecting to workspace:', sandboxId) + const workspace = await client.get(sandboxId) + console.log('[DaytonaProvider] Connected to workspace:', workspace.id) + + return new DaytonaSandboxInstance(workspace, this.getServerUrl()) + } +} diff --git a/packages/sandbox/src/lib/providers/e2b.ts b/packages/sandbox/src/lib/providers/e2b.ts new file mode 100644 index 00000000..5a56e858 --- /dev/null +++ b/packages/sandbox/src/lib/providers/e2b.ts @@ -0,0 +1,120 @@ +/** + * E2B Sandbox Provider + * + * Adapter that wraps @e2b/code-interpreter Sandbox to implement ISandbox. + * This is the default provider. + */ + +import { Sandbox } from '@e2b/code-interpreter' +import type { + ISandbox, + ISandboxFiles, + ISandboxCommands, + ISandboxProvider, + SandboxCreateConfig, + CommandOptions, + CommandResult, + FileWatchEvent, + WatchHandle, + WatchOptions, +} from './types' + +class E2BSandboxFiles implements ISandboxFiles { + constructor(private sandbox: Sandbox) {} + + async write(path: string, content: string): Promise { + await this.sandbox.files.write(path, content) + } + + async read(path: string): Promise { + return this.sandbox.files.read(path) + } + + async exists(path: string): Promise { + return this.sandbox.files.exists(path) + } + + async makeDir(path: string): Promise { + await this.sandbox.files.makeDir(path) + } + + async watchDir( + path: string, + callback: (event: FileWatchEvent) => void, + options?: WatchOptions, + ): Promise { + // E2B native file watching + const handle = await this.sandbox.files.watchDir( + path, + callback as any, + options as any, + ) + return handle as unknown as WatchHandle + } +} + +class E2BSandboxCommands implements ISandboxCommands { + constructor(private sandbox: Sandbox) {} + + async run(command: string, options?: CommandOptions): Promise { + const result = await this.sandbox.commands.run(command, options as any) + return { + stdout: (result as any).stdout ?? '', + stderr: (result as any).stderr ?? '', + exitCode: (result as any).exitCode ?? 0, + } + } +} + +class E2BSandboxInstance implements ISandbox { + readonly files: E2BSandboxFiles + readonly commands: E2BSandboxCommands + + constructor(private _sandbox: Sandbox) { + this.files = new E2BSandboxFiles(_sandbox) + this.commands = new E2BSandboxCommands(_sandbox) + } + + get sandboxId(): string { + return this._sandbox.sandboxId + } + + getHost(port: number): string { + return this._sandbox.getHost(port) + } + + async getPreviewUrl(port: number): Promise { + return `https://${this._sandbox.getHost(port)}` + } + + async setTimeout(ms: number): Promise { + await this._sandbox.setTimeout(ms) + } + + async close(): Promise { + await this._sandbox.close() + } +} + +export class E2BProvider implements ISandboxProvider { + async create(config: SandboxCreateConfig): Promise { + if (!config.templateId) { + throw new Error('[E2BProvider] templateId is required to create an E2B sandbox') + } + + const sandbox = await Sandbox.create(config.templateId, { + metadata: config.metadata, + timeoutMs: config.timeoutMs, + envs: config.envs, + } as any) + + console.log(`[E2BProvider] Created sandbox: ${sandbox.sandboxId}`) + return new E2BSandboxInstance(sandbox) + } + + async connect(sandboxId: string): Promise { + const sandbox = await Sandbox.connect(sandboxId) + console.log(`[E2BProvider] Connected to sandbox: ${sandbox.sandboxId}`) + return new E2BSandboxInstance(sandbox) + } +} diff --git a/packages/sandbox/src/lib/providers/index.ts b/packages/sandbox/src/lib/providers/index.ts new file mode 100644 index 00000000..8327f797 --- /dev/null +++ b/packages/sandbox/src/lib/providers/index.ts @@ -0,0 +1,64 @@ +/** + * Sandbox Provider Factory + * + * Returns the active sandbox provider based on the SANDBOX_PROVIDER environment variable. + * + * Usage: + * import { getSandboxProvider } from '@react-native-vibe-code/sandbox/lib' + * + * const provider = getSandboxProvider() + * const sandbox = await provider.create({ templateId: 'my-template-id' }) + * // or + * const sandbox = await provider.connect(existingSandboxId) + * + * Supported values for SANDBOX_PROVIDER: + * - 'e2b' (default): E2B Code Interpreter (@e2b/code-interpreter) + * - 'daytona': Daytona Workspace (@daytonaio/sdk) + */ + +export * from './types' +export { E2BProvider } from './e2b' +export { DaytonaProvider } from './daytona' + +import type { ISandboxProvider, SandboxProviderName } from './types' +import { E2BProvider } from './e2b' +import { DaytonaProvider } from './daytona' + +let _providerInstance: ISandboxProvider | null = null + +/** + * Returns the singleton sandbox provider for the current process. + * Provider is determined by the SANDBOX_PROVIDER environment variable. + * + * @returns ISandboxProvider - the active provider + */ +export function getSandboxProvider(): ISandboxProvider { + if (_providerInstance) { + return _providerInstance + } + + const providerName = ( + (process.env.SANDBOX_PROVIDER as SandboxProviderName | undefined) ?? 'e2b' + ).toLowerCase() as SandboxProviderName + + switch (providerName) { + case 'daytona': + console.log('[SandboxProvider] Using Daytona provider') + _providerInstance = new DaytonaProvider() + break + case 'e2b': + default: + console.log('[SandboxProvider] Using E2B provider (default)') + _providerInstance = new E2BProvider() + break + } + + return _providerInstance +} + +/** + * Reset the provider singleton (useful for testing). + */ +export function resetSandboxProvider(): void { + _providerInstance = null +} diff --git a/packages/sandbox/src/lib/providers/types.ts b/packages/sandbox/src/lib/providers/types.ts new file mode 100644 index 00000000..32e031fa --- /dev/null +++ b/packages/sandbox/src/lib/providers/types.ts @@ -0,0 +1,111 @@ +/** + * Sandbox Provider Abstraction Layer + * + * Defines the unified interface for sandbox providers (E2B, Daytona, etc.) + * Provider is selected via the SANDBOX_PROVIDER environment variable: + * - 'e2b' (default): Uses @e2b/code-interpreter + * - 'daytona': Uses @daytonaio/sdk + */ + +export interface CommandResult { + stdout: string + stderr: string + exitCode: number + /** Stop a background command (only available for background commands) */ + stop?: () => Promise +} + +export interface CommandOptions { + timeoutMs?: number + requestTimeoutMs?: number + /** Run command in background mode (non-blocking) */ + background?: boolean + /** Environment variables to set for the command */ + envs?: Record + onStdout?: (data: string) => void + onStderr?: (data: string) => void +} + +export interface FileWatchEvent { + name?: string + path?: string + filename?: string + type?: string + eventType?: string + operation?: string +} + +export interface WatchOptions { + recursive?: boolean + timeoutMs?: number +} + +export interface WatchHandle { + stop: () => void | Promise + type?: string +} + +export interface ISandboxFiles { + write(path: string, content: string): Promise + read(path: string): Promise + exists(path: string): Promise + makeDir(path: string): Promise + /** Native file watching (may not be available on all providers - falls back to polling) */ + watchDir?( + path: string, + callback: (event: FileWatchEvent) => void, + options?: WatchOptions, + ): Promise +} + +export interface ISandboxCommands { + run(command: string, options?: CommandOptions): Promise +} + +/** + * Unified sandbox interface that all providers must implement. + * Mirrors the E2B Sandbox API for backwards compatibility. + */ +export interface ISandbox { + /** Unique sandbox/workspace identifier */ + readonly sandboxId: string + readonly files: ISandboxFiles + readonly commands: ISandboxCommands + /** + * Get the public hostname for a port (sync). + * For async preview URL resolution use getPreviewUrl() if available. + */ + getHost(port: number): string + /** Get public preview URL for a port (may be async for some providers) */ + getPreviewUrl?(port: number): Promise + /** Set sandbox inactivity timeout in milliseconds */ + setTimeout(ms: number): Promise + /** Optional: close/cleanup the sandbox connection */ + close?(): Promise +} + +export interface SandboxCreateConfig { + /** E2B template ID (used by E2B provider) */ + templateId?: string + /** Docker image name (used by Daytona provider) */ + image?: string + /** Key-value metadata/labels attached to the sandbox */ + metadata?: Record + /** Inactivity timeout in milliseconds */ + timeoutMs?: number + /** Environment variables to set in the sandbox */ + envs?: Record +} + +/** + * Interface for sandbox provider implementations. + */ +export interface ISandboxProvider { + /** Create a new sandbox instance */ + create(config: SandboxCreateConfig): Promise + /** Connect to an existing sandbox by ID */ + connect(sandboxId: string): Promise +} + +/** Supported sandbox provider names */ +export type SandboxProviderName = 'e2b' | 'daytona' diff --git a/packages/sandbox/src/lib/sandbox-file-watcher.ts b/packages/sandbox/src/lib/sandbox-file-watcher.ts index 09439f5d..7c2d14d3 100644 --- a/packages/sandbox/src/lib/sandbox-file-watcher.ts +++ b/packages/sandbox/src/lib/sandbox-file-watcher.ts @@ -1,4 +1,4 @@ -import { Sandbox } from '@e2b/code-interpreter' +import type { ISandbox, FileWatchEvent as ProviderFileWatchEvent, WatchHandle } from './providers/types' export interface FileChangeEvent { type: 'file_change' @@ -11,46 +11,53 @@ export interface FileChangeEvent { } export class SandboxFileWatcher { - private watchers = new Map() + private watchers = new Map() /** - * Start watching file changes in a sandbox using E2B's native file watching + * Start watching file changes in a sandbox using native file watching if available, + * falling back to polling for providers that don't support it (e.g. Daytona). + * * @param projectId - The project ID for the sandbox - * @param sandbox - The E2B sandbox instance + * @param sandbox - The sandbox instance (ISandbox) * @param onFileChange - Callback function when files change */ async startWatching( projectId: string, - sandbox: Sandbox, - onFileChange: (event: FileChangeEvent) => void + sandbox: ISandbox, + onFileChange: (event: FileChangeEvent) => void, ): Promise { // Stop existing watcher if any await this.stopWatching(projectId) try { - // Check if the sandbox has the files.watchDir method - if (typeof sandbox.files?.watchDir === 'function') { - // Use E2B's native file watching API - // Set timeoutMs: 0 to disable timeout for long-running watch operations - const watchHandle = await sandbox.files.watchDir('/home/user/app', async (event: any) => { - try { - // Extract the file path relative to the app directory - let filePath = event.name || event.path || event.filename || '' - if (filePath.startsWith('/home/user/app/')) { - filePath = filePath.replace('/home/user/app/', '') - } else if (filePath.startsWith('/home/user/app')) { - filePath = filePath.replace('/home/user/app', '') - } else if (filePath.startsWith('./')) { - filePath = filePath.replace('./', '') - } + // Check if the sandbox has native file watching support + const hasNativeWatch = typeof sandbox.files?.watchDir === 'function' - // Skip if no valid file path - if (!filePath) { - return - } + if (hasNativeWatch) { + // Try to use the native watchDir - if it returns undefined the provider + // signals no native support (e.g. Daytona), fall back to polling. + const watchHandleOrUndefined = await (sandbox.files.watchDir!( + '/home/user/app', + async (event: ProviderFileWatchEvent) => { + try { + // Extract the file path relative to the app directory + let filePath = event.name || event.path || event.filename || '' + if (filePath.startsWith('/home/user/app/')) { + filePath = filePath.replace('/home/user/app/', '') + } else if (filePath.startsWith('/home/user/app')) { + filePath = filePath.replace('/home/user/app', '') + } else if (filePath.startsWith('./')) { + filePath = filePath.replace('./', '') + } - // Filter out temporary files and directories - if (filePath.includes('.git/') || + // Skip if no valid file path + if (!filePath) { + return + } + + // Filter out temporary files and directories + if ( + filePath.includes('.git/') || filePath.includes('node_modules/') || filePath.includes('.expo/') || filePath.includes('.next/') || @@ -59,110 +66,158 @@ export class SandboxFileWatcher { filePath.endsWith('.tmp') || filePath.endsWith('.swp') || filePath.includes('~') || - filePath.startsWith('.')) { - return - } + filePath.startsWith('.') + ) { + return + } - // Map E2B event types to our action types - let action: 'created' | 'modified' | 'deleted' - const eventType = (event.type || event.eventType || event.operation || 'modified').toString().toLowerCase() + // Map event types to our action types + let action: 'created' | 'modified' | 'deleted' + const eventType = ( + event.type || + event.eventType || + event.operation || + 'modified' + ) + .toString() + .toLowerCase() + + if ( + eventType.includes('create') || + eventType.includes('add') || + eventType.includes('write') + ) { + action = 'created' + } else if ( + eventType.includes('delete') || + eventType.includes('remove') + ) { + action = 'deleted' + } else { + action = 'modified' + } - if (eventType.includes('create') || eventType.includes('add') || eventType.includes('write')) { - action = 'created' - } else if (eventType.includes('delete') || eventType.includes('remove')) { - action = 'deleted' - } else { - action = 'modified' + const changes = [ + { + path: filePath, + action, + timestamp: new Date().toISOString(), + }, + ] + + onFileChange({ + type: 'file_change', + projectId, + files: changes, + }) + } catch (error) { + console.error( + '❌ [FileWatcher] Error processing file change event:', + error, + ) } + }, + { recursive: true, timeoutMs: 0 }, + ) as any) - const changes = [{ - path: filePath, - action, - timestamp: new Date().toISOString() - }] + if (watchHandleOrUndefined != null) { + this.watchers.set(projectId, { + sandbox, + watchHandle: watchHandleOrUndefined, + }) + return + } - onFileChange({ - type: 'file_change', - projectId, - files: changes - }) + console.log( + `[FileWatcher] Native watchDir returned no handle for project ${projectId}, falling back to polling`, + ) + } - } catch (error) { - console.error('❌ [FileWatcher] Error processing E2B file change event:', error) - } - }, { recursive: true, timeoutMs: 0 }) - - this.watchers.set(projectId, { sandbox, watchHandle }) - } else { - // Initialize the last_check file - await sandbox.commands.run('touch /tmp/last_check') - - // OPTIMIZED: Fallback polling with exponential backoff - let pollCount = 0 - let pollInterval: NodeJS.Timeout - - const pollForChanges = async () => { - try { - const result = await sandbox.commands.run('find /home/user/app -type f -newer /tmp/last_check 2>/dev/null | head -10 && touch /tmp/last_check', { timeoutMs: 30000 }) - if (result.stdout && result.stdout.trim()) { - const changedFiles = result.stdout.trim().split('\n') - const changes = changedFiles.map(fullPath => { - let filePath = fullPath.replace('/home/user/app/', '') - return { - path: filePath, - action: 'modified' as const, - timestamp: new Date().toISOString() - } - }).filter(change => + // Polling fallback (used for providers without native file watching, e.g. Daytona) + await this._startPolling(projectId, sandbox, onFileChange) + } catch (error) { + console.error( + `❌ [FileWatcher] Failed to start file watcher for project ${projectId}:`, + error, + ) + this.watchers.delete(projectId) + } + } + + private async _startPolling( + projectId: string, + sandbox: ISandbox, + onFileChange: (event: FileChangeEvent) => void, + ): Promise { + // Initialize the last_check file + await sandbox.commands.run('touch /tmp/last_check') + + // OPTIMIZED: Polling with exponential backoff + let pollCount = 0 + let pollInterval: NodeJS.Timeout + + const pollForChanges = async () => { + try { + const result = await sandbox.commands.run( + 'find /home/user/app -type f -newer /tmp/last_check 2>/dev/null | head -10 && touch /tmp/last_check', + { timeoutMs: 30000 }, + ) + if (result.stdout && result.stdout.trim()) { + const changedFiles = result.stdout.trim().split('\n') + const changes = changedFiles + .map((fullPath) => { + const filePath = fullPath.replace('/home/user/app/', '') + return { + path: filePath, + action: 'modified' as const, + timestamp: new Date().toISOString(), + } + }) + .filter( + (change) => change.path && !change.path.includes('.git/') && !change.path.includes('node_modules/') && !change.path.includes('.expo/') && - !change.path.includes('.next/') - ) + !change.path.includes('.next/'), + ) - if (changes.length > 0) { - onFileChange({ - type: 'file_change', - projectId, - files: changes - }) - } - } - pollCount++ - } catch (error) { - console.error('❌ [FileWatcher] Polling error:', error) + if (changes.length > 0) { + onFileChange({ + type: 'file_change', + projectId, + files: changes, + }) } } + pollCount++ + } catch (error) { + console.error('❌ [FileWatcher] Polling error:', error) + } + } - // Smart polling: 5s for first 6 checks, then 15s, then 30s - const getNextInterval = () => { - if (pollCount < 6) return 5000 // First 30 seconds: poll every 5s - if (pollCount < 12) return 15000 // Next minute: poll every 15s - return 30000 // After that: poll every 30s - } - - const scheduleNextPoll = () => { - pollInterval = setTimeout(async () => { - await pollForChanges() - scheduleNextPoll() - }, getNextInterval()) - } + // Smart polling: 5s for first 6 checks, then 15s, then 30s + const getNextInterval = () => { + if (pollCount < 6) return 5000 // First 30 seconds: poll every 5s + if (pollCount < 12) return 15000 // Next minute: poll every 15s + return 30000 // After that: poll every 30s + } + const scheduleNextPoll = () => { + pollInterval = setTimeout(async () => { + await pollForChanges() scheduleNextPoll() + }, getNextInterval()) + } - const watchHandle = { - stop: () => clearInterval(pollInterval), - type: 'polling' - } - - this.watchers.set(projectId, { sandbox, watchHandle }) - } + scheduleNextPoll() - } catch (error) { - console.error(`❌ [FileWatcher] Failed to start E2B file watcher for project ${projectId}:`, error) - this.watchers.delete(projectId) + const watchHandle: WatchHandle = { + stop: () => clearTimeout(pollInterval), + type: 'polling', } + + this.watchers.set(projectId, { sandbox, watchHandle }) } /** @@ -172,8 +227,11 @@ export class SandboxFileWatcher { const watcher = this.watchers.get(projectId) if (watcher) { try { - // Stop the file watcher handle (E2B native or polling) - if (watcher.watchHandle && typeof watcher.watchHandle.stop === 'function') { + // Stop the file watcher handle (native or polling) + if ( + watcher.watchHandle && + typeof watcher.watchHandle.stop === 'function' + ) { if (watcher.watchHandle.type === 'polling') { watcher.watchHandle.stop() } else { @@ -181,7 +239,10 @@ export class SandboxFileWatcher { } } } catch (error) { - console.error(`❌ [FileWatcher] Error stopping file watcher for project ${projectId}:`, error) + console.error( + `❌ [FileWatcher] Error stopping file watcher for project ${projectId}:`, + error, + ) } finally { this.watchers.delete(projectId) } @@ -193,7 +254,7 @@ export class SandboxFileWatcher { */ async stopAllWatchers(): Promise { const projectIds = Array.from(this.watchers.keys()) - await Promise.all(projectIds.map(id => this.stopWatching(id))) + await Promise.all(projectIds.map((id) => this.stopWatching(id))) } /** diff --git a/packages/sandbox/src/lib/server-utils.ts b/packages/sandbox/src/lib/server-utils.ts index bb83f79b..922a10db 100644 --- a/packages/sandbox/src/lib/server-utils.ts +++ b/packages/sandbox/src/lib/server-utils.ts @@ -1,9 +1,9 @@ import { db, projects, eq } from '@react-native-vibe-code/database' -import { Sandbox } from '@e2b/code-interpreter' +import type { ISandbox } from './providers/types' import { detectAndNotifyRuntimeError } from '@react-native-vibe-code/error-manager/server' export async function startExpoServer( - sandbox: Sandbox, + sandbox: ISandbox, projectId?: string, customNgrokUrl?: string, ): Promise<{ url: string; serverReady: boolean; ngrokUrl?: string }> { diff --git a/packages/sandbox/templates/daytona-expo-template/Dockerfile b/packages/sandbox/templates/daytona-expo-template/Dockerfile new file mode 100644 index 00000000..7557e2fe --- /dev/null +++ b/packages/sandbox/templates/daytona-expo-template/Dockerfile @@ -0,0 +1,145 @@ +# Daytona Expo Sandbox Template +# +# This Dockerfile creates the same environment as the E2B expo template, +# but packaged for use with Daytona workspaces. +# +# Usage with Daytona SDK: +# const workspace = await daytona.create({ image: 'react-native-vibe-code/expo-sandbox:latest' }) +# +# Build and push to your container registry: +# docker build -t react-native-vibe-code/expo-sandbox:latest . +# docker push react-native-vibe-code/expo-sandbox:latest +# +# Required environment variables at runtime (set via DAYTONA_IMAGE_ENV or workspace envVars): +# - ANTHROPIC_API_KEY +# - EXPO_PUBLIC_PROJECT_ID (optional, set per-project) + +# Use imbios/bun-node image which comes with both Node.js and Bun pre-installed +FROM imbios/bun-node:20-slim + +# Fix dpkg issues and install system dependencies +RUN apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get update && \ + apt-get install -y --no-install-recommends --fix-broken \ + git \ + curl \ + wget \ + unzip \ + zip \ + ca-certificates \ + sudo \ + && rm -rf /var/lib/apt/lists/* + +# Create user if it doesn't exist and set up sudo +RUN if ! id user > /dev/null 2>&1; then \ + useradd -m -s /bin/bash user && \ + echo "user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; \ + fi + +# Install ngrok +RUN wget -q https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz \ + && tar -xzf ngrok-v3-stable-linux-amd64.tgz \ + && mv ngrok /usr/local/bin/ \ + && rm ngrok-v3-stable-linux-amd64.tgz + +# Set working directory +WORKDIR /home/user + +# Create app directory and set ownership +RUN mkdir -p /home/user/app && chown -R user:user /home/user/app + +# Change to app directory +WORKDIR /home/user/app + +# First, copy only package.json and bun.lock to leverage Bun's global cache +COPY --chown=user:user local-expo-app/package.json local-expo-app/bun.lock* ./ + +# Verify patches directory exists and copy it before bun install +COPY --chown=user:user local-expo-app/patches ./patches/ +RUN ls -la patches/ && echo "Patches directory found" + +# Switch to user for bun operations +USER user + +# Pre-install dependencies to populate Bun's global cache (~/.bun/install/cache) +RUN bun install + +# Switch back to root for file operations +USER root + +# Now copy the rest of the local expo app template (excluding node_modules) +COPY --chown=user:user local-expo-app/ ./ +RUN rm -rf node_modules + +# Ensure temp directories are writable for bun +RUN mkdir -p /tmp/bun-cache && chmod 777 /tmp/bun-cache +ENV BUN_INSTALL_CACHE_DIR=/tmp/bun-cache + +# Switch to user for bun operations +USER user + +# Run bun install again (will be instant due to cache) +RUN bun install + +# Verify patch was applied after bun install +RUN grep -q "ngrokurl" node_modules/@expo/cli/build/bin/cli && \ + echo "Patch successfully applied!" || \ + (echo "Patch NOT applied!" && exit 1) + +# Switch back to root +USER root + +# Expose the default Expo port +EXPOSE 8081 + +# Create claude-sdk directory in container root with proper permissions +WORKDIR /claude-sdk +RUN chmod 755 /claude-sdk && chown user:user /claude-sdk + +# Switch to user for bun operations +USER user + +# Initialize bun project for claude-sdk +RUN bun init -y + +# Install dependencies for claude-sdk +RUN bun install @anthropic-ai/claude-agent-sdk tsx execa + +# Create and set permissions for Metro cache directory +RUN mkdir -p /tmp/metro-cache +USER root +RUN chmod 777 /tmp/metro-cache +USER user +ENV TMPDIR=/tmp/metro-cache + +# Increase Node.js memory limit +ENV NODE_OPTIONS="--max_old_space_size=4096" + +# Install expo/ngrok +RUN bun install @expo/ngrok + +# Install dev dependencies for claude-sdk +RUN bun install --dev @types/node@^24.0.3 + +# Switch back to root for file operations +USER root + +# Copy the claude execution script (with image attachment support) +COPY templates/expo-template/claude-executor.ts index.ts + +# Copy the structure script +COPY templates/expo-template/get-structure.js get-structure.js + +# Copy the edit file script +COPY templates/expo-template/edit-file.js edit-file.js + +# Add start script to package.json +RUN node -e "const pkg = require('./package.json'); pkg.scripts = pkg.scripts || {}; pkg.scripts.start = 'tsx index.ts'; pkg.scripts['get-structure'] = 'node get-structure.js'; pkg.scripts['edit-file'] = 'node edit-file.js'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));" + +# Pre-create generated_code directory with proper permissions +RUN mkdir -p /claude-sdk/generated_code && chmod 755 /claude-sdk/generated_code && chown -R user:user /claude-sdk + +# Return to app directory and switch to user +WORKDIR /home/user/app +USER user diff --git a/packages/sandbox/templates/daytona-expo-template/daytona.yaml b/packages/sandbox/templates/daytona-expo-template/daytona.yaml new file mode 100644 index 00000000..0b0242f8 --- /dev/null +++ b/packages/sandbox/templates/daytona-expo-template/daytona.yaml @@ -0,0 +1,47 @@ +# Daytona Workspace Configuration for Expo Sandbox +# +# This file configures the Daytona workspace template for React Native Expo development. +# It mirrors the capabilities of the E2B expo-template. +# +# Usage with Daytona CLI: +# daytona create --config packages/sandbox/templates/daytona-expo-template/daytona.yaml +# +# Or programmatically via the @daytonaio/sdk: +# const workspace = await daytona.create({ +# image: '/react-native-vibe-code/expo-sandbox:latest', +# envVars: { ANTHROPIC_API_KEY: '...' }, +# resources: { cpu: 4, memory: 4096 }, +# }) + +name: expo-sandbox +description: React Native Expo sandbox for AI-powered app generation + +# Docker image (build from the Dockerfile in this directory and push to your registry) +# Replace with your actual registry URL after building: +# docker build -t /react-native-vibe-code/expo-sandbox:latest \ +# -f packages/sandbox/templates/daytona-expo-template/Dockerfile . +# docker push /react-native-vibe-code/expo-sandbox:latest +image: ubuntu:22.04 # Replace with your built image + +# Resource allocation (matches E2B template configuration) +resources: + cpu: 4 + memory: 4096 # MB + +# Exposed ports +ports: + - port: 8081 + name: expo-web + description: Expo web server + +# Environment variables (set sensitive values via Daytona secrets or DAYTONA_IMAGE_ENV) +# These are provided at runtime via the @daytonaio/sdk envVars option. +env: + NODE_OPTIONS: "--max_old_space_size=4096" + TMPDIR: "/tmp/metro-cache" + BUN_INSTALL_CACHE_DIR: "/tmp/bun-cache" + # ANTHROPIC_API_KEY: set at runtime + # NGROK_AUTHTOKEN: set at runtime + +# Auto-stop interval in minutes (equivalent to E2B's 1-hour timeout) +autoStopInterval: 60 diff --git a/packages/sandbox/templates/daytona-github-template/Dockerfile b/packages/sandbox/templates/daytona-github-template/Dockerfile new file mode 100644 index 00000000..183db0d6 --- /dev/null +++ b/packages/sandbox/templates/daytona-github-template/Dockerfile @@ -0,0 +1,163 @@ +# Daytona GitHub Sandbox Template +# +# This Dockerfile creates the same environment as the E2B github template, +# but packaged for use with Daytona workspaces. +# +# Usage with Daytona SDK: +# const workspace = await daytona.create({ image: 'react-native-vibe-code/github-sandbox:latest' }) +# +# Build and push to your container registry: +# docker build -t react-native-vibe-code/github-sandbox:latest . +# docker push react-native-vibe-code/github-sandbox:latest +# +# Required environment variables at runtime (set via DAYTONA_IMAGE_ENV or workspace envVars): +# - ANTHROPIC_API_KEY +# - GITHUB_TOKEN +# - GITHUB_OWNER + +# Use imbios/bun-node image which comes with both Node.js and Bun pre-installed +FROM imbios/bun-node:20-slim + +# Fix dpkg issues and install system dependencies +RUN apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get update && \ + apt-get install -y --no-install-recommends --fix-broken \ + git \ + curl \ + wget \ + unzip \ + zip \ + ca-certificates \ + sudo \ + && rm -rf /var/lib/apt/lists/* + +# Create user if it doesn't exist and set up sudo +RUN if ! id user > /dev/null 2>&1; then \ + useradd -m -s /bin/bash user && \ + echo "user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; \ + fi + +# Install ngrok +RUN wget -q https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz \ + && tar -xzf ngrok-v3-stable-linux-amd64.tgz \ + && mv ngrok /usr/local/bin/ \ + && rm ngrok-v3-stable-linux-amd64.tgz + +# Note: Configure ngrok auth token at runtime via environment variable or startup script + +WORKDIR /home/user + +# Create project directory +RUN mkdir -p /home/user/project + +# Increase Node.js memory limit +ENV NODE_OPTIONS="--max_old_space_size=4096" + +# Pre-populate bun's global cache with local-expo-app dependencies +# This makes `bun install` at runtime instant by using cached packages +WORKDIR /tmp/cache-warmup +RUN mkdir -p /tmp/cache-warmup && chown user:user /tmp/cache-warmup + +# Copy package files to warm up the cache +COPY --chown=user:user local-expo-app/package.json local-expo-app/bun.lock* ./ +COPY --chown=user:user local-expo-app/patches ./patches/ + +# Switch to user and install dependencies to populate bun's global cache +USER user +RUN bun install + +# Switch back to root and clean up temp directory (cache is now populated) +USER root +WORKDIR /home/user +RUN rm -rf /tmp/cache-warmup + +# Create a script to help with GitHub operations +RUN echo '#!/bin/bash\n\ +set -e\n\ +\n\ +PROJECT_ID=$1\n\ +GITHUB_OWNER=${GITHUB_OWNER:-"your-org"}\n\ +\n\ +if [ -z "$PROJECT_ID" ]; then\n\ + echo "Usage: github-setup.sh "\n\ + exit 1\n\ +fi\n\ +\n\ +if [ -z "$GITHUB_TOKEN" ]; then\n\ + echo "Error: GITHUB_TOKEN environment variable is required"\n\ + exit 1\n\ +fi\n\ +\n\ +REPO_NAME="project-${PROJECT_ID}"\n\ +\n\ +# Configure git\n\ +git config --global user.name "Daytona User"\n\ +git config --global user.email "user@daytona.dev"\n\ +\n\ +# Clone or pull the repository\n\ +cd /home/user\n\ +if [ -d "project/.git" ]; then\n\ + cd project\n\ + git pull origin main || true\n\ +else\n\ + git clone https://${GITHUB_TOKEN}@github.com/${GITHUB_OWNER}/${REPO_NAME}.git project || {\n\ + mkdir -p project\n\ + cd project\n\ + git init\n\ + git remote add origin https://${GITHUB_TOKEN}@github.com/${GITHUB_OWNER}/${REPO_NAME}.git\n\ + }\n\ +fi\n\ +\n\ +echo "GitHub setup complete for project ${PROJECT_ID}"' > /home/user/github-setup.sh + +RUN chmod +x /home/user/github-setup.sh + +# Create claude-sdk directory in container root with proper permissions +WORKDIR /claude-sdk +RUN chmod 755 /claude-sdk && chown user:user /claude-sdk + +# Switch to user for bun operations +USER user + +# Initialize bun project for claude-sdk +RUN bun init -y + +# Install dependencies for claude-sdk +# Migrated to claude-agent-sdk (formerly claude-code) +RUN bun install @anthropic-ai/claude-agent-sdk tsx execa + +# Install dev dependencies for claude-sdk +RUN bun install --dev @types/node@^24.0.3 + +# Switch back to root for file operations +USER root + +# Copy the claude execution script +COPY templates/expo-template/claude-executor.ts index.ts + +# Copy the test script +COPY templates/expo-template/claude-test.ts test.ts + +# Copy the structure script +COPY templates/expo-template/get-structure.js get-structure.js + +# Copy the edit file script +COPY templates/expo-template/edit-file.js edit-file.js + +# Copy the EAS login script +COPY templates/expo-template/eas-login.js eas-login.js +RUN chmod +x eas-login.js + +# Add start script and test-run script to package.json +RUN node -e "const pkg = require('./package.json'); pkg.scripts = pkg.scripts || {}; pkg.scripts.start = 'tsx index.ts'; pkg.scripts['test-run'] = 'tsx test.ts'; pkg.scripts['get-structure'] = 'node get-structure.js'; pkg.scripts['edit-file'] = 'node edit-file.js'; pkg.scripts['eas-login'] = 'node eas-login.js'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));" + +# Note: ANTHROPIC_API_KEY should be set at runtime via environment variables +# Do not hardcode API keys in the Docker image for security reasons + +# Pre-create generated_code directory with proper permissions +RUN mkdir -p /claude-sdk/generated_code && chmod 755 /claude-sdk/generated_code && chown -R user:user /claude-sdk + +# Return to user home directory and switch to user +WORKDIR /home/user +USER user diff --git a/packages/sandbox/templates/daytona-github-template/daytona.yaml b/packages/sandbox/templates/daytona-github-template/daytona.yaml new file mode 100644 index 00000000..aa581fe1 --- /dev/null +++ b/packages/sandbox/templates/daytona-github-template/daytona.yaml @@ -0,0 +1,40 @@ +# Daytona Workspace Configuration for GitHub Sandbox +# +# This file configures the Daytona workspace template for GitHub-integrated +# React Native development. It mirrors the capabilities of the E2B github-template. +# +# Usage with Daytona CLI: +# daytona create --config packages/sandbox/templates/daytona-github-template/daytona.yaml +# +# Or programmatically via the @daytonaio/sdk: +# const workspace = await daytona.create({ +# image: '/react-native-vibe-code/github-sandbox:latest', +# envVars: { ANTHROPIC_API_KEY: '...', GITHUB_TOKEN: '...', GITHUB_OWNER: '...' }, +# resources: { cpu: 4, memory: 4096 }, +# }) + +name: github-sandbox +description: React Native GitHub sandbox for AI-powered app generation with GitHub integration + +# Docker image (build from the Dockerfile in this directory and push to your registry) +# Replace with your actual registry URL after building: +# docker build -t /react-native-vibe-code/github-sandbox:latest \ +# -f packages/sandbox/templates/daytona-github-template/Dockerfile . +# docker push /react-native-vibe-code/github-sandbox:latest +image: ubuntu:22.04 # Replace with your built image + +# Resource allocation (matches E2B template configuration) +resources: + cpu: 4 + memory: 4096 # MB + +# Environment variables (set sensitive values via Daytona secrets or DAYTONA_IMAGE_ENV) +# These are provided at runtime via the @daytonaio/sdk envVars option. +env: + NODE_OPTIONS: "--max_old_space_size=4096" + # ANTHROPIC_API_KEY: set at runtime + # GITHUB_TOKEN: set at runtime + # GITHUB_OWNER: set at runtime + +# Auto-stop interval in minutes (equivalent to E2B's 1-hour timeout) +autoStopInterval: 60