diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df4ab5b..32be74c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: pull_request: + types: [opened, synchronize, reopened] branches: [main, master] push: branches: [main, master] @@ -52,7 +53,8 @@ jobs: run: pnpm run build - name: release preview with pkg-pr-new working-directory: packages/sdk - run: pnpm dlx pkg-pr-new publish + run: pnpm dlx pkg-pr-new publish --force env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_SHA: ${{ github.sha }} diff --git a/biome.json b/biome.json index 6ce9912..ba3ee87 100644 --- a/biome.json +++ b/biome.json @@ -33,7 +33,8 @@ "noExplicitAny": "off" }, "style": { - "noNonNullAssertion": "off" + "noNonNullAssertion": "off", + "noUnusedTemplateLiteral": "off" }, "correctness": { "noUnusedVariables": "warn" diff --git a/packages/sdk/examples/daytona-full-lifecycle.ts b/packages/sdk/examples/daytona-full-lifecycle.ts new file mode 100644 index 0000000..4745c69 --- /dev/null +++ b/packages/sdk/examples/daytona-full-lifecycle.ts @@ -0,0 +1,513 @@ +/** + * Daytona Full Lifecycle Example + * + * This example demonstrates a complete sandbox lifecycle workflow using Daytona SDK: + * 1. Create and start a sandbox + * 2. Fetch sandbox information + * 3. Git clone a repository + * 4. Call analyze API to get entrypoint + * 5. Write entrypoint.sh file + * 6. Configure npm registry + * 7. Start application server + * + * Usage: + * # From project root: + * bun packages/sdk/examples/daytona-full-lifecycle.ts + * + * # From packages/sdk directory: + * bun examples/daytona-full-lifecycle.ts + * + * # From examples directory: + * bun daytona-full-lifecycle.ts + * + * # With environment variable: + * export DAYTONA_API_KEY=your-api-key + * bun daytona-full-lifecycle.ts + * + * # Or inline: + * DAYTONA_API_KEY=your-api-key bun daytona-full-lifecycle.ts + * + * Requirements: + * - DAYTONA_API_KEY environment variable must be set (can be in .env file) + * - @daytonaio/sdk package must be installed: npm install @daytonaio/sdk + */ + +import { config as loadEnv } from 'dotenv' +import { existsSync } from 'node:fs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { Daytona } from '@daytonaio/sdk' + +// Load environment variables from .env file +// Try multiple locations: current directory, examples directory, project root +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const envPaths = [ + resolve(__dirname, '.env'), // examples/.env + resolve(__dirname, '../.env'), // packages/sdk/.env + resolve(__dirname, '../../.env'), // project root .env + resolve(process.cwd(), '.env'), // current working directory .env +] + +let envLoaded = false +for (const envPath of envPaths) { + if (existsSync(envPath)) { + loadEnv({ path: envPath, override: false }) + console.log(`✅ Loaded environment variables from ${envPath}`) + envLoaded = true + break + } +} + +if (!envLoaded) { + console.warn('⚠️ .env file not found in any of these locations:') + for (const path of envPaths) { + console.warn(` - ${path}`) + } + console.warn(' Using system environment variables (export DAYTONA_API_KEY=...)') +} + +// Check for DAYTONA_API_KEY +if (!process.env.DAYTONA_API_KEY) { + console.error('') + console.error('❌ Missing required environment variable: DAYTONA_API_KEY') + console.error('') + console.error('Please set it using one of these methods:') + console.error('') + console.error('1. Export in shell:') + console.error(' export DAYTONA_API_KEY=your-api-key') + console.error(' bun daytona-full-lifecycle.ts') + console.error('') + console.error('2. Create .env file in project root:') + console.error(' echo "DAYTONA_API_KEY=your-api-key" > .env') + console.error('') + console.error('3. Pass inline:') + console.error(' DAYTONA_API_KEY=your-api-key bun daytona-full-lifecycle.ts') + console.error('') + console.error('To get your API key:') + console.error(' 1. Go to https://www.daytona.io/dashboard') + console.error(' 2. Create a new API key') + console.error('') + process.exit(1) +} + +// Helper function: generate unique name +const generateSandboxName = (prefix: string) => { + const timestamp = Date.now() + const random = Math.floor(Math.random() * 1000) + const sanitizedPrefix = prefix.replace(/\./g, '-') + return `example-${sanitizedPrefix}-${timestamp}-${random}` +} + +// Helper function: wait for server startup with smart detection +async function waitForServerStartup( + sandbox: any, // eslint-disable-line @typescript-eslint/no-explicit-any + maxWaitTime = 180000 +): Promise { + const startTime = Date.now() + const checkInterval = 3000 // Check every 3 seconds + + console.log('') + console.log('⏳ Waiting for server to start...') + console.log(' Checking logs and process status...') + console.log('') + + // Wait 10 seconds first to let package installation start + await new Promise(resolve => setTimeout(resolve, 10000)) + + while (Date.now() - startTime < maxWaitTime) { + try { + // Check sandbox state (Daytona uses 'state' property) + // States: 'STARTED', 'STOPPED', 'DELETED', 'ARCHIVED' + const state = (sandbox as any).state || 'STARTED' + + if (state === 'STOPPED' || state === 'DELETED' || state === 'ARCHIVED') { + console.log('') + console.error(`❌ Sandbox stopped with state: ${state}`) + return false + } + + // Display progress + const elapsed = Math.floor((Date.now() - startTime) / 1000) + process.stdout.write(`\r State: ${state} | Elapsed: ${elapsed}s`) + + await new Promise(resolve => setTimeout(resolve, checkInterval)) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + console.log(`\n⚠️ Check failed: ${errorMessage}, retrying...`) + await new Promise(resolve => setTimeout(resolve, checkInterval)) + } + } + + console.log('') + console.warn(`⚠️ Server did not start within ${maxWaitTime / 1000}s`) + return false +} + +async function main() { + // Initialize Daytona client + const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY! }) + const name = generateSandboxName('daytona-full-lifecycle') + const REPO_URL = 'https://github.com/zjy365/reddit-ai-assistant-extension' + const REPO_NAME = 'reddit-ai-assistant-extension' + const ANALYZE_API_URL = 'https://pgitgrfugqfk.usw.sealos.io/analyze' + + try { + const overallStartTime = Date.now() + console.log('🚀 Starting Daytona full lifecycle example...') + console.log(`📦 Creating sandbox: ${name}`) + + // 1. Create Sandbox + const createStartTime = Date.now() + const sandbox = await daytona.create({ + name, + language: 'typescript', // or 'node', 'python', etc. + }) as any // eslint-disable-line @typescript-eslint/no-explicit-any + const createDuration = Date.now() - createStartTime + console.log(`✅ Sandbox created: ${sandbox.name || name} (${(createDuration / 1000).toFixed(2)}s)`) + + // 2. Wait for sandbox to be ready + console.log('⏳ Waiting for sandbox to be ready...') + const waitStartTime = Date.now() + // Daytona sandbox states: 'STARTED', 'STOPPED', 'DELETED', 'ARCHIVED' + let state = (sandbox as any).state || 'UNKNOWN' + const startTime = Date.now() + + while (state !== 'STARTED' && Date.now() - startTime < 60000) { + await new Promise(resolve => setTimeout(resolve, 2000)) + state = (sandbox as any).state || 'STARTED' + process.stdout.write('.') + } + const waitDuration = Date.now() - waitStartTime + const totalStartupTime = Date.now() - overallStartTime + console.log('') + console.log(`✅ Sandbox is ${state}`) + console.log(` Sandbox ID: ${(sandbox as any).id || 'N/A'}`) + console.log(` ⏱️ Startup time: ${(waitDuration / 1000).toFixed(2)}s (wait) + ${(createDuration / 1000).toFixed(2)}s (create) = ${(totalStartupTime / 1000).toFixed(2)}s (total)`) + + // 3. Get user root directory and check HOME + console.log('') + console.log('🔍 Checking user root directory and HOME...') + let userRootDir = '/home/daytona' // Default fallback + try { + // Get user root directory using Daytona SDK method + if (typeof (sandbox as any).getUserRootDir === 'function') { + userRootDir = await (sandbox as any).getUserRootDir() + console.log(`📁 User root directory: ${userRootDir}`) + } + + // Check HOME using executeCommand for shell commands + const homeResult = await sandbox.process.executeCommand('echo $HOME') + const homeDir = homeResult.result?.trim() || userRootDir + console.log(`📁 HOME: ${homeDir}`) + if (homeDir !== userRootDir) { + userRootDir = homeDir + } + } catch (error) { + console.warn('⚠️ Could not check directories:', error instanceof Error ? error.message : String(error)) + console.log(`📁 Using default root directory: ${userRootDir}`) + } + + // Build absolute path for repository directory + const REPO_DIR = `${userRootDir}/${REPO_NAME}` + console.log(`📁 Repository will be cloned to: ${REPO_DIR}`) + + // 4. Clean up directory first to avoid clone conflicts + // console.log('') + // console.log('🧹 Cleaning up directory...') + // try { + // // Use fs module to delete directory if it exists + // const deleteCode = ` + // const fs = require('fs'); + // const path = require('path'); + // try { + // if (fs.existsSync('${REPO_DIR}')) { + // fs.rmSync('${REPO_DIR}', { recursive: true, force: true }); + // console.log('Directory deleted'); + // } else { + // console.log('Directory does not exist'); + // } + // } catch (e) { + // console.log('Cleanup skipped:', e.message); + // } + // ` + // await sandbox.process.codeRun(deleteCode) + // } catch { + // // Ignore errors if directory doesn't exist + // } + + // 5. Git clone repository + console.log('') + console.log(`📥 Cloning repository: ${REPO_URL}`) + let cloneSuccess = false + const maxRetries = 3 + + // Try using Daytona SDK git.clone first + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + console.log(` Attempt ${attempt}/${maxRetries} using git.clone...`) + // Use Daytona SDK git.clone(url, targetDir) + // targetDir is relative to workspace, or absolute path starting with / + await sandbox.git.clone(REPO_URL, REPO_DIR) + console.log('✅ Repository cloned successfully') + cloneSuccess = true + break + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + const isLastAttempt = attempt === maxRetries + + if (errorMessage.includes('connection refused') || errorMessage.includes('dial tcp')) { + console.warn(`⚠️ Network error (attempt ${attempt}/${maxRetries}): ${errorMessage}`) + if (!isLastAttempt) { + console.log(' Retrying in 3 seconds...') + await new Promise(resolve => setTimeout(resolve, 3000)) + continue + } + } + + if (isLastAttempt) { + // Fallback: try using git command directly + console.log('') + console.log('📝 Trying fallback: using git command directly...') + try { + const gitCloneResult = await sandbox.process.executeCommand( + `git clone ${REPO_URL} ${REPO_DIR}`, + '.' + ) + if (gitCloneResult.exitCode === 0) { + console.log('✅ Repository cloned successfully (using git command)') + cloneSuccess = true + break + } + throw new Error(`Git clone failed: ${gitCloneResult.result}`) + } catch (fallbackError) { + const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError) + throw new Error(`Failed to clone repository after ${maxRetries} attempts and fallback: ${errorMessage}. Fallback error: ${fallbackMessage}`) + } + } + } + } + + if (!cloneSuccess) { + throw new Error('Failed to clone repository: All attempts failed') + } + + // Verify repository was cloned using fs.listFiles + console.log('📋 Verifying repository contents...') + try { + const files = await sandbox.fs.listFiles(REPO_DIR) + console.log(`📁 Found ${files.length} items in repository`) + for (const file of files.slice(0, 10)) { + console.log(` ${(file as any).isDir ? '📁' : '📄'} ${(file as any).name}${(file as any).size ? ` (${(file as any).size} bytes)` : ''}`) + } + if (files.length > 10) { + console.log(` ... and ${files.length - 10} more items`) + } + } catch (error) { + console.warn('⚠️ Could not list files:', error instanceof Error ? error.message : String(error)) + } + + // 6. Call analyze API using fetch with retry logic + console.log('') + console.log('🔍 Calling analyze API...') + + // Helper function to call analyze API with timeout and retry + const callAnalyzeAPI = async (retries = 3, timeout = 60000) => { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + // Create abort controller for timeout + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), timeout) + + const response = await fetch(ANALYZE_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + repo_url: REPO_URL, + }), + signal: controller.signal, + keepalive: true, + }) + + clearTimeout(timeoutId) + + if (!response.ok) { + throw new Error(`Analyze API failed: ${response.statusText}`) + } + + return await response.json() + } catch (error) { + const isLastAttempt = attempt === retries + const errorMessage = error instanceof Error ? error.message : String(error) + + if (isLastAttempt) { + throw new Error(`Analyze API failed after ${retries} attempts: ${errorMessage}`) + } + + console.log(`⚠️ Attempt ${attempt} failed: ${errorMessage}`) + console.log(`🔄 Retrying (${attempt + 1}/${retries})...`) + + // Exponential backoff: wait 2s, 4s, 8s... + await new Promise(resolve => setTimeout(resolve, 2000 * attempt)) + } + } + } + + const analyzeData = await callAnalyzeAPI() + console.log('✅ Analyze API response received') + console.log(`📝 Entrypoint length: ${analyzeData.entrypoint?.length || 0} characters`) + + // 7. Check Node.js and npm versions + console.log('') + console.log('🔍 Checking Node.js and npm versions...') + const nodeVersionResult = await sandbox.process.executeCommand('node -v') + console.log(`📦 Node.js version: ${nodeVersionResult.result?.trim() || 'N/A'}`) + + const npmVersionResult = await sandbox.process.executeCommand('npm -v') + console.log(`📦 npm version: ${npmVersionResult.result?.trim() || 'N/A'}`) + + // 8. Check package manager requirements + console.log('') + console.log('🔧 Checking package manager requirements...') + + const usesPnpm = analyzeData.entrypoint?.includes('pnpm') || false + + if (usesPnpm) { + console.log('📦 Detected pnpm usage...') + try { + const pnpmVersionResult = await sandbox.process.executeCommand('pnpm -v') + console.log(`📦 pnpm version: ${pnpmVersionResult.result?.trim() || 'N/A'}`) + } catch (error) { + console.warn('⚠️ pnpm not available:', error instanceof Error ? error.message : String(error)) + } + } + + // 9. Prepare entrypoint.sh with command fixes + const entrypointPath = `${REPO_DIR}/entrypoint.sh` + console.log('') + console.log(`💾 Preparing entrypoint.sh...`) + + const entrypointScript = analyzeData.entrypoint + .replace(/pnpm\s+(dev|start|build)\s+--\s+-/g, 'pnpm $1 -') + .replace(/npm\s+(dev|start|build)\s+--\s+-/g, 'npm run $1 -') + + // Write entrypoint.sh file using fs.uploadFile + try { + // Convert script to Buffer + const fileContent = Buffer.from(entrypointScript, 'utf-8') + + // Upload file using Daytona SDK fs.uploadFile + await sandbox.fs.uploadFile(fileContent, entrypointPath) + + // Set file permissions to executable (755) + await sandbox.fs.setFilePermissions(entrypointPath, { mode: '755' }) + + console.log('✅ entrypoint.sh written successfully') + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + throw new Error(`Failed to write entrypoint.sh: ${errorMessage}`) + } + + // 10. Configure npm registry + console.log('') + console.log('🔧 Configuring npm registry...') + + const expectedRegistry = 'https://registry.npmmirror.com' + + const registryResult = await sandbox.process.executeCommand( + `npm config set registry ${expectedRegistry}`, + REPO_DIR + ) + if (registryResult.exitCode !== 0) { + console.warn(`⚠️ Failed to set npm registry: ${registryResult.result}`) + } else { + console.log(`✅ npm registry set to: ${expectedRegistry}`) + } + + // 11. Start entrypoint.sh (run asynchronously in background) + console.log('') + console.log('🚀 Starting application via entrypoint.sh...') + + // Start the server process + let serverProcess: any + try { + // Try using process.start if available + if (sandbox.process && typeof (sandbox.process as any).start === 'function') { + serverProcess = await (sandbox.process as any).start({ + command: `bash ${entrypointPath} development`, + cwd: REPO_DIR, + }) + console.log(`✅ Application started!`) + console.log(` Process ID: ${serverProcess.id || 'N/A'}`) + } else { + // Fallback: use executeCommand for background execution + console.log('📝 Starting server in background...') + // Use executeCommand with nohup to run in background + serverProcess = await sandbox.process.executeCommand( + `nohup bash ${entrypointPath} development > /tmp/server.log 2>&1 & echo $!`, + REPO_DIR + ) + console.log(`✅ Application started!`) + console.log(` Process output: ${serverProcess.result?.trim() || 'N/A'}`) + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + throw new Error(`Failed to start server: ${errorMessage}`) + } + + // Wait for server startup + const isReady = await waitForServerStartup(sandbox, 180000) + + if (!isReady) { + console.warn('⚠️ Server may not have started within timeout, but continuing...') + } + + // Get preview URL for port 3000 + console.log('') + console.log('🔗 Getting preview URL for port 3000...') + try { + // Try different methods to get preview URL + let previewUrl: string + if (typeof (sandbox as any).getPreviewLink === 'function') { + const previewLink = await (sandbox as any).getPreviewLink(3000) + previewUrl = typeof previewLink === 'string' ? previewLink : (previewLink?.url || String(previewLink)) + } else if ((sandbox as any).preview && typeof (sandbox as any).preview.getUrl === 'function') { + previewUrl = await (sandbox as any).preview.getUrl(3000) + } else { + // Fallback: construct URL from sandbox info + const sandboxInfo = (sandbox as any).getInfo ? await (sandbox as any).getInfo() : {} + previewUrl = sandboxInfo.previewUrl || `https://${sandboxInfo.id || name || 'sandbox'}.daytona.io:3000` + } + console.log(`✅ Preview URL: ${previewUrl}`) + } catch (error) { + console.warn('⚠️ Failed to get preview URL:', error instanceof Error ? error.message : String(error)) + console.warn(' This might be because the sandbox does not have port 3000 configured') + } + + console.log('') + console.log('🎉 Daytona full lifecycle example completed successfully!') + console.log('') + console.log('📋 Summary:') + console.log(` Sandbox: ${name}`) + console.log(` Repository: ${REPO_URL}`) + console.log(` Project Dir: ${REPO_DIR}`) + console.log(` Server: npm run dev`) + console.log('') + console.log('💡 Cleanup: Delete sandbox when done') + console.log(` await sandbox.delete()`) + + } catch (error) { + console.error('❌ Error occurred:', error) + throw error + } +} + +// Run the example +main().catch((error) => { + console.error('Failed to run example:', error) + process.exit(1) +}) + diff --git a/packages/sdk/examples/full-lifecycle.ts b/packages/sdk/examples/full-lifecycle.ts new file mode 100644 index 0000000..0556e06 --- /dev/null +++ b/packages/sdk/examples/full-lifecycle.ts @@ -0,0 +1,214 @@ +/** + * Devbox Full Lifecycle Example - TEST VERSION + * Testing echo $HOME only + */ + +import { config as loadEnv } from 'dotenv' +import { existsSync, readFileSync } from 'node:fs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { DevboxSDK } from '../src/core/devbox-sdk' +import { DevboxRuntime } from '../src/api/types' +import { parseKubeconfigServerUrl } from '../src/utils/kubeconfig' + +// Load environment variables from .env file +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const envPaths = [ + resolve(__dirname, '.env'), + resolve(__dirname, '../.env'), + resolve(__dirname, '../../.env'), + resolve(process.cwd(), '.env'), +] + +let envLoaded = false +for (const envPath of envPaths) { + if (existsSync(envPath)) { + loadEnv({ path: envPath, override: false }) + console.log(`✅ Loaded environment variables from ${envPath}`) + envLoaded = true + break + } +} + +if (!envLoaded) { + console.warn('⚠️ .env file not found, using system environment variables') +} + +if (!process.env.KUBECONFIG) { + console.error('❌ Missing required environment variable: KUBECONFIG') + process.exit(1) +} + +let kubeconfigContent = process.env.KUBECONFIG + +if (!kubeconfigContent.includes('apiVersion') && existsSync(kubeconfigContent)) { + kubeconfigContent = readFileSync(kubeconfigContent, 'utf-8') +} else if (kubeconfigContent.includes('\\n')) { + kubeconfigContent = kubeconfigContent.replace(/\\n/g, '\n') +} + +const kubeconfigUrl = parseKubeconfigServerUrl(kubeconfigContent) +if (!kubeconfigUrl) { + console.error('❌ Failed to parse API server URL from kubeconfig') + process.exit(1) +} + +const SDK_CONFIG = { + kubeconfig: kubeconfigContent, + http: { + timeout: 300000, + retries: 3, + rejectUnauthorized: false, + }, +} + +const generateDevboxName = (prefix: string) => { + const timestamp = Date.now() + const random = Math.floor(Math.random() * 1000) + const sanitizedPrefix = prefix.replace(/\./g, '-') + return `example-${sanitizedPrefix}-${timestamp}-${random}` +} + +async function main() { + const sdk = new DevboxSDK(SDK_CONFIG) + const name = generateDevboxName('test-home') + + try { + console.log('🚀 Starting test...') + console.log(`📦 Creating devbox: ${name}`) + + const devbox = await sdk.createDevbox({ + name, + runtime: DevboxRuntime.TEST_AGENT, + resource: { cpu: 1, memory: 2 }, + }) + + console.log(`✅ Devbox created: ${devbox.name}`) + console.log('⏳ Starting devbox...') + + await devbox.start() + let currentDevbox = await sdk.getDevbox(name) + const startTime = Date.now() + + while (currentDevbox.status !== 'Running' && Date.now() - startTime < 30000) { + await new Promise(resolve => setTimeout(resolve, 2000)) + currentDevbox = await sdk.getDevbox(name) + process.stdout.write('.') + } + + console.log('') + console.log(`✅ Devbox is ${currentDevbox.status}`) + + // TEST: Check HOME environment variable + // Now both methods work because all commands are wrapped with sh -c by default + console.log('') + console.log('🔍 Testing echo $HOME...') + + // Method 1: Direct command (now automatically wrapped with sh -c) + const homeResult1 = await currentDevbox.execSync({ + command: 'echo', + args: ['$HOME'], + }) + console.log('Method 1 (echo $HOME):', homeResult1) + console.log(`📁 HOME: ${homeResult1.stdout.trim()}`) + + // Method 2: Using environment variable in command + const homeResult2 = await currentDevbox.execSync({ + command: 'echo', + args: ['My home is $HOME'], + }) + console.log('Method 2 (echo My home is $HOME):', homeResult2) + console.log(`📁 Result: ${homeResult2.stdout.trim()}`) + + // Method 3: Using pipes (shell feature) + const homeResult3 = await currentDevbox.execSync({ + command: 'echo $HOME | wc -c', + }) + console.log('Method 3 (pipe test):', homeResult3) + console.log(`📁 HOME length: ${homeResult3.stdout.trim()} characters`) + +/* + // 原有的其他操作都已注释 + // const REPO_URL = 'https://github.com/zjy365/reddit-ai-assistant-extension' + // const REPO_DIR = '/home/devbox/project/reddit-ai-assistant-extension' + // const ANALYZE_API_URL = 'https://pgitgrfugqfk.usw.sealos.io/analyze' + + // 3. Fetch devbox info to verify it's ready + // const fetchedDevbox = await sdk.getDevbox(name) + // console.log(`📋 Devbox info: ${fetchedDevbox.name} - ${fetchedDevbox.status}`) + + // 4. Clean up directory first to avoid clone conflicts and permission issues + // console.log('🧹 Cleaning up directory...') + // try { + // await currentDevbox.execSync({ + // command: 'rm', + // args: ['-rf', REPO_DIR], + // }) + // } catch { + // // Ignore errors if directory doesn't exist + // } + + // 5. Git clone repository + // console.log(`📥 Cloning repository: ${REPO_URL}`) + // await currentDevbox.git.clone({ + // url: REPO_URL, + // targetDir: REPO_DIR, + // }) + // console.log('✅ Repository cloned successfully') + + // Verify repository was cloned by checking if directory exists + // const repoFiles = await currentDevbox.listFiles(REPO_DIR) + // console.log(`📁 Found ${repoFiles.files.length} files in repository`) + + // List directory contents using ls command + // console.log('📋 Listing directory contents:') + // const lsResult = await currentDevbox.execSync({ + // command: 'ls', + // args: ['-la', REPO_DIR], + // }) + // console.log(lsResult.stdout) + + // 6. Call analyze API using fetch with retry logic + // console.log('🔍 Calling analyze API...') + // const callAnalyzeAPI = async (retries = 3, timeout = 60000) => { ... } + // const analyzeData = await callAnalyzeAPI() + + // 7. Check Node.js and npm versions + // const nodeVersionResult = await currentDevbox.execSync({ ... }) + // const npmVersionResult = await currentDevbox.execSync({ ... }) + + // 8. Enable pnpm via corepack (if needed) + // if (usesPnpm) { ... } + + // 9. Prepare entrypoint.sh with command fixes + // const entrypointPath = `${REPO_DIR}/entrypoint.sh` + // await currentDevbox.writeFile(entrypointPath, entrypointScript, { mode: 0o755 }) + + // 11. Configure npm registry + // await currentDevbox.execSync({ command: 'npm', args: ['config', 'set', 'registry', expectedRegistry] }) + + // 12. Start entrypoint.sh + // const serverProcess = await currentDevbox.executeCommand({ ... }) + // const isReady = await waitForServerStartup(currentDevbox, serverProcess.processId, 3000, 180000) + + // Get preview URL for port 3000 + // const previewLink = await currentDevbox.getPreviewLink(3000) +*/ + + console.log('') + console.log('🎉 Test completed!') + + } catch (error) { + console.error('❌ Error occurred:', error) + throw error + } finally { + await sdk.close() + } +} + +main().catch((error) => { + console.error('Failed to run example:', error) + process.exit(1) +}) diff --git a/packages/sdk/src/api/client.ts b/packages/sdk/src/api/client.ts index 5d3b0bd..28f2863 100644 --- a/packages/sdk/src/api/client.ts +++ b/packages/sdk/src/api/client.ts @@ -290,9 +290,40 @@ export class DevboxAPI { const response = await this.httpClient.post(this.endpoints.devboxCreate(), { data: request, }) - const responseData = response.data as { data: DevboxCreateResponse } + + if (!response || !response.data) { + throw new Error( + `Invalid API response: response or response.data is undefined. Response: ${JSON.stringify(response)}` + ) + } + + // Handle two possible response formats: + // 1. { data: DevboxCreateResponse } - standard format + // 2. DevboxCreateResponse - direct data + let createResponse: DevboxCreateResponse + + if (typeof response.data === 'object' && 'data' in response.data) { + // Format 1: { data: DevboxCreateResponse } + const responseData = response.data as { data: DevboxCreateResponse } + if (!responseData.data) { + throw new Error( + `Invalid API response structure: expected { data: DevboxCreateResponse }, but data field is undefined. Full response: ${JSON.stringify(response.data)}` + ) + } + createResponse = responseData.data + } else { + // Format 2: direct DevboxCreateResponse + createResponse = response.data as DevboxCreateResponse + } + + if (!createResponse || !createResponse.name) { + throw new Error( + `Invalid DevboxCreateResponse: missing 'name' field. Response data: ${JSON.stringify(createResponse)}` + ) + } + return this.transformCreateResponseToDevboxInfo( - responseData.data, + createResponse, config.runtime, config.resource ) diff --git a/packages/sdk/src/core/devbox-instance.ts b/packages/sdk/src/core/devbox-instance.ts index 2ba0709..4ab07a2 100644 --- a/packages/sdk/src/core/devbox-instance.ts +++ b/packages/sdk/src/core/devbox-instance.ts @@ -38,6 +38,7 @@ import type { SyncExecutionResponse, TimeRange, TransferResult, + WaitForReadyOptions, // WatchRequest, // Temporarily disabled - ws module removed WriteOptions, } from './types' @@ -657,21 +658,30 @@ export class DevboxInstance { // Process execution /** * Execute a process asynchronously + * All commands are automatically executed through shell (sh -c) for consistent behavior + * This ensures environment variables, pipes, redirections, etc. work as expected * @param options Process execution options * @returns Process execution response with process_id and pid */ async executeCommand(options: ProcessExecOptions): Promise { const urlResolver = this.sdk.getUrlResolver() + + // Build full command string + let fullCommand = options.command + if (options.args && options.args.length > 0) { + fullCommand = `${options.command} ${options.args.join(' ')}` + } + + // Wrap with sh -c for shell feature support (env vars, pipes, etc.) return await urlResolver.executeWithConnection(this.name, async client => { const response = await client.post( API_ENDPOINTS.CONTAINER.PROCESS.EXEC, { body: { - command: options.command, - args: options.args, + command: 'sh', + args: ['-c', fullCommand], cwd: options.cwd, env: options.env, - shell: options.shell, timeout: options.timeout, }, } @@ -682,21 +692,30 @@ export class DevboxInstance { /** * Execute a process synchronously and wait for completion + * All commands are automatically executed through shell (sh -c) for consistent behavior + * This ensures environment variables, pipes, redirections, etc. work as expected * @param options Process execution options * @returns Synchronous execution response with stdout, stderr, and exit code */ async execSync(options: ProcessExecOptions): Promise { const urlResolver = this.sdk.getUrlResolver() + + // Build full command string + let fullCommand = options.command + if (options.args && options.args.length > 0) { + fullCommand = `${options.command} ${options.args.join(' ')}` + } + + // Wrap with sh -c for shell feature support (env vars, pipes, etc.) return await urlResolver.executeWithConnection(this.name, async client => { const response = await client.post( API_ENDPOINTS.CONTAINER.PROCESS.EXEC_SYNC, { body: { - command: options.command, - args: options.args, + command: 'sh', + args: ['-c', fullCommand], cwd: options.cwd, env: options.env, - shell: options.shell, timeout: options.timeout, }, } @@ -742,10 +761,11 @@ export class DevboxInstance { /** * Build shell command to execute code + * Note: sh -c wrapper is now handled by wrapCommandWithShell * @param code Code string to execute * @param language Programming language ('node' or 'python') * @param argv Command line arguments - * @returns Shell command string + * @returns Shell command string (without sh -c wrapper) */ private buildCodeCommand(code: string, language: 'node' | 'python', argv?: string[]): string { const base64Code = Buffer.from(code).toString('base64') @@ -753,10 +773,10 @@ export class DevboxInstance { if (language === 'python') { // Python: python3 -u -c "exec(__import__('base64').b64decode('').decode())" - return `sh -c 'python3 -u -c "exec(__import__(\\"base64\\").b64decode(\\"${base64Code}\\").decode())"${argvStr}'` + return `python3 -u -c "exec(__import__(\\"base64\\").b64decode(\\"${base64Code}\\").decode())"${argvStr}` } // Node.js: echo | base64 --decode | node -e "$(cat)" - return `sh -c 'echo ${base64Code} | base64 --decode | node -e "$(cat)"${argvStr}'` + return `echo ${base64Code} | base64 --decode | node -e "$(cat)"${argvStr}` } /** @@ -866,24 +886,55 @@ export class DevboxInstance { try { const urlResolver = this.sdk.getUrlResolver() return await urlResolver.checkDevboxHealth(this.name) - } catch (error) { + } catch { return false } } /** * Wait for the Devbox to be ready and healthy - * @param timeout Timeout in milliseconds (default: 300000 = 5 minutes) - * @param checkInterval Check interval in milliseconds (default: 2000) + * @param timeoutOrOptions Timeout in milliseconds (for backward compatibility) or options object + * @param checkInterval Check interval in milliseconds (for backward compatibility, ignored if first param is options) */ - async waitForReady(timeout = 300000, checkInterval = 2000): Promise { + async waitForReady( + timeoutOrOptions?: number | WaitForReadyOptions, + checkInterval = 2000 + ): Promise { + // Handle backward compatibility: if first param is a number, treat as old API + // If no params provided, use exponential backoff (new default behavior) + const options: WaitForReadyOptions = + typeof timeoutOrOptions === 'number' + ? { + timeout: timeoutOrOptions, + checkInterval, + // For backward compatibility: if explicitly called with numbers, use fixed interval + useExponentialBackoff: false, + } + : timeoutOrOptions ?? { + // Default: use exponential backoff when called without params + useExponentialBackoff: true, + } + + const { + timeout = 300000, // 5 minutes + checkInterval: fixedInterval, + useExponentialBackoff = fixedInterval === undefined, + initialCheckInterval = 200, // 0.2 seconds - faster initial checks + maxCheckInterval = 5000, // 5 seconds + backoffMultiplier = 1.5, + } = options + const startTime = Date.now() + let currentInterval = useExponentialBackoff ? initialCheckInterval : (fixedInterval ?? 2000) + let checkCount = 0 + let lastStatus = this.status while (Date.now() - startTime < timeout) { try { // 1. Check Devbox status via API await this.refreshInfo() + // If status changed to Running, immediately check health (don't wait for next interval) if (this.status === 'Running') { // 2. Check health status via Bun server const healthy = await this.isHealthy() @@ -891,13 +942,36 @@ export class DevboxInstance { if (healthy) { return } + + // If status is Running but not healthy yet, use shorter interval for health checks + // This helps detect when health becomes available faster + if (lastStatus !== 'Running') { + // Status just changed to Running, reset interval to check health more frequently + currentInterval = Math.min(initialCheckInterval * 2, 1000) // Max 1s for health checks + checkCount = 1 + } + } else if (lastStatus !== this.status) { + // Status changed but not Running yet, reset interval to check more frequently + currentInterval = initialCheckInterval + checkCount = 0 } - } catch (error) { + + lastStatus = this.status + } catch { // Continue waiting on error } + // Calculate next interval for exponential backoff + if (useExponentialBackoff) { + currentInterval = Math.min( + initialCheckInterval * (backoffMultiplier ** checkCount), + maxCheckInterval + ) + checkCount++ + } + // Wait before next check - await new Promise(resolve => setTimeout(resolve, checkInterval)) + await new Promise(resolve => setTimeout(resolve, currentInterval)) } throw new Error(`Devbox '${this.name}' did not become ready within ${timeout}ms`) diff --git a/packages/sdk/src/core/devbox-sdk.ts b/packages/sdk/src/core/devbox-sdk.ts index 62f8374..cb8b8f2 100644 --- a/packages/sdk/src/core/devbox-sdk.ts +++ b/packages/sdk/src/core/devbox-sdk.ts @@ -54,13 +54,24 @@ export class DevboxSDK { const { waitUntilReady = true, timeout = 180000, // 3 minutes - checkInterval = 2000, // 2 seconds + checkInterval, + useExponentialBackoff = true, + initialCheckInterval = 200, // 0.2 seconds - faster initial checks + maxCheckInterval = 5000, // 5 seconds + backoffMultiplier = 1.5, } = options const instance = await this.createDevboxAsync(config) if (waitUntilReady) { - await instance.waitForReady(timeout, checkInterval) + await instance.waitForReady({ + timeout, + checkInterval, + useExponentialBackoff, + initialCheckInterval, + maxCheckInterval, + backoffMultiplier, + }) } return instance diff --git a/packages/sdk/src/core/types.ts b/packages/sdk/src/core/types.ts index b59e2c1..1f56675 100644 --- a/packages/sdk/src/core/types.ts +++ b/packages/sdk/src/core/types.ts @@ -58,10 +58,78 @@ export interface DevboxCreateOptions { timeout?: number /** * Interval between health checks (in milliseconds) - * @default 2000 (2 seconds) - * @description Only used when waitUntilReady is true + * @default 500 (0.5 seconds) - uses exponential backoff if not specified + * @description Only used when waitUntilReady is true. + * If not specified, exponential backoff will be used (starts at 500ms, max 5000ms). + * If specified, fixed interval will be used. */ checkInterval?: number + /** + * Use exponential backoff for health checks + * @default true + * @description When true, check interval starts small and increases exponentially up to maxCheckInterval. + * When false, uses fixed checkInterval. + */ + useExponentialBackoff?: boolean + /** + * Initial check interval for exponential backoff (in milliseconds) + * @default 500 (0.5 seconds) + * @description Only used when useExponentialBackoff is true + */ + initialCheckInterval?: number + /** + * Maximum check interval for exponential backoff (in milliseconds) + * @default 5000 (5 seconds) + * @description Only used when useExponentialBackoff is true + */ + maxCheckInterval?: number + /** + * Backoff multiplier for exponential backoff + * @default 1.5 + * @description Only used when useExponentialBackoff is true + */ + backoffMultiplier?: number +} + +/** + * Options for waiting for Devbox to be ready + */ +export interface WaitForReadyOptions { + /** + * Maximum time to wait for the Devbox to become ready (in milliseconds) + * @default 300000 (5 minutes) + */ + timeout?: number + /** + * Fixed interval between health checks (in milliseconds) + * @description If specified, uses fixed interval. Otherwise uses exponential backoff. + */ + checkInterval?: number + /** + * Use exponential backoff for health checks + * @default true (when checkInterval is not specified) + * @description When true, check interval starts small and increases exponentially up to maxCheckInterval. + * When false, uses fixed checkInterval. + */ + useExponentialBackoff?: boolean + /** + * Initial check interval for exponential backoff (in milliseconds) + * @default 500 (0.5 seconds) + * @description Only used when useExponentialBackoff is true + */ + initialCheckInterval?: number + /** + * Maximum check interval for exponential backoff (in milliseconds) + * @default 5000 (5 seconds) + * @description Only used when useExponentialBackoff is true + */ + maxCheckInterval?: number + /** + * Backoff multiplier for exponential backoff + * @default 1.5 + * @description Only used when useExponentialBackoff is true + */ + backoffMultiplier?: number } export interface ResourceInfo { diff --git a/packages/sdk/tests/devbox-create-performance.test.ts b/packages/sdk/tests/devbox-create-performance.test.ts new file mode 100644 index 0000000..cfdd5c3 --- /dev/null +++ b/packages/sdk/tests/devbox-create-performance.test.ts @@ -0,0 +1,350 @@ +/** + * Devbox Creation Performance Test + * + * Purpose: Diagnose performance bottlenecks when creating a new devbox via SDK + * Records timing for each step, including: + * 1. API call time (createDevbox) + * 2. Time waiting for Running status (refreshInfo calls) + * 3. Health check time (isHealthy calls) + * 4. Total time + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { DevboxSDK } from '../src/core/devbox-sdk' +import type { DevboxInstance } from '../src/core/devbox-instance' +import { TEST_CONFIG } from './setup' +import { DevboxRuntime } from '../src/api/types' + +interface PerformanceMetrics { + // API call phase + apiCallStart: number + apiCallEnd: number + apiCallDuration: number + + // Waiting phase + waitStart: number + waitEnd: number + waitDuration: number + + // refreshInfo call details + refreshInfoCalls: Array<{ + timestamp: number + duration: number + status: string + elapsed: number // Time elapsed since waitStart + }> + + // isHealthy call details + isHealthyCalls: Array<{ + timestamp: number + duration: number + healthy: boolean + elapsed: number // Time elapsed since waitStart + }> + + // Total duration + totalDuration: number + + // Status change timestamps + statusChanges: Array<{ + timestamp: number + status: string + elapsed: number + }> +} + +/** + * Helper function to format performance metrics + */ +function formatDuration(ms: number): string { + if (ms < 1000) { + return `${ms.toFixed(2)}ms` + } + return `${(ms / 1000).toFixed(2)}s` +} + +function printMetrics(metrics: PerformanceMetrics, devboxName: string) { + console.log(`\n${'='.repeat(80)}`) + console.log(`Performance Analysis Report: ${devboxName}`) + console.log('='.repeat(80)) + + console.log('\n📊 Overall Time Statistics:') + console.log(` Total Duration: ${formatDuration(metrics.totalDuration)}`) + console.log(` API Call Duration: ${formatDuration(metrics.apiCallDuration)} (${((metrics.apiCallDuration / metrics.totalDuration) * 100).toFixed(1)}%)`) + console.log(` Wait Duration: ${formatDuration(metrics.waitDuration)} (${((metrics.waitDuration / metrics.totalDuration) * 100).toFixed(1)}%)`) + + console.log('\n🔌 API Call Phase:') + console.log(` Start Time: ${new Date(metrics.apiCallStart).toISOString()}`) + console.log(` End Time: ${new Date(metrics.apiCallEnd).toISOString()}`) + console.log(` Duration: ${formatDuration(metrics.apiCallDuration)}`) + + console.log('\n⏳ Waiting Phase Details:') + console.log(` Start Time: ${new Date(metrics.waitStart).toISOString()}`) + console.log(` End Time: ${new Date(metrics.waitEnd).toISOString()}`) + console.log(` Total Duration: ${formatDuration(metrics.waitDuration)}`) + + if (metrics.statusChanges.length > 0) { + console.log('\n📈 Status Change Timeline:') + metrics.statusChanges.forEach((change, index) => { + console.log(` ${index + 1}. [${formatDuration(change.elapsed)}] Status: ${change.status}`) + }) + } + + if (metrics.refreshInfoCalls.length > 0) { + console.log('\n🔄 refreshInfo Call Statistics:') + console.log(` Total Calls: ${metrics.refreshInfoCalls.length}`) + const durations = metrics.refreshInfoCalls.map(c => c.duration) + const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length + const maxDuration = Math.max(...durations) + const minDuration = Math.min(...durations) + console.log(` Average Duration: ${formatDuration(avgDuration)}`) + console.log(` Max Duration: ${formatDuration(maxDuration)}`) + console.log(` Min Duration: ${formatDuration(minDuration)}`) + console.log(` Total Duration: ${formatDuration(durations.reduce((a, b) => a + b, 0))}`) + + console.log('\n Detailed Call Records:') + metrics.refreshInfoCalls.forEach((call, index) => { + console.log(` ${index + 1}. [${formatDuration(call.elapsed)}] Duration: ${formatDuration(call.duration)}, Status: ${call.status}`) + }) + } + + if (metrics.isHealthyCalls.length > 0) { + console.log('\n💚 isHealthy Call Statistics:') + console.log(` Total Calls: ${metrics.isHealthyCalls.length}`) + const durations = metrics.isHealthyCalls.map(c => c.duration) + const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length + const maxDuration = Math.max(...durations) + const minDuration = Math.min(...durations) + console.log(` Average Duration: ${formatDuration(avgDuration)}`) + console.log(` Max Duration: ${formatDuration(maxDuration)}`) + console.log(` Min Duration: ${formatDuration(minDuration)}`) + console.log(` Total Duration: ${formatDuration(durations.reduce((a, b) => a + b, 0))}`) + + console.log('\n Detailed Call Records:') + metrics.isHealthyCalls.forEach((call, index) => { + console.log(` ${index + 1}. [${formatDuration(call.elapsed)}] Duration: ${formatDuration(call.duration)}, Healthy: ${call.healthy}`) + }) + } + + console.log(`\n${'='.repeat(80)}`) +} + +describe('Devbox Creation Performance Test', () => { + let sdk: DevboxSDK + + beforeEach(async () => { + sdk = new DevboxSDK(TEST_CONFIG) + }, 10000) + + afterEach(async () => { + if (sdk) { + await sdk.close() + } + }, 10000) + + it('should record detailed performance metrics when creating a devbox', async () => { + const devboxName = `perf-test-${Date.now()}` + const metrics: PerformanceMetrics = { + apiCallStart: 0, + apiCallEnd: 0, + apiCallDuration: 0, + waitStart: 0, + waitEnd: 0, + waitDuration: 0, + refreshInfoCalls: [], + isHealthyCalls: [], + totalDuration: 0, + statusChanges: [], + } + + // Record API call time + metrics.apiCallStart = Date.now() + + // Create devbox instance (without waiting for ready) + const instance = await sdk.createDevboxAsync({ + name: devboxName, + runtime: DevboxRuntime.TEST_AGENT, + resource: { + cpu: 2, + memory: 4, + }, + ports: [{ number: 8080, protocol: 'HTTP' }], + }) + + metrics.apiCallEnd = Date.now() + metrics.apiCallDuration = metrics.apiCallEnd - metrics.apiCallStart + + // Record initial status + let lastStatus = instance.status + metrics.statusChanges.push({ + timestamp: metrics.apiCallEnd, + status: lastStatus, + elapsed: 0, + }) + + // Manually implement waitForReady and record detailed metrics + metrics.waitStart = Date.now() + const timeout = 300000 // 5 minutes + const checkInterval = 2000 // 2 seconds + const waitStartTime = Date.now() + + // Save original refreshInfo and isHealthy methods + const originalRefreshInfo = instance.refreshInfo.bind(instance) + const originalIsHealthy = instance.isHealthy.bind(instance) + + // Wrap refreshInfo to record performance + instance.refreshInfo = async function() { + const start = Date.now() + await originalRefreshInfo() + const duration = Date.now() - start + const elapsed = Date.now() - waitStartTime + + metrics.refreshInfoCalls.push({ + timestamp: Date.now(), + duration, + status: this.status, + elapsed, + }) + + // Record status changes + if (this.status !== lastStatus) { + metrics.statusChanges.push({ + timestamp: Date.now(), + status: this.status, + elapsed, + }) + lastStatus = this.status + } + } + + // Wrap isHealthy to record performance + instance.isHealthy = async () => { + const start = Date.now() + const result = await originalIsHealthy() + const duration = Date.now() - start + const elapsed = Date.now() - waitStartTime + + metrics.isHealthyCalls.push({ + timestamp: Date.now(), + duration, + healthy: result, + elapsed, + }) + + return result + } + + // Execute waiting logic + while (Date.now() - waitStartTime < timeout) { + try { + await instance.refreshInfo() + + if (instance.status === 'Running') { + const healthy = await instance.isHealthy() + + if (healthy) { + metrics.waitEnd = Date.now() + metrics.waitDuration = metrics.waitEnd - metrics.waitStart + metrics.totalDuration = metrics.waitEnd - metrics.apiCallStart + + // Print performance report + printMetrics(metrics, devboxName) + + // Verify devbox is ready + expect(instance.status).toBe('Running') + expect(healthy).toBe(true) + + // Cleanup: delete devbox + await instance.delete() + return + } + } + } catch (error) { + // Continue waiting but log error + console.warn(`Error during wait: ${error}`) + } + + // Wait interval + await new Promise(resolve => setTimeout(resolve, checkInterval)) + } + + metrics.waitEnd = Date.now() + metrics.waitDuration = metrics.waitEnd - metrics.waitStart + metrics.totalDuration = metrics.waitEnd - metrics.apiCallStart + + // Print performance report (even if timeout) + printMetrics(metrics, devboxName) + + // Cleanup: delete devbox + try { + await instance.delete() + } catch (error) { + console.warn(`Error cleaning up devbox: ${error}`) + } + + throw new Error(`Devbox '${devboxName}' did not become ready within ${timeout}ms`) + }, 360000) // 6 minute timeout + + it('should compare performance difference between createDevbox and createDevboxAsync + waitForReady', async () => { + const devboxName1 = `perf-compare-1-${Date.now()}` + const devboxName2 = `perf-compare-2-${Date.now()}` + + // Method 1: Use createDevbox (default wait) + const start1 = Date.now() + const instance1 = await sdk.createDevbox({ + name: devboxName1, + runtime: DevboxRuntime.TEST_AGENT, + resource: { + cpu: 2, + memory: 4, + }, + ports: [{ number: 8080, protocol: 'HTTP' }], + }) + const duration1 = Date.now() - start1 + + // Method 2: Use createDevboxAsync + manual waitForReady (with same options as Method 1) + const start2 = Date.now() + const instance2 = await sdk.createDevboxAsync({ + name: devboxName2, + runtime: DevboxRuntime.TEST_AGENT, + resource: { + cpu: 2, + memory: 4, + }, + ports: [{ number: 8080, protocol: 'HTTP' }], + }) + const apiCallDuration = Date.now() - start2 + + const waitStart = Date.now() + // Use same options as createDevbox default: exponential backoff with 500ms initial, 5000ms max + await instance2.waitForReady({ + timeout: 180000, // 3 minutes (same as createDevbox default) + useExponentialBackoff: true, + initialCheckInterval: 500, + maxCheckInterval: 5000, + backoffMultiplier: 1.5, + }) + const waitDuration = Date.now() - waitStart + const duration2 = Date.now() - start2 + + console.log(`\n${'='.repeat(80)}`) + console.log('Performance Comparison Test') + console.log('='.repeat(80)) + console.log(`Method 1 (createDevbox): ${formatDuration(duration1)}`) + console.log('Method 2 (createDevboxAsync + waitForReady):') + console.log(` API Call: ${formatDuration(apiCallDuration)}`) + console.log(` Wait for Ready: ${formatDuration(waitDuration)}`) + console.log(` Total: ${formatDuration(duration2)}`) + console.log(`Difference: ${formatDuration(Math.abs(duration1 - duration2))}`) + console.log(`${'='.repeat(80)}\n`) + + // Verify both instances are ready + expect(instance1.status).toBe('Running') + expect(instance2.status).toBe('Running') + + // Cleanup + await instance1.delete() + await instance2.delete() + }, 360000) // 6 minute timeout +}) + diff --git a/packages/server-rust/docs/openapi.yaml b/packages/server-rust/docs/openapi.yaml index 114d4ad..73b5429 100644 --- a/packages/server-rust/docs/openapi.yaml +++ b/packages/server-rust/docs/openapi.yaml @@ -1939,10 +1939,6 @@ components: example: PATH: "/usr/bin:/bin" DEBUG: "true" - shell: - type: string - description: Shell to use for execution - example: "/bin/bash" timeout: type: integer description: Timeout in seconds @@ -1995,10 +1991,6 @@ components: description: Environment variables example: PATH: "/usr/bin:/bin" - shell: - type: string - description: Shell to use for execution - example: "/bin/bash" timeout: type: integer description: Timeout in seconds