Merge branch 'main' into morph-fast-apply
This commit is contained in:
@@ -92,7 +92,7 @@ export function executeSearchPlan(
|
||||
matchedPattern = pattern;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.warn(`[file-search] Invalid regex pattern: ${pattern}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { SandboxProvider, SandboxProviderConfig } from './types';
|
||||
import { E2BProvider } from './providers/e2b-provider';
|
||||
import { VercelProvider } from './providers/vercel-provider';
|
||||
|
||||
export class SandboxFactory {
|
||||
static create(provider?: string, config?: SandboxProviderConfig): SandboxProvider {
|
||||
// Use environment variable if provider not specified
|
||||
const selectedProvider = provider || process.env.SANDBOX_PROVIDER || 'e2b';
|
||||
|
||||
|
||||
switch (selectedProvider.toLowerCase()) {
|
||||
case 'e2b':
|
||||
return new E2BProvider(config || {});
|
||||
|
||||
case 'vercel':
|
||||
return new VercelProvider(config || {});
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown sandbox provider: ${selectedProvider}. Supported providers: e2b, vercel`);
|
||||
}
|
||||
}
|
||||
|
||||
static getAvailableProviders(): string[] {
|
||||
return ['e2b', 'vercel'];
|
||||
}
|
||||
|
||||
static isProviderAvailable(provider: string): boolean {
|
||||
switch (provider.toLowerCase()) {
|
||||
case 'e2b':
|
||||
return !!process.env.E2B_API_KEY;
|
||||
|
||||
case 'vercel':
|
||||
// Vercel can use OIDC (automatic) or PAT
|
||||
return !!process.env.VERCEL_OIDC_TOKEN ||
|
||||
(!!process.env.VERCEL_TOKEN && !!process.env.VERCEL_TEAM_ID && !!process.env.VERCEL_PROJECT_ID);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
import { Sandbox } from '@e2b/code-interpreter';
|
||||
import { SandboxProvider, SandboxInfo, CommandResult } from '../types';
|
||||
// SandboxProviderConfig available through parent class
|
||||
import { appConfig } from '@/config/app.config';
|
||||
|
||||
export class E2BProvider extends SandboxProvider {
|
||||
private existingFiles: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* Attempt to reconnect to an existing E2B sandbox
|
||||
*/
|
||||
async reconnect(sandboxId: string): Promise<boolean> {
|
||||
try {
|
||||
|
||||
// Try to connect to existing sandbox
|
||||
// Note: E2B SDK doesn't directly support reconnection, but we can try to recreate
|
||||
// For now, return false to indicate reconnection isn't supported
|
||||
// In the future, E2B may add this capability
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(`[E2BProvider] Failed to reconnect to sandbox ${sandboxId}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async createSandbox(): Promise<SandboxInfo> {
|
||||
try {
|
||||
|
||||
// Kill existing sandbox if any
|
||||
if (this.sandbox) {
|
||||
try {
|
||||
await this.sandbox.kill();
|
||||
} catch (e) {
|
||||
console.error('Failed to close existing sandbox:', e);
|
||||
}
|
||||
this.sandbox = null;
|
||||
}
|
||||
|
||||
// Clear existing files tracking
|
||||
this.existingFiles.clear();
|
||||
|
||||
// Create base sandbox
|
||||
this.sandbox = await Sandbox.create({
|
||||
apiKey: this.config.e2b?.apiKey || process.env.E2B_API_KEY,
|
||||
timeoutMs: this.config.e2b?.timeoutMs || appConfig.e2b.timeoutMs
|
||||
});
|
||||
|
||||
const sandboxId = (this.sandbox as any).sandboxId || Date.now().toString();
|
||||
const host = (this.sandbox as any).getHost(appConfig.e2b.vitePort);
|
||||
|
||||
|
||||
this.sandboxInfo = {
|
||||
sandboxId,
|
||||
url: `https://${host}`,
|
||||
provider: 'e2b',
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
// Set extended timeout on the sandbox instance if method available
|
||||
if (typeof this.sandbox.setTimeout === 'function') {
|
||||
this.sandbox.setTimeout(appConfig.e2b.timeoutMs);
|
||||
}
|
||||
|
||||
return this.sandboxInfo;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[E2BProvider] Error creating sandbox:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async runCommand(command: string): Promise<CommandResult> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
|
||||
const result = await this.sandbox.runCode(`
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
result = subprocess.run(${JSON.stringify(command.split(' '))},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
shell=False)
|
||||
|
||||
print("STDOUT:")
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print("\\nSTDERR:")
|
||||
print(result.stderr)
|
||||
print(f"\\nReturn code: {result.returncode}")
|
||||
`);
|
||||
|
||||
const output = result.logs.stdout.join('\n');
|
||||
const stderr = result.logs.stderr.join('\n');
|
||||
|
||||
return {
|
||||
stdout: output,
|
||||
stderr,
|
||||
exitCode: result.error ? 1 : 0,
|
||||
success: !result.error
|
||||
};
|
||||
}
|
||||
|
||||
async writeFile(path: string, content: string): Promise<void> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
const fullPath = path.startsWith('/') ? path : `/home/user/app/${path}`;
|
||||
|
||||
// Use the E2B filesystem API to write the file
|
||||
// Note: E2B SDK uses files.write() method
|
||||
if ((this.sandbox as any).files && typeof (this.sandbox as any).files.write === 'function') {
|
||||
// Use the files.write API if available
|
||||
await (this.sandbox as any).files.write(fullPath, Buffer.from(content));
|
||||
} else {
|
||||
// Fallback to Python code execution
|
||||
await this.sandbox.runCode(`
|
||||
import os
|
||||
|
||||
# Ensure directory exists
|
||||
dir_path = os.path.dirname("${fullPath}")
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
|
||||
# Write file
|
||||
with open("${fullPath}", 'w') as f:
|
||||
f.write(${JSON.stringify(content)})
|
||||
print(f"✓ Written: ${fullPath}")
|
||||
`);
|
||||
}
|
||||
|
||||
this.existingFiles.add(path);
|
||||
}
|
||||
|
||||
async readFile(path: string): Promise<string> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
const fullPath = path.startsWith('/') ? path : `/home/user/app/${path}`;
|
||||
|
||||
const result = await this.sandbox.runCode(`
|
||||
with open("${fullPath}", 'r') as f:
|
||||
content = f.read()
|
||||
print(content)
|
||||
`);
|
||||
|
||||
return result.logs.stdout.join('\n');
|
||||
}
|
||||
|
||||
async listFiles(directory: string = '/home/user/app'): Promise<string[]> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
const result = await this.sandbox.runCode(`
|
||||
import os
|
||||
import json
|
||||
|
||||
def list_files(path):
|
||||
files = []
|
||||
for root, dirs, filenames in os.walk(path):
|
||||
# Skip node_modules and .git
|
||||
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '.next', 'dist', 'build']]
|
||||
for filename in filenames:
|
||||
rel_path = os.path.relpath(os.path.join(root, filename), path)
|
||||
files.append(rel_path)
|
||||
return files
|
||||
|
||||
files = list_files("${directory}")
|
||||
print(json.dumps(files))
|
||||
`);
|
||||
|
||||
try {
|
||||
return JSON.parse(result.logs.stdout.join(''));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async installPackages(packages: string[]): Promise<CommandResult> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
const packageList = packages.join(' ');
|
||||
const flags = appConfig.packages.useLegacyPeerDeps ? '--legacy-peer-deps' : '';
|
||||
|
||||
|
||||
const result = await this.sandbox.runCode(`
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
|
||||
# Install packages
|
||||
result = subprocess.run(
|
||||
['npm', 'install', ${flags ? `'${flags}',` : ''} ${packages.map(p => `'${p}'`).join(', ')}],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
print("STDOUT:")
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print("\\nSTDERR:")
|
||||
print(result.stderr)
|
||||
print(f"\\nReturn code: {result.returncode}")
|
||||
`);
|
||||
|
||||
const output = result.logs.stdout.join('\n');
|
||||
const stderr = result.logs.stderr.join('\n');
|
||||
|
||||
// Restart Vite if configured
|
||||
if (appConfig.packages.autoRestartVite && !result.error) {
|
||||
await this.restartViteServer();
|
||||
}
|
||||
|
||||
return {
|
||||
stdout: output,
|
||||
stderr,
|
||||
exitCode: result.error ? 1 : 0,
|
||||
success: !result.error
|
||||
};
|
||||
}
|
||||
|
||||
async setupViteApp(): Promise<void> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
|
||||
// Write all files in a single Python script
|
||||
const setupScript = `
|
||||
import os
|
||||
import json
|
||||
|
||||
print('Setting up React app with Vite and Tailwind...')
|
||||
|
||||
# Create directory structure
|
||||
os.makedirs('/home/user/app/src', exist_ok=True)
|
||||
|
||||
# Package.json
|
||||
package_json = {
|
||||
"name": "sandbox-app",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"vite": "^4.3.9",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"postcss": "^8.4.31",
|
||||
"autoprefixer": "^10.4.16"
|
||||
}
|
||||
}
|
||||
|
||||
with open('/home/user/app/package.json', 'w') as f:
|
||||
json.dump(package_json, f, indent=2)
|
||||
print('✓ package.json')
|
||||
|
||||
# Vite config
|
||||
vite_config = """import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
hmr: false,
|
||||
allowedHosts: ['.e2b.app', '.e2b.dev', '.vercel.run', 'localhost', '127.0.0.1']
|
||||
}
|
||||
})"""
|
||||
|
||||
with open('/home/user/app/vite.config.js', 'w') as f:
|
||||
f.write(vite_config)
|
||||
print('✓ vite.config.js')
|
||||
|
||||
# Tailwind config
|
||||
tailwind_config = """/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}"""
|
||||
|
||||
with open('/home/user/app/tailwind.config.js', 'w') as f:
|
||||
f.write(tailwind_config)
|
||||
print('✓ tailwind.config.js')
|
||||
|
||||
# PostCSS config
|
||||
postcss_config = """export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}"""
|
||||
|
||||
with open('/home/user/app/postcss.config.js', 'w') as f:
|
||||
f.write(postcss_config)
|
||||
print('✓ postcss.config.js')
|
||||
|
||||
# Index.html
|
||||
index_html = """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sandbox App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
with open('/home/user/app/index.html', 'w') as f:
|
||||
f.write(index_html)
|
||||
print('✓ index.html')
|
||||
|
||||
# Main.jsx
|
||||
main_jsx = """import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)"""
|
||||
|
||||
with open('/home/user/app/src/main.jsx', 'w') as f:
|
||||
f.write(main_jsx)
|
||||
print('✓ src/main.jsx')
|
||||
|
||||
# App.jsx
|
||||
app_jsx = """function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
|
||||
<div className="text-center max-w-2xl">
|
||||
<p className="text-lg text-gray-400">
|
||||
Sandbox Ready<br/>
|
||||
Start building your React app with Vite and Tailwind CSS!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App"""
|
||||
|
||||
with open('/home/user/app/src/App.jsx', 'w') as f:
|
||||
f.write(app_jsx)
|
||||
print('✓ src/App.jsx')
|
||||
|
||||
# Index.css
|
||||
index_css = """@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background-color: rgb(17 24 39);
|
||||
}"""
|
||||
|
||||
with open('/home/user/app/src/index.css', 'w') as f:
|
||||
f.write(index_css)
|
||||
print('✓ src/index.css')
|
||||
|
||||
print('\\nAll files created successfully!')
|
||||
`;
|
||||
|
||||
await this.sandbox.runCode(setupScript);
|
||||
|
||||
// Install dependencies
|
||||
await this.sandbox.runCode(`
|
||||
import subprocess
|
||||
|
||||
print('Installing npm packages...')
|
||||
result = subprocess.run(
|
||||
['npm', 'install'],
|
||||
cwd='/home/user/app',
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print('✓ Dependencies installed successfully')
|
||||
else:
|
||||
print(f'⚠ Warning: npm install had issues: {result.stderr}')
|
||||
`);
|
||||
|
||||
// Start Vite dev server
|
||||
await this.sandbox.runCode(`
|
||||
import subprocess
|
||||
import os
|
||||
import time
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
|
||||
# Kill any existing Vite processes
|
||||
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
|
||||
time.sleep(1)
|
||||
|
||||
# Start Vite dev server
|
||||
env = os.environ.copy()
|
||||
env['FORCE_COLOR'] = '0'
|
||||
|
||||
process = subprocess.Popen(
|
||||
['npm', 'run', 'dev'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=env
|
||||
)
|
||||
|
||||
print(f'✓ Vite dev server started with PID: {process.pid}')
|
||||
print('Waiting for server to be ready...')
|
||||
`);
|
||||
|
||||
// Wait for Vite to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay));
|
||||
|
||||
// Track initial files
|
||||
this.existingFiles.add('src/App.jsx');
|
||||
this.existingFiles.add('src/main.jsx');
|
||||
this.existingFiles.add('src/index.css');
|
||||
this.existingFiles.add('index.html');
|
||||
this.existingFiles.add('package.json');
|
||||
this.existingFiles.add('vite.config.js');
|
||||
this.existingFiles.add('tailwind.config.js');
|
||||
this.existingFiles.add('postcss.config.js');
|
||||
}
|
||||
|
||||
async restartViteServer(): Promise<void> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
|
||||
await this.sandbox.runCode(`
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
|
||||
# Kill existing Vite process
|
||||
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
|
||||
time.sleep(2)
|
||||
|
||||
# Start Vite dev server
|
||||
env = os.environ.copy()
|
||||
env['FORCE_COLOR'] = '0'
|
||||
|
||||
process = subprocess.Popen(
|
||||
['npm', 'run', 'dev'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=env
|
||||
)
|
||||
|
||||
print(f'✓ Vite restarted with PID: {process.pid}')
|
||||
`);
|
||||
|
||||
// Wait for Vite to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay));
|
||||
}
|
||||
|
||||
getSandboxUrl(): string | null {
|
||||
return this.sandboxInfo?.url || null;
|
||||
}
|
||||
|
||||
getSandboxInfo(): SandboxInfo | null {
|
||||
return this.sandboxInfo;
|
||||
}
|
||||
|
||||
async terminate(): Promise<void> {
|
||||
if (this.sandbox) {
|
||||
try {
|
||||
await this.sandbox.kill();
|
||||
} catch (e) {
|
||||
console.error('Failed to terminate sandbox:', e);
|
||||
}
|
||||
this.sandbox = null;
|
||||
this.sandboxInfo = null;
|
||||
}
|
||||
}
|
||||
|
||||
isAlive(): boolean {
|
||||
return !!this.sandbox;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
import { Sandbox } from '@vercel/sandbox';
|
||||
import { SandboxProvider, SandboxInfo, CommandResult } from '../types';
|
||||
// SandboxProviderConfig available through parent class
|
||||
|
||||
export class VercelProvider extends SandboxProvider {
|
||||
private existingFiles: Set<string> = new Set();
|
||||
|
||||
async createSandbox(): Promise<SandboxInfo> {
|
||||
try {
|
||||
|
||||
// Kill existing sandbox if any
|
||||
if (this.sandbox) {
|
||||
try {
|
||||
await this.sandbox.stop();
|
||||
} catch (e) {
|
||||
console.error('Failed to stop existing sandbox:', e);
|
||||
}
|
||||
this.sandbox = null;
|
||||
}
|
||||
|
||||
// Clear existing files tracking
|
||||
this.existingFiles.clear();
|
||||
|
||||
// Create Vercel sandbox
|
||||
|
||||
const sandboxConfig: any = {
|
||||
timeout: 300000, // 5 minutes in ms
|
||||
runtime: 'node22', // Use node22 runtime for Vercel sandboxes
|
||||
ports: [5173] // Vite port
|
||||
};
|
||||
|
||||
// Add authentication based on environment variables
|
||||
if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) {
|
||||
sandboxConfig.teamId = process.env.VERCEL_TEAM_ID;
|
||||
sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID;
|
||||
sandboxConfig.token = process.env.VERCEL_TOKEN;
|
||||
} else if (process.env.VERCEL_OIDC_TOKEN) {
|
||||
sandboxConfig.oidcToken = process.env.VERCEL_OIDC_TOKEN;
|
||||
}
|
||||
|
||||
this.sandbox = await Sandbox.create(sandboxConfig);
|
||||
|
||||
const sandboxId = this.sandbox.sandboxId;
|
||||
// Sandbox created successfully
|
||||
|
||||
// Get the sandbox URL using the correct Vercel Sandbox API
|
||||
const sandboxUrl = this.sandbox.domain(5173);
|
||||
|
||||
this.sandboxInfo = {
|
||||
sandboxId,
|
||||
url: sandboxUrl,
|
||||
provider: 'vercel',
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
return this.sandboxInfo;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[VercelProvider] Error creating sandbox:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async runCommand(command: string): Promise<CommandResult> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Parse command into cmd and args (matching PR syntax)
|
||||
const parts = command.split(' ');
|
||||
const cmd = parts[0];
|
||||
const args = parts.slice(1);
|
||||
|
||||
// Vercel uses runCommand with cmd and args object (based on PR)
|
||||
const result = await this.sandbox.runCommand({
|
||||
cmd: cmd,
|
||||
args: args,
|
||||
cwd: '/vercel/sandbox',
|
||||
env: {}
|
||||
});
|
||||
|
||||
// Handle stdout and stderr - they might be functions in Vercel SDK
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
try {
|
||||
if (typeof result.stdout === 'function') {
|
||||
stdout = await result.stdout();
|
||||
} else {
|
||||
stdout = result.stdout || '';
|
||||
}
|
||||
} catch (e) {
|
||||
stdout = '';
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof result.stderr === 'function') {
|
||||
stderr = await result.stderr();
|
||||
} else {
|
||||
stderr = result.stderr || '';
|
||||
}
|
||||
} catch (e) {
|
||||
stderr = '';
|
||||
}
|
||||
|
||||
return {
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
exitCode: result.exitCode || 0,
|
||||
success: result.exitCode === 0
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
stdout: '',
|
||||
stderr: error.message || 'Command failed',
|
||||
exitCode: 1,
|
||||
success: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async writeFile(path: string, content: string): Promise<void> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
// Vercel sandbox default working directory is /vercel/sandbox
|
||||
const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`;
|
||||
|
||||
// Writing file to sandbox
|
||||
|
||||
// Based on Vercel SDK docs, writeFiles expects path and Buffer content
|
||||
try {
|
||||
const buffer = Buffer.from(content, 'utf-8');
|
||||
// Writing file with buffer
|
||||
|
||||
await this.sandbox.writeFiles([{
|
||||
path: fullPath,
|
||||
content: buffer
|
||||
}]);
|
||||
|
||||
this.existingFiles.add(path);
|
||||
} catch (writeError: any) {
|
||||
// Log detailed error information
|
||||
console.error(`[VercelProvider] writeFiles failed for ${fullPath}:`, {
|
||||
error: writeError,
|
||||
message: writeError?.message,
|
||||
response: writeError?.response,
|
||||
statusCode: writeError?.response?.status,
|
||||
responseData: writeError?.response?.data
|
||||
});
|
||||
|
||||
// Fallback to command-based approach if writeFiles fails
|
||||
// Falling back to command-based file write
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
|
||||
if (dir) {
|
||||
const mkdirResult = await this.sandbox.runCommand({
|
||||
cmd: 'mkdir',
|
||||
args: ['-p', dir]
|
||||
});
|
||||
// Directory created
|
||||
}
|
||||
|
||||
// Write file using echo and redirection
|
||||
const escapedContent = content
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/\n/g, '\\n');
|
||||
|
||||
const writeResult = await this.sandbox.runCommand({
|
||||
cmd: 'sh',
|
||||
args: ['-c', `echo "${escapedContent}" > "${fullPath}"`]
|
||||
});
|
||||
|
||||
// File written
|
||||
|
||||
if (writeResult.exitCode === 0) {
|
||||
this.existingFiles.add(path);
|
||||
} else {
|
||||
throw new Error(`Failed to write file via command: ${writeResult.stderr}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async readFile(path: string): Promise<string> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
// Vercel sandbox default working directory is /vercel/sandbox
|
||||
const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`;
|
||||
|
||||
const result = await this.sandbox.runCommand({
|
||||
cmd: 'cat',
|
||||
args: [fullPath]
|
||||
});
|
||||
|
||||
// Handle stdout and stderr - they might be functions in Vercel SDK
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
try {
|
||||
if (typeof result.stdout === 'function') {
|
||||
stdout = await result.stdout();
|
||||
} else {
|
||||
stdout = result.stdout || '';
|
||||
}
|
||||
} catch (e) {
|
||||
stdout = '';
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof result.stderr === 'function') {
|
||||
stderr = await result.stderr();
|
||||
} else {
|
||||
stderr = result.stderr || '';
|
||||
}
|
||||
} catch (e) {
|
||||
stderr = '';
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Failed to read file: ${stderr}`);
|
||||
}
|
||||
|
||||
return stdout;
|
||||
}
|
||||
|
||||
async listFiles(directory: string = '/vercel/sandbox'): Promise<string[]> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
const result = await this.sandbox.runCommand({
|
||||
cmd: 'sh',
|
||||
args: ['-c', `find ${directory} -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/.next/*" -not -path "*/dist/*" -not -path "*/build/*" | sed "s|^${directory}/||"`],
|
||||
cwd: '/'
|
||||
});
|
||||
|
||||
// Handle stdout - it might be a function in Vercel SDK
|
||||
let stdout = '';
|
||||
|
||||
try {
|
||||
if (typeof result.stdout === 'function') {
|
||||
stdout = await result.stdout();
|
||||
} else {
|
||||
stdout = result.stdout || '';
|
||||
}
|
||||
} catch (e) {
|
||||
stdout = '';
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return stdout.split('\n').filter((line: string) => line.trim() !== '');
|
||||
}
|
||||
|
||||
async installPackages(packages: string[]): Promise<CommandResult> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
const flags = process.env.NPM_FLAGS || '';
|
||||
|
||||
// Installing packages
|
||||
|
||||
// Build args array
|
||||
const args = ['install'];
|
||||
if (flags) {
|
||||
args.push(...flags.split(' '));
|
||||
}
|
||||
args.push(...packages);
|
||||
|
||||
const result = await this.sandbox.runCommand({
|
||||
cmd: 'npm',
|
||||
args: args,
|
||||
cwd: '/vercel/sandbox'
|
||||
});
|
||||
|
||||
// Handle stdout and stderr - they might be functions in Vercel SDK
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
try {
|
||||
if (typeof result.stdout === 'function') {
|
||||
stdout = await result.stdout();
|
||||
} else {
|
||||
stdout = result.stdout || '';
|
||||
}
|
||||
} catch (e) {
|
||||
stdout = '';
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof result.stderr === 'function') {
|
||||
stderr = await result.stderr();
|
||||
} else {
|
||||
stderr = result.stderr || '';
|
||||
}
|
||||
} catch (e) {
|
||||
stderr = '';
|
||||
}
|
||||
|
||||
// Restart Vite if configured and successful
|
||||
if (result.exitCode === 0 && process.env.AUTO_RESTART_VITE === 'true') {
|
||||
await this.restartViteServer();
|
||||
}
|
||||
|
||||
return {
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
exitCode: result.exitCode || 0,
|
||||
success: result.exitCode === 0
|
||||
};
|
||||
}
|
||||
|
||||
async setupViteApp(): Promise<void> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
// Setting up Vite app for sandbox
|
||||
|
||||
// Create directory structure
|
||||
const mkdirResult = await this.sandbox.runCommand({
|
||||
cmd: 'mkdir',
|
||||
args: ['-p', '/vercel/sandbox/src']
|
||||
});
|
||||
// Directory structure created
|
||||
|
||||
// Create package.json
|
||||
const packageJson = {
|
||||
name: "sandbox-app",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
scripts: {
|
||||
dev: "vite --host",
|
||||
build: "vite build",
|
||||
preview: "vite preview"
|
||||
},
|
||||
dependencies: {
|
||||
react: "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
devDependencies: {
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
vite: "^4.3.9",
|
||||
tailwindcss: "^3.3.0",
|
||||
postcss: "^8.4.31",
|
||||
autoprefixer: "^10.4.16"
|
||||
}
|
||||
};
|
||||
|
||||
await this.writeFile('package.json', JSON.stringify(packageJson, null, 2));
|
||||
|
||||
// Create vite.config.js
|
||||
const viteConfig = `import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
allowedHosts: [
|
||||
'.vercel.run', // Allow all Vercel sandbox domains
|
||||
'.e2b.dev', // Allow all E2B sandbox domains
|
||||
'localhost'
|
||||
],
|
||||
hmr: {
|
||||
clientPort: 443,
|
||||
protocol: 'wss'
|
||||
}
|
||||
}
|
||||
})`;
|
||||
|
||||
await this.writeFile('vite.config.js', viteConfig);
|
||||
|
||||
// Create tailwind.config.js
|
||||
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}`;
|
||||
|
||||
await this.writeFile('tailwind.config.js', tailwindConfig);
|
||||
|
||||
// Create postcss.config.js
|
||||
const postcssConfig = `export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}`;
|
||||
|
||||
await this.writeFile('postcss.config.js', postcssConfig);
|
||||
|
||||
// Create index.html
|
||||
const indexHtml = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sandbox App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
await this.writeFile('index.html', indexHtml);
|
||||
|
||||
// Create src/main.jsx
|
||||
const mainJsx = `import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)`;
|
||||
|
||||
await this.writeFile('src/main.jsx', mainJsx);
|
||||
|
||||
// Create src/App.jsx
|
||||
const appJsx = `function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
|
||||
<div className="text-center max-w-2xl">
|
||||
<p className="text-lg text-gray-400">
|
||||
Vercel Sandbox Ready<br/>
|
||||
Start building your React app with Vite and Tailwind CSS!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App`;
|
||||
|
||||
await this.writeFile('src/App.jsx', appJsx);
|
||||
|
||||
// Create src/index.css
|
||||
const indexCss = `@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background-color: rgb(17 24 39);
|
||||
}`;
|
||||
|
||||
await this.writeFile('src/index.css', indexCss);
|
||||
|
||||
// Installing npm dependencies
|
||||
|
||||
// Install dependencies
|
||||
try {
|
||||
const installResult = await this.sandbox.runCommand({
|
||||
cmd: 'npm',
|
||||
args: ['install'],
|
||||
cwd: '/vercel/sandbox'
|
||||
});
|
||||
|
||||
// npm install completed
|
||||
|
||||
if (installResult.exitCode === 0) {
|
||||
// Dependencies installed successfully
|
||||
} else {
|
||||
console.warn('[VercelProvider] npm install had issues:', installResult.stderr);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[VercelProvider] npm install error:', {
|
||||
message: error?.message,
|
||||
response: error?.response?.status,
|
||||
responseText: error?.text
|
||||
});
|
||||
// Try alternative approach - run as shell command
|
||||
try {
|
||||
const altResult = await this.sandbox.runCommand({
|
||||
cmd: 'sh',
|
||||
args: ['-c', 'cd /vercel/sandbox && npm install'],
|
||||
cwd: '/vercel/sandbox'
|
||||
});
|
||||
if (altResult.exitCode === 0) {
|
||||
// Alternative npm install succeeded
|
||||
} else {
|
||||
console.warn('[VercelProvider] Alternative npm install also had issues:', altResult.stderr);
|
||||
}
|
||||
} catch (altError) {
|
||||
console.error('[VercelProvider] Alternative npm install also failed:', altError);
|
||||
console.warn('[VercelProvider] Continuing without npm install - packages may need to be installed manually');
|
||||
}
|
||||
}
|
||||
|
||||
// Start Vite dev server
|
||||
// Starting Vite dev server
|
||||
|
||||
// Kill any existing Vite processes
|
||||
await this.sandbox.runCommand({
|
||||
cmd: 'sh',
|
||||
args: ['-c', 'pkill -f vite || true'],
|
||||
cwd: '/'
|
||||
});
|
||||
|
||||
// Start Vite in background
|
||||
await this.sandbox.runCommand({
|
||||
cmd: 'sh',
|
||||
args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'],
|
||||
cwd: '/vercel/sandbox'
|
||||
});
|
||||
|
||||
// Vite server started in background
|
||||
|
||||
// Wait for Vite to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 7000));
|
||||
|
||||
// Track initial files
|
||||
this.existingFiles.add('src/App.jsx');
|
||||
this.existingFiles.add('src/main.jsx');
|
||||
this.existingFiles.add('src/index.css');
|
||||
this.existingFiles.add('index.html');
|
||||
this.existingFiles.add('package.json');
|
||||
this.existingFiles.add('vite.config.js');
|
||||
this.existingFiles.add('tailwind.config.js');
|
||||
this.existingFiles.add('postcss.config.js');
|
||||
}
|
||||
|
||||
async restartViteServer(): Promise<void> {
|
||||
if (!this.sandbox) {
|
||||
throw new Error('No active sandbox');
|
||||
}
|
||||
|
||||
// Restarting Vite server
|
||||
|
||||
// Kill existing Vite process
|
||||
await this.sandbox.runCommand({
|
||||
cmd: 'sh',
|
||||
args: ['-c', 'pkill -f vite || true'],
|
||||
cwd: '/'
|
||||
});
|
||||
|
||||
// Wait a moment
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Start Vite in background
|
||||
await this.sandbox.runCommand({
|
||||
cmd: 'sh',
|
||||
args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'],
|
||||
cwd: '/vercel/sandbox'
|
||||
});
|
||||
|
||||
// Vite server started in background
|
||||
|
||||
// Wait for Vite to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 7000));
|
||||
}
|
||||
|
||||
getSandboxUrl(): string | null {
|
||||
return this.sandboxInfo?.url || null;
|
||||
}
|
||||
|
||||
getSandboxInfo(): SandboxInfo | null {
|
||||
return this.sandboxInfo;
|
||||
}
|
||||
|
||||
async terminate(): Promise<void> {
|
||||
if (this.sandbox) {
|
||||
try {
|
||||
await this.sandbox.stop();
|
||||
} catch (e) {
|
||||
console.error('Failed to terminate sandbox:', e);
|
||||
}
|
||||
this.sandbox = null;
|
||||
this.sandboxInfo = null;
|
||||
}
|
||||
}
|
||||
|
||||
isAlive(): boolean {
|
||||
return !!this.sandbox;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { SandboxProvider } from './types';
|
||||
import { SandboxFactory } from './factory';
|
||||
|
||||
interface SandboxInfo {
|
||||
sandboxId: string;
|
||||
provider: SandboxProvider;
|
||||
createdAt: Date;
|
||||
lastAccessed: Date;
|
||||
}
|
||||
|
||||
class SandboxManager {
|
||||
private sandboxes: Map<string, SandboxInfo> = new Map();
|
||||
private activeSandboxId: string | null = null;
|
||||
|
||||
/**
|
||||
* Get or create a sandbox provider for the given sandbox ID
|
||||
*/
|
||||
async getOrCreateProvider(sandboxId: string): Promise<SandboxProvider> {
|
||||
// Check if we already have this sandbox
|
||||
const existing = this.sandboxes.get(sandboxId);
|
||||
if (existing) {
|
||||
existing.lastAccessed = new Date();
|
||||
return existing.provider;
|
||||
}
|
||||
|
||||
// Try to reconnect to existing sandbox
|
||||
|
||||
try {
|
||||
const provider = SandboxFactory.create();
|
||||
|
||||
// For E2B provider, try to reconnect
|
||||
if (provider.constructor.name === 'E2BProvider') {
|
||||
// E2B sandboxes can be reconnected using the sandbox ID
|
||||
const reconnected = await (provider as any).reconnect(sandboxId);
|
||||
if (reconnected) {
|
||||
this.sandboxes.set(sandboxId, {
|
||||
sandboxId,
|
||||
provider,
|
||||
createdAt: new Date(),
|
||||
lastAccessed: new Date()
|
||||
});
|
||||
this.activeSandboxId = sandboxId;
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
// For Vercel or if reconnection failed, return the new provider
|
||||
// The caller will need to handle creating a new sandbox
|
||||
return provider;
|
||||
} catch (error) {
|
||||
console.error(`[SandboxManager] Error reconnecting to sandbox ${sandboxId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new sandbox
|
||||
*/
|
||||
registerSandbox(sandboxId: string, provider: SandboxProvider): void {
|
||||
this.sandboxes.set(sandboxId, {
|
||||
sandboxId,
|
||||
provider,
|
||||
createdAt: new Date(),
|
||||
lastAccessed: new Date()
|
||||
});
|
||||
this.activeSandboxId = sandboxId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active sandbox provider
|
||||
*/
|
||||
getActiveProvider(): SandboxProvider | null {
|
||||
if (!this.activeSandboxId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sandbox = this.sandboxes.get(this.activeSandboxId);
|
||||
if (sandbox) {
|
||||
sandbox.lastAccessed = new Date();
|
||||
return sandbox.provider;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific sandbox provider
|
||||
*/
|
||||
getProvider(sandboxId: string): SandboxProvider | null {
|
||||
const sandbox = this.sandboxes.get(sandboxId);
|
||||
if (sandbox) {
|
||||
sandbox.lastAccessed = new Date();
|
||||
return sandbox.provider;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active sandbox
|
||||
*/
|
||||
setActiveSandbox(sandboxId: string): boolean {
|
||||
if (this.sandboxes.has(sandboxId)) {
|
||||
this.activeSandboxId = sandboxId;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate a sandbox
|
||||
*/
|
||||
async terminateSandbox(sandboxId: string): Promise<void> {
|
||||
const sandbox = this.sandboxes.get(sandboxId);
|
||||
if (sandbox) {
|
||||
try {
|
||||
await sandbox.provider.terminate();
|
||||
} catch (error) {
|
||||
console.error(`[SandboxManager] Error terminating sandbox ${sandboxId}:`, error);
|
||||
}
|
||||
this.sandboxes.delete(sandboxId);
|
||||
|
||||
if (this.activeSandboxId === sandboxId) {
|
||||
this.activeSandboxId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate all sandboxes
|
||||
*/
|
||||
async terminateAll(): Promise<void> {
|
||||
const promises = Array.from(this.sandboxes.values()).map(sandbox =>
|
||||
sandbox.provider.terminate().catch(err =>
|
||||
console.error(`[SandboxManager] Error terminating sandbox ${sandbox.sandboxId}:`, err)
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
this.sandboxes.clear();
|
||||
this.activeSandboxId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old sandboxes (older than maxAge milliseconds)
|
||||
*/
|
||||
async cleanup(maxAge: number = 3600000): Promise<void> {
|
||||
const now = new Date();
|
||||
const toDelete: string[] = [];
|
||||
|
||||
for (const [id, info] of this.sandboxes.entries()) {
|
||||
const age = now.getTime() - info.lastAccessed.getTime();
|
||||
if (age > maxAge) {
|
||||
toDelete.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of toDelete) {
|
||||
await this.terminateSandbox(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const sandboxManager = new SandboxManager();
|
||||
|
||||
// Also maintain backward compatibility with global state
|
||||
declare global {
|
||||
var sandboxManager: SandboxManager;
|
||||
}
|
||||
|
||||
// Ensure the global reference points to our singleton
|
||||
global.sandboxManager = sandboxManager;
|
||||
@@ -0,0 +1,65 @@
|
||||
export interface SandboxFile {
|
||||
path: string;
|
||||
content: string;
|
||||
lastModified?: number;
|
||||
}
|
||||
|
||||
export interface SandboxInfo {
|
||||
sandboxId: string;
|
||||
url: string;
|
||||
provider: 'e2b' | 'vercel';
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface CommandResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface SandboxProviderConfig {
|
||||
e2b?: {
|
||||
apiKey: string;
|
||||
timeoutMs?: number;
|
||||
template?: string;
|
||||
};
|
||||
vercel?: {
|
||||
teamId?: string;
|
||||
projectId?: string;
|
||||
token?: string;
|
||||
authMethod?: 'oidc' | 'pat';
|
||||
};
|
||||
}
|
||||
|
||||
export abstract class SandboxProvider {
|
||||
protected config: SandboxProviderConfig;
|
||||
protected sandbox: any;
|
||||
protected sandboxInfo: SandboxInfo | null = null;
|
||||
|
||||
constructor(config: SandboxProviderConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
abstract createSandbox(): Promise<SandboxInfo>;
|
||||
abstract runCommand(command: string): Promise<CommandResult>;
|
||||
abstract writeFile(path: string, content: string): Promise<void>;
|
||||
abstract readFile(path: string): Promise<string>;
|
||||
abstract listFiles(directory?: string): Promise<string[]>;
|
||||
abstract installPackages(packages: string[]): Promise<CommandResult>;
|
||||
abstract getSandboxUrl(): string | null;
|
||||
abstract getSandboxInfo(): SandboxInfo | null;
|
||||
abstract terminate(): Promise<void>;
|
||||
abstract isAlive(): boolean;
|
||||
|
||||
// Optional methods that providers can override
|
||||
async setupViteApp(): Promise<void> {
|
||||
// Default implementation for setting up a Vite React app
|
||||
throw new Error('setupViteApp not implemented for this provider');
|
||||
}
|
||||
|
||||
async restartViteServer(): Promise<void> {
|
||||
// Default implementation for restarting Vite
|
||||
throw new Error('restartViteServer not implemented for this provider');
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user