From 9427849d774f9a08e0ba02adcb16c35372dcf837 Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Fri, 26 Dec 2025 15:50:36 +0800 Subject: [PATCH 01/10] perf(sdk): optimize devbox creation wait strategy with exponential backoff - Implement exponential backoff for health checks (200ms initial, max 5000ms) - Reduce initial check interval from 500ms to 200ms for faster detection - Immediately check health status when status changes to Running - Use shorter interval (max 1s) when Running but not healthy yet - Reset check interval on status changes for faster response - Add WaitForReadyOptions interface for flexible configuration - Maintain backward compatibility with old API signature This optimization significantly reduces the time to detect when a devbox becomes ready, improving overall creation performance. --- packages/sdk/src/core/devbox-instance.ts | 67 +++- packages/sdk/src/core/devbox-sdk.ts | 15 +- packages/sdk/src/core/types.ts | 72 +++- .../tests/devbox-create-performance.test.ts | 350 ++++++++++++++++++ 4 files changed, 494 insertions(+), 10 deletions(-) create mode 100644 packages/sdk/tests/devbox-create-performance.test.ts diff --git a/packages/sdk/src/core/devbox-instance.ts b/packages/sdk/src/core/devbox-instance.ts index 2ba0709..507eda1 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' @@ -866,24 +867,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 +923,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 +}) + From 627f20f78889877fec2e4335256ede1c25c32f4f Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Fri, 26 Dec 2025 16:02:26 +0800 Subject: [PATCH 02/10] fix(ci): rebuild preview package on PR commits --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 }} From f6492630874bfe86ee472f3f94eb9e8ce2c1ba3d Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Fri, 26 Dec 2025 18:32:12 +0800 Subject: [PATCH 03/10] update examples --- packages/sdk/examples/full-lifecycle.ts | 366 ++++++++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 packages/sdk/examples/full-lifecycle.ts diff --git a/packages/sdk/examples/full-lifecycle.ts b/packages/sdk/examples/full-lifecycle.ts new file mode 100644 index 0000000..da0815c --- /dev/null +++ b/packages/sdk/examples/full-lifecycle.ts @@ -0,0 +1,366 @@ +/** + * Devbox Full Lifecycle Example + * + * This example demonstrates a complete devbox lifecycle workflow: + * 1. Create and start a devbox + * 2. Fetch devbox information + * 3. Git clone a repository + * 4. Call analyze API to get entrypoint + * 5. Write entrypoint.sh file + * + * Usage: + * # From project root: + * bun packages/sdk/examples/full-lifecycle.ts + * + * # From packages/sdk directory: + * bun examples/full-lifecycle.ts + * + * # From examples directory: + * bun full-lifecycle.ts + * + * # With environment variable: + * export KUBECONFIG=/path/to/kubeconfig + * bun full-lifecycle.ts + * + * # Or inline: + * KUBECONFIG=/path/to/kubeconfig bun full-lifecycle.ts + * + * Requirements: + * - KUBECONFIG environment variable must be set (can be in .env file) + */ + +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 +// 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 KUBECONFIG=...)') +} + +// Check for KUBECONFIG - support both .env file and export command +if (!process.env.KUBECONFIG) { + console.error('') + console.error('โŒ Missing required environment variable: KUBECONFIG') + console.error('') + console.error('Please set it using one of these methods:') + console.error('') + console.error('1. Export file path in shell:') + console.error(' export KUBECONFIG=/path/to/kubeconfig') + console.error(' bun full-lifecycle.ts') + console.error('') + console.error('2. Export kubeconfig content (use $\' for multiline):') + console.error(' export KUBECONFIG=$\'apiVersion: v1\\n...') + console.error('') + console.error('3. Create .env file in project root:') + console.error(' echo "KUBECONFIG=/path/to/kubeconfig" > .env') + console.error('') + console.error('4. Pass inline:') + console.error(' KUBECONFIG=/path/to/kubeconfig bun full-lifecycle.ts') + console.error('') + process.exit(1) +} + +// Handle KUBECONFIG - could be a file path or content +let kubeconfigContent = process.env.KUBECONFIG + +// If it looks like a file path (doesn't contain 'apiVersion' and exists as file), read it +if (!kubeconfigContent.includes('apiVersion') && existsSync(kubeconfigContent)) { + console.log(`๐Ÿ“„ Reading kubeconfig from file: ${kubeconfigContent}`) + kubeconfigContent = readFileSync(kubeconfigContent, 'utf-8') +} else if (kubeconfigContent.includes('\\n')) { + // If it contains escaped newlines, convert them to actual newlines + console.log('๐Ÿ”„ Converting escaped newlines in kubeconfig...') + kubeconfigContent = kubeconfigContent.replace(/\\n/g, '\n') +} + +// Parse API URL from kubeconfig +const kubeconfigUrl = parseKubeconfigServerUrl(kubeconfigContent) +if (!kubeconfigUrl) { + console.error('') + console.error('โŒ Failed to parse API server URL from kubeconfig') + console.error('') + console.error('Please ensure:') + console.error(' 1. KUBECONFIG is a valid kubeconfig file path or YAML content') + console.error(' 2. The kubeconfig contains a valid server URL') + console.error(' 3. If using multiline content, use $\' syntax in shell') + console.error('') + process.exit(1) +} + +const SDK_CONFIG = { + kubeconfig: kubeconfigContent, + http: { + timeout: 300000, + retries: 3, + rejectUnauthorized: false, + }, +} + +// Helper function: generate unique name +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('full-lifecycle') + const REPO_URL = 'https://github.com/pdsuwwz/nextjs-nextra-starter' + const REPO_DIR = '/home/devbox/project/reddit-ai-assistant-extension' + const ANALYZE_API_URL = 'https://pgitgrfugqfk.usw.sealos.io/analyze' + + try { + console.log('๐Ÿš€ Starting full lifecycle example...') + console.log(`๐Ÿ“ฆ Creating devbox: ${name}`) + + // 1. Create Devbox + const devbox = await sdk.createDevbox({ + name, + runtime: DevboxRuntime.TEST_AGENT, + resource: { cpu: 1, memory: 2 }, + }) + + console.log(`โœ… Devbox created: ${devbox.name}`) + + // 2. Start Devbox and wait for Running status + 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}`) + + // 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) + + // Configure npm to use Taobao mirror + console.log('') + console.log('๐Ÿ”ง Configuring npm to use Taobao mirror...') + await currentDevbox.execSync({ + command: 'npm', + args: ['config', 'set', 'registry', 'https://registry.npmmirror.com'], + }) + console.log('โœ… npm registry configured') + + // Verify npm registry configuration + const npmRegistryCheck = await currentDevbox.execSync({ + command: 'npm', + args: ['config', 'get', 'registry'], + }) + console.log(`๐Ÿ“ฆ Current npm registry: ${npmRegistryCheck.stdout.trim()}`) + + // 6. Call analyze API using fetch + console.log('๐Ÿ” Calling analyze API...') + const analyzeResponse = await fetch(ANALYZE_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + repo_url: REPO_URL, + }), + }) + + if (!analyzeResponse.ok) { + throw new Error(`Analyze API failed: ${analyzeResponse.statusText}`) + } + + const analyzeData = await analyzeResponse.json() + console.log('โœ… Analyze API response received') + console.log(`๐Ÿ“ Entrypoint length: ${analyzeData.entrypoint?.length || 0} characters`) + + // Check Node.js and npm versions before writing entrypoint + console.log('') + console.log('๐Ÿ” Checking Node.js and npm versions...') + const nodeVersionResult = await currentDevbox.execSync({ + command: 'node', + args: ['-v'], + }) + console.log(`๐Ÿ“ฆ Node.js version: ${nodeVersionResult.stdout.trim()}`) + + const npmVersionResult = await currentDevbox.execSync({ + command: 'npm', + args: ['-v'], + }) + console.log(`๐Ÿ“ฆ npm version: ${npmVersionResult.stdout.trim()}`) + + // 7. Write entrypoint.sh file + const entrypointPath = `${REPO_DIR}/entrypoint.sh` + console.log('') + console.log(`๐Ÿ’พ Writing entrypoint.sh to ${entrypointPath}...`) + + // Replace pnpm with npm in entrypoint script + const entrypointScript = analyzeData.entrypoint.replace(/pnpm/g, 'npm') + + await currentDevbox.writeFile(entrypointPath, entrypointScript, { + mode: 0o755, + }) + console.log('โœ… entrypoint.sh written successfully (converted pnpm to npm)') + + // 8. Verify entrypoint.sh was created with correct content + const entrypointContent = await currentDevbox.readFile(entrypointPath) + if (entrypointContent.toString() === entrypointScript) { + console.log('โœ… entrypoint.sh content verified') + } else { + console.warn('โš ๏ธ entrypoint.sh content mismatch') + } + + // Verify file permissions (if supported) + const entrypointInfo = await currentDevbox.listFiles(REPO_DIR) + const entrypointFile = entrypointInfo.files.find(f => f.name === 'entrypoint.sh') + if (entrypointFile) { + console.log('โœ… entrypoint.sh found in directory listing') + } + + // Cat the entrypoint.sh file to view its content + console.log('') + console.log('๐Ÿ“„ Viewing entrypoint.sh file content:') + const catResult = await currentDevbox.execSync({ + command: 'cat', + args: [entrypointPath], + }) + console.log(catResult.stdout) + console.log('') + + // Add execute permission to entrypoint.sh + console.log('') + console.log('๐Ÿ”ง Adding execute permission to entrypoint.sh...') + await currentDevbox.execSync({ + command: 'chmod', + args: ['+x', entrypointPath], + }) + console.log('โœ… Execute permission added') + + // List directory contents again after adding execute permission + console.log('') + console.log('๐Ÿ“‹ Final directory contents after adding execute permission:') + const finalLsResult = await currentDevbox.execSync({ + command: 'ls', + args: ['-la', entrypointPath], + }) + console.log(finalLsResult.stdout) + + // Execute the entrypoint.sh script + console.log('') + console.log('๐Ÿš€ Executing entrypoint.sh script...') + console.log(`๐Ÿ“‚ Changing directory to ${REPO_DIR}`) + const execResult = await currentDevbox.execSync({ + command: 'bash', + args: [entrypointPath, 'development'], + cwd: REPO_DIR, + }) + console.log('๐Ÿ“ค Script output:') + console.log(execResult.stdout) + if (execResult.stderr) { + console.log('โš ๏ธ Script errors:') + console.log(execResult.stderr) + } + console.log(`โœ… Script executed with exit code: ${execResult.exitCode}`) + + // Get preview URL for port 3000 + console.log('') + console.log('๐Ÿ”— Getting preview URL for port 3000...') + try { + const previewLink = await currentDevbox.getPreviewLink(3000) + console.log(`โœ… Preview URL: ${previewLink.url}`) + console.log(` Protocol: ${previewLink.protocol}`) + console.log(` Port: ${previewLink.port}`) + } catch (error) { + console.warn('โš ๏ธ Failed to get preview URL:', error instanceof Error ? error.message : String(error)) + console.warn(' This might be because the devbox does not have port 3000 configured or agentServer is not available') + } + + console.log('') + console.log('๐ŸŽ‰ Full lifecycle example completed successfully!') + console.log(`๐Ÿ“ฆ Devbox name: ${name}`) + console.log(`๐Ÿ“ Repository: ${REPO_DIR}`) + console.log(`๐Ÿ“„ Entrypoint: ${entrypointPath}`) + + // Cleanup option + console.log('') + console.log('๐Ÿ’ก Note: Devbox will be cleaned up automatically or you can delete it manually:') + console.log(` await sdk.getDevbox('${name}').delete()`) + + } catch (error) { + console.error('โŒ Error occurred:', error) + throw error + } finally { + await sdk.close() + } +} + +// Run the example +main().catch((error) => { + console.error('Failed to run example:', error) + process.exit(1) +}) + From dd23d29618f42a2d4fa6ab62751ce00432c3f56a Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Fri, 26 Dec 2025 22:32:13 +0800 Subject: [PATCH 04/10] update --- packages/sdk/examples/full-lifecycle.ts | 225 ++++++++++++++---------- packages/sdk/src/api/client.ts | 41 ++++- 2 files changed, 171 insertions(+), 95 deletions(-) diff --git a/packages/sdk/examples/full-lifecycle.ts b/packages/sdk/examples/full-lifecycle.ts index da0815c..c09a39e 100644 --- a/packages/sdk/examples/full-lifecycle.ts +++ b/packages/sdk/examples/full-lifecycle.ts @@ -137,7 +137,7 @@ const generateDevboxName = (prefix: string) => { async function main() { const sdk = new DevboxSDK(SDK_CONFIG) const name = generateDevboxName('full-lifecycle') - const REPO_URL = 'https://github.com/pdsuwwz/nextjs-nextra-starter' + const REPO_URL = 'https://github.com/steven-tey/precedent.git' const REPO_DIR = '/home/devbox/project/reddit-ai-assistant-extension' const ANALYZE_API_URL = 'https://pgitgrfugqfk.usw.sealos.io/analyze' @@ -202,43 +202,59 @@ async function main() { }) console.log(lsResult.stdout) - // Configure npm to use Taobao mirror - console.log('') - console.log('๐Ÿ”ง Configuring npm to use Taobao mirror...') - await currentDevbox.execSync({ - command: 'npm', - args: ['config', 'set', 'registry', 'https://registry.npmmirror.com'], - }) - console.log('โœ… npm registry configured') - - // Verify npm registry configuration - const npmRegistryCheck = await currentDevbox.execSync({ - command: 'npm', - args: ['config', 'get', 'registry'], - }) - console.log(`๐Ÿ“ฆ Current npm registry: ${npmRegistryCheck.stdout.trim()}`) - - // 6. Call analyze API using fetch + // 6. Call analyze API using fetch with retry logic console.log('๐Ÿ” Calling analyze API...') - const analyzeResponse = await fetch(ANALYZE_API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - repo_url: REPO_URL, - }), - }) - if (!analyzeResponse.ok) { - throw new Error(`Analyze API failed: ${analyzeResponse.statusText}`) + // 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, + // Add keep-alive for better connection handling + 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 analyzeResponse.json() + const analyzeData = await callAnalyzeAPI() console.log('โœ… Analyze API response received') console.log(`๐Ÿ“ Entrypoint length: ${analyzeData.entrypoint?.length || 0} characters`) - // Check Node.js and npm versions before writing entrypoint + // 7. Check Node.js and npm versions console.log('') console.log('๐Ÿ” Checking Node.js and npm versions...') const nodeVersionResult = await currentDevbox.execSync({ @@ -246,85 +262,107 @@ async function main() { args: ['-v'], }) console.log(`๐Ÿ“ฆ Node.js version: ${nodeVersionResult.stdout.trim()}`) - + const npmVersionResult = await currentDevbox.execSync({ command: 'npm', args: ['-v'], }) console.log(`๐Ÿ“ฆ npm version: ${npmVersionResult.stdout.trim()}`) - // 7. Write entrypoint.sh file + // 8. Enable pnpm via corepack (if needed) + console.log('') + console.log('๐Ÿ”ง Checking package manager requirements...') + + const usesPnpm = analyzeData.entrypoint.includes('pnpm') + + if (usesPnpm) { + console.log('๐Ÿ“ฆ Detected pnpm usage, enabling via corepack...') + try { + await currentDevbox.execSync({ + command: 'corepack', + args: ['enable'], + }) + console.log('โœ… pnpm enabled via corepack') + + const pnpmVersionResult = await currentDevbox.execSync({ + command: 'pnpm', + args: ['-v'], + }) + console.log(`๐Ÿ“ฆ pnpm version: ${pnpmVersionResult.stdout.trim()}`) + } catch (error) { + console.warn('โš ๏ธ Failed to enable pnpm via corepack:', error instanceof Error ? error.message : String(error)) + } + } + + // 9. Prepare entrypoint.sh with command fixes const entrypointPath = `${REPO_DIR}/entrypoint.sh` console.log('') - console.log(`๐Ÿ’พ Writing entrypoint.sh to ${entrypointPath}...`) - - // Replace pnpm with npm in entrypoint script - const entrypointScript = analyzeData.entrypoint.replace(/pnpm/g, 'npm') - + console.log(`๐Ÿ’พ Preparing entrypoint.sh...`) + + let 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 -') + await currentDevbox.writeFile(entrypointPath, entrypointScript, { mode: 0o755, }) - console.log('โœ… entrypoint.sh written successfully (converted pnpm to npm)') - - // 8. Verify entrypoint.sh was created with correct content - const entrypointContent = await currentDevbox.readFile(entrypointPath) - if (entrypointContent.toString() === entrypointScript) { - console.log('โœ… entrypoint.sh content verified') - } else { - console.warn('โš ๏ธ entrypoint.sh content mismatch') - } - - // Verify file permissions (if supported) - const entrypointInfo = await currentDevbox.listFiles(REPO_DIR) - const entrypointFile = entrypointInfo.files.find(f => f.name === 'entrypoint.sh') - if (entrypointFile) { - console.log('โœ… entrypoint.sh found in directory listing') - } + console.log('โœ… entrypoint.sh written successfully') - // Cat the entrypoint.sh file to view its content - console.log('') - console.log('๐Ÿ“„ Viewing entrypoint.sh file content:') - const catResult = await currentDevbox.execSync({ - command: 'cat', - args: [entrypointPath], - }) - console.log(catResult.stdout) + // 10. Configure npm registry console.log('') + console.log('๐Ÿ”ง Configuring npm registry...') + + const homeDir = '/home/devbox' + const expectedRegistry = 'https://registry.npmmirror.com' - // Add execute permission to entrypoint.sh - console.log('') - console.log('๐Ÿ”ง Adding execute permission to entrypoint.sh...') await currentDevbox.execSync({ - command: 'chmod', - args: ['+x', entrypointPath], + command: 'npm', + args: ['config', 'set', 'registry', expectedRegistry], + cwd: REPO_DIR, + env: { HOME: homeDir }, }) - console.log('โœ… Execute permission added') + console.log(`โœ… npm registry set to: ${expectedRegistry}`) - // List directory contents again after adding execute permission + // 11. ๅฏๅŠจ entrypoint.sh๏ผˆๅŽๅฐๅผ‚ๆญฅ่ฟ่กŒ๏ผŒ้ฟๅ…่ถ…ๆ—ถ๏ผ‰ console.log('') - console.log('๐Ÿ“‹ Final directory contents after adding execute permission:') - const finalLsResult = await currentDevbox.execSync({ - command: 'ls', - args: ['-la', entrypointPath], - }) - console.log(finalLsResult.stdout) + console.log('๐Ÿš€ Starting application via entrypoint.sh...') - // Execute the entrypoint.sh script - console.log('') - console.log('๐Ÿš€ Executing entrypoint.sh script...') - console.log(`๐Ÿ“‚ Changing directory to ${REPO_DIR}`) - const execResult = await currentDevbox.execSync({ + const serverProcess = await currentDevbox.executeCommand({ command: 'bash', args: [entrypointPath, 'development'], cwd: REPO_DIR, + env: { + HOME: homeDir, + NPM_CONFIG_USERCONFIG: `${homeDir}/.npmrc`, + NPM_CONFIG_CACHE: `${homeDir}/.npm`, + }, + timeout: 600, }) - console.log('๐Ÿ“ค Script output:') - console.log(execResult.stdout) - if (execResult.stderr) { - console.log('โš ๏ธ Script errors:') - console.log(execResult.stderr) + + console.log(`โœ… Application started!`) + console.log(` Process ID: ${serverProcess.processId}`) + console.log(` PID: ${serverProcess.pid}`) + + // ็ญ‰ๅพ…ๆœๅŠกๅ™จๅฏๅŠจ๏ผˆpnpm install + pnpm dev ้œ€่ฆๆ—ถ้—ด๏ผ‰ + console.log('') + console.log('โณ Waiting for dependencies installation and server startup...') + console.log(' This may take 1-2 minutes...') + await new Promise(resolve => setTimeout(resolve, 60000)) // ็ญ‰ๅพ… 60 ็ง’ + + // ๆฃ€ๆŸฅ่ฟ›็จ‹็Šถๆ€ + try { + const status = await currentDevbox.getProcessStatus(serverProcess.processId) + console.log(`๐Ÿ“Š Server status: ${status.processStatus}`) + + if (status.processStatus !== 'running') { + console.warn('โš ๏ธ Process may have stopped, checking logs...') + const logs = await currentDevbox.getProcessLogs(serverProcess.processId) + console.log('๐Ÿ“‹ Process logs:') + console.log(logs.logs.join('\n')) + } + } catch (error) { + console.warn('โš ๏ธ Could not check process status') } - console.log(`โœ… Script executed with exit code: ${execResult.exitCode}`) // Get preview URL for port 3000 console.log('') @@ -341,14 +379,15 @@ async function main() { console.log('') console.log('๐ŸŽ‰ Full lifecycle example completed successfully!') - console.log(`๐Ÿ“ฆ Devbox name: ${name}`) - console.log(`๐Ÿ“ Repository: ${REPO_DIR}`) - console.log(`๐Ÿ“„ Entrypoint: ${entrypointPath}`) - - // Cleanup option console.log('') - console.log('๐Ÿ’ก Note: Devbox will be cleaned up automatically or you can delete it manually:') - console.log(` await sdk.getDevbox('${name}').delete()`) + console.log('๐Ÿ“‹ Summary:') + console.log(` Devbox: ${name}`) + console.log(` Repository: ${REPO_URL}`) + console.log(` Project Dir: ${REPO_DIR}`) + console.log(` Server: npm run dev`) + console.log('') + console.log('๐Ÿ’ก Cleanup: Delete devbox when done') + console.log(` sdk.getDevbox('${name}').then(d => d.delete())`) } catch (error) { console.error('โŒ Error occurred:', error) diff --git a/packages/sdk/src/api/client.ts b/packages/sdk/src/api/client.ts index 5d3b0bd..22dbae2 100644 --- a/packages/sdk/src/api/client.ts +++ b/packages/sdk/src/api/client.ts @@ -290,9 +290,46 @@ 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)}` + ) + } + + // ๅค„็†ไธค็งๅฏ่ƒฝ็š„ๅ“ๅบ”ๆ ผๅผ๏ผš + // 1. { data: DevboxCreateResponse } - ๆ ‡ๅ‡†ๆ ผๅผ + // 2. DevboxCreateResponse - ็›ดๆŽฅ่ฟ”ๅ›žๆ•ฐๆฎ + let createResponse: DevboxCreateResponse + + if (typeof response.data === 'object' && 'data' in response.data) { + // ๆ ผๅผ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 { + // ๆ ผๅผ2: ็›ดๆŽฅๆ˜ฏ 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 ) From 5f1d2fd2638ffb5b175a37f2845ae01d4bed9bdc Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Fri, 26 Dec 2025 22:35:16 +0800 Subject: [PATCH 05/10] update repo --- packages/sdk/examples/full-lifecycle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/examples/full-lifecycle.ts b/packages/sdk/examples/full-lifecycle.ts index c09a39e..af51e18 100644 --- a/packages/sdk/examples/full-lifecycle.ts +++ b/packages/sdk/examples/full-lifecycle.ts @@ -137,7 +137,7 @@ const generateDevboxName = (prefix: string) => { async function main() { const sdk = new DevboxSDK(SDK_CONFIG) const name = generateDevboxName('full-lifecycle') - const REPO_URL = 'https://github.com/steven-tey/precedent.git' + 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' From 172384b36aeda09ef11e6a01428504cd3181e472 Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Fri, 26 Dec 2025 22:55:11 +0800 Subject: [PATCH 06/10] add waitForServerStartup --- packages/sdk/examples/full-lifecycle.ts | 128 ++++++++++++++++++++---- 1 file changed, 110 insertions(+), 18 deletions(-) diff --git a/packages/sdk/examples/full-lifecycle.ts b/packages/sdk/examples/full-lifecycle.ts index af51e18..920367f 100644 --- a/packages/sdk/examples/full-lifecycle.ts +++ b/packages/sdk/examples/full-lifecycle.ts @@ -134,6 +134,112 @@ const generateDevboxName = (prefix: string) => { return `example-${sanitizedPrefix}-${timestamp}-${random}` } +// Helper function: wait for server startup with smart detection +async function waitForServerStartup( + devbox: any, + processId: string, + port = 3000, + maxWaitTime = 180000 +): Promise { + const startTime = Date.now() + const checkInterval = 3000 // ๆฏ 3 ็ง’ๆฃ€ๆŸฅไธ€ๆฌก + + // ๆœๅŠกๅ™จๅฏๅŠจๆˆๅŠŸ็š„ๆ—ฅๅฟ—ๅ…ณ้”ฎๅญ— + const successPatterns = [ + /Ready in/i, // Next.js: "โœ“ Ready in 2.4s" + /Local:.*http/i, // Next.js: "- Local: http://localhost:3000" + /started server on/i, // "started server on ..." + /Listening on/i, // "Listening on http://..." + ] + + console.log('') + console.log('โณ Waiting for server to start...') + console.log(` Checking logs, port ${port}, and process status...`) + console.log('') + + // ๅ…ˆ็ญ‰ๅพ… 10 ็ง’่ฎฉ pnpm install ๅผ€ๅง‹่ฟ่กŒ + await new Promise(resolve => setTimeout(resolve, 10000)) + + let lastLogLength = 0 + + while (Date.now() - startTime < maxWaitTime) { + try { + // 1. ๆฃ€ๆŸฅ่ฟ›็จ‹็Šถๆ€ + const status = await devbox.getProcessStatus(processId) + + if (status.processStatus === 'failed' || status.processStatus === 'completed') { + console.log('') + console.error(`โŒ Process stopped with status: ${status.processStatus}`) + const logs = await devbox.getProcessLogs(processId) + console.log('๐Ÿ“‹ Full logs:') + console.log(logs.logs.join('\n')) + return false + } + + // 2. ๆฃ€ๆŸฅๆ—ฅๅฟ— + const logsResponse = await devbox.getProcessLogs(processId) + const allLogs = logsResponse.logs.join('\n') + + // ๆ˜พ็คบๆ–ฐ็š„ๆ—ฅๅฟ—่พ“ๅ‡บ + if (allLogs.length > lastLogLength) { + const newLogs = allLogs.substring(lastLogLength) + const newLines = newLogs.split('\n').filter(line => line.trim()) + if (newLines.length > 0) { + console.log('๐Ÿ“‹ New logs:') + newLines.forEach(line => console.log(` ${line}`)) + } + lastLogLength = allLogs.length + } + + // ๆฃ€ๆŸฅๆ˜ฏๅฆๆœ‰ๅฏๅŠจๆˆๅŠŸ็š„ๆ ‡ๅฟ— + const isReady = successPatterns.some(pattern => pattern.test(allLogs)) + + if (isReady) { + console.log('') + console.log('โœ… Found server ready signal in logs!') + + // 3. ้ชŒ่ฏ็ซฏๅฃๆ˜ฏๅฆๅผ€ๆ”พ + try { + const portsResponse = await devbox.getPorts() + if (portsResponse.ports.includes(port)) { + console.log(`โœ… Port ${port} is open`) + console.log('') + return true + } else { + console.log(`โณ Port ${port} not yet open, waiting...`) + } + } catch (error) { + console.log('โณ Port check failed, retrying...') + } + } + + // ๆ˜พ็คบ่ฟ›ๅบฆ + const elapsed = Math.floor((Date.now() - startTime) / 1000) + process.stdout.write(`\r Status: ${status.processStatus} | 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`) + + // ่ถ…ๆ—ถๅŽๆ˜พ็คบๆœ€ๅŽ็š„ๆ—ฅๅฟ— + try { + const logs = await devbox.getProcessLogs(processId) + console.log('๐Ÿ“‹ Latest logs:') + console.log(logs.logs.join('\n')) + } catch (error) { + // ignore + } + + return false +} + async function main() { const sdk = new DevboxSDK(SDK_CONFIG) const name = generateDevboxName('full-lifecycle') @@ -343,25 +449,11 @@ async function main() { console.log(` Process ID: ${serverProcess.processId}`) console.log(` PID: ${serverProcess.pid}`) - // ็ญ‰ๅพ…ๆœๅŠกๅ™จๅฏๅŠจ๏ผˆpnpm install + pnpm dev ้œ€่ฆๆ—ถ้—ด๏ผ‰ - console.log('') - console.log('โณ Waiting for dependencies installation and server startup...') - console.log(' This may take 1-2 minutes...') - await new Promise(resolve => setTimeout(resolve, 60000)) // ็ญ‰ๅพ… 60 ็ง’ - - // ๆฃ€ๆŸฅ่ฟ›็จ‹็Šถๆ€ - try { - const status = await currentDevbox.getProcessStatus(serverProcess.processId) - console.log(`๐Ÿ“Š Server status: ${status.processStatus}`) + // ๆ™บ่ƒฝ็ญ‰ๅพ…ๆœๅŠกๅ™จๅฏๅŠจ๏ผˆๆฃ€ๆŸฅๆ—ฅๅฟ— + ็ซฏๅฃ + ่ฟ›็จ‹็Šถๆ€๏ผ‰ + const isReady = await waitForServerStartup(currentDevbox, serverProcess.processId, 3000, 180000) - if (status.processStatus !== 'running') { - console.warn('โš ๏ธ Process may have stopped, checking logs...') - const logs = await currentDevbox.getProcessLogs(serverProcess.processId) - console.log('๐Ÿ“‹ Process logs:') - console.log(logs.logs.join('\n')) - } - } catch (error) { - console.warn('โš ๏ธ Could not check process status') + if (!isReady) { + throw new Error('Server failed to start within timeout') } // Get preview URL for port 3000 From 29f1704ab8547abaa28384366058c4aefbd8a805 Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Mon, 29 Dec 2025 16:26:36 +0800 Subject: [PATCH 07/10] update --- biome.json | 3 ++- packages/sdk/examples/full-lifecycle.ts | 36 ++++++++++++------------- packages/sdk/src/api/client.ts | 22 ++++++--------- 3 files changed, 28 insertions(+), 33 deletions(-) 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/full-lifecycle.ts b/packages/sdk/examples/full-lifecycle.ts index 920367f..2edcec9 100644 --- a/packages/sdk/examples/full-lifecycle.ts +++ b/packages/sdk/examples/full-lifecycle.ts @@ -142,9 +142,9 @@ async function waitForServerStartup( maxWaitTime = 180000 ): Promise { const startTime = Date.now() - const checkInterval = 3000 // ๆฏ 3 ็ง’ๆฃ€ๆŸฅไธ€ๆฌก + const checkInterval = 3000 // Check every 3 seconds - // ๆœๅŠกๅ™จๅฏๅŠจๆˆๅŠŸ็š„ๆ—ฅๅฟ—ๅ…ณ้”ฎๅญ— + // Server startup success log keywords const successPatterns = [ /Ready in/i, // Next.js: "โœ“ Ready in 2.4s" /Local:.*http/i, // Next.js: "- Local: http://localhost:3000" @@ -157,14 +157,14 @@ async function waitForServerStartup( console.log(` Checking logs, port ${port}, and process status...`) console.log('') - // ๅ…ˆ็ญ‰ๅพ… 10 ็ง’่ฎฉ pnpm install ๅผ€ๅง‹่ฟ่กŒ + // Wait 10 seconds first to let pnpm install start running await new Promise(resolve => setTimeout(resolve, 10000)) let lastLogLength = 0 while (Date.now() - startTime < maxWaitTime) { try { - // 1. ๆฃ€ๆŸฅ่ฟ›็จ‹็Šถๆ€ + // 1. Check process status const status = await devbox.getProcessStatus(processId) if (status.processStatus === 'failed' || status.processStatus === 'completed') { @@ -176,44 +176,44 @@ async function waitForServerStartup( return false } - // 2. ๆฃ€ๆŸฅๆ—ฅๅฟ— + // 2. Check logs const logsResponse = await devbox.getProcessLogs(processId) const allLogs = logsResponse.logs.join('\n') - // ๆ˜พ็คบๆ–ฐ็š„ๆ—ฅๅฟ—่พ“ๅ‡บ + // Display new log output if (allLogs.length > lastLogLength) { const newLogs = allLogs.substring(lastLogLength) - const newLines = newLogs.split('\n').filter(line => line.trim()) + const newLines = newLogs.split('\n').filter((line: string) => line.trim()) if (newLines.length > 0) { console.log('๐Ÿ“‹ New logs:') - newLines.forEach(line => console.log(` ${line}`)) + for (const line of newLines) { + console.log(` ${line}`) + } } lastLogLength = allLogs.length } - // ๆฃ€ๆŸฅๆ˜ฏๅฆๆœ‰ๅฏๅŠจๆˆๅŠŸ็š„ๆ ‡ๅฟ— + // Check for startup success indicators const isReady = successPatterns.some(pattern => pattern.test(allLogs)) if (isReady) { console.log('') console.log('โœ… Found server ready signal in logs!') - // 3. ้ชŒ่ฏ็ซฏๅฃๆ˜ฏๅฆๅผ€ๆ”พ + // 3. Verify if port is open try { const portsResponse = await devbox.getPorts() if (portsResponse.ports.includes(port)) { console.log(`โœ… Port ${port} is open`) - console.log('') return true - } else { - console.log(`โณ Port ${port} not yet open, waiting...`) } } catch (error) { console.log('โณ Port check failed, retrying...') } + console.log(`โณ Port ${port} not yet open, waiting...`) } - // ๆ˜พ็คบ่ฟ›ๅบฆ + // Display progress const elapsed = Math.floor((Date.now() - startTime) / 1000) process.stdout.write(`\r Status: ${status.processStatus} | Elapsed: ${elapsed}s`) @@ -228,7 +228,7 @@ async function waitForServerStartup( console.log('') console.warn(`โš ๏ธ Server did not start within ${maxWaitTime / 1000}s`) - // ่ถ…ๆ—ถๅŽๆ˜พ็คบๆœ€ๅŽ็š„ๆ—ฅๅฟ— + // Display latest logs after timeout try { const logs = await devbox.getProcessLogs(processId) console.log('๐Ÿ“‹ Latest logs:') @@ -405,7 +405,7 @@ async function main() { console.log('') console.log(`๐Ÿ’พ Preparing entrypoint.sh...`) - let entrypointScript = analyzeData.entrypoint + 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 -') @@ -429,7 +429,7 @@ async function main() { }) console.log(`โœ… npm registry set to: ${expectedRegistry}`) - // 11. ๅฏๅŠจ entrypoint.sh๏ผˆๅŽๅฐๅผ‚ๆญฅ่ฟ่กŒ๏ผŒ้ฟๅ…่ถ…ๆ—ถ๏ผ‰ + // 11. Start entrypoint.sh (run asynchronously in background to avoid timeout) console.log('') console.log('๐Ÿš€ Starting application via entrypoint.sh...') @@ -449,7 +449,7 @@ async function main() { console.log(` Process ID: ${serverProcess.processId}`) console.log(` PID: ${serverProcess.pid}`) - // ๆ™บ่ƒฝ็ญ‰ๅพ…ๆœๅŠกๅ™จๅฏๅŠจ๏ผˆๆฃ€ๆŸฅๆ—ฅๅฟ— + ็ซฏๅฃ + ่ฟ›็จ‹็Šถๆ€๏ผ‰ + // Intelligently wait for server startup (check logs + port + process status) const isReady = await waitForServerStartup(currentDevbox, serverProcess.processId, 3000, 180000) if (!isReady) { diff --git a/packages/sdk/src/api/client.ts b/packages/sdk/src/api/client.ts index 22dbae2..28f2863 100644 --- a/packages/sdk/src/api/client.ts +++ b/packages/sdk/src/api/client.ts @@ -291,40 +291,34 @@ export class DevboxAPI { data: request, }) - // ๆฃ€ๆŸฅๅ“ๅบ”ๆ•ฐๆฎๆ˜ฏๅฆๅญ˜ๅœจ if (!response || !response.data) { throw new Error( - `Invalid API response: response or response.data is undefined. ` + - `Response: ${JSON.stringify(response)}` + `Invalid API response: response or response.data is undefined. Response: ${JSON.stringify(response)}` ) } - // ๅค„็†ไธค็งๅฏ่ƒฝ็š„ๅ“ๅบ”ๆ ผๅผ๏ผš - // 1. { data: DevboxCreateResponse } - ๆ ‡ๅ‡†ๆ ผๅผ - // 2. DevboxCreateResponse - ็›ดๆŽฅ่ฟ”ๅ›žๆ•ฐๆฎ + // 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) { - // ๆ ผๅผ1: { data: DevboxCreateResponse } + // 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)}` + `Invalid API response structure: expected { data: DevboxCreateResponse }, but data field is undefined. Full response: ${JSON.stringify(response.data)}` ) } createResponse = responseData.data } else { - // ๆ ผๅผ2: ็›ดๆŽฅๆ˜ฏ DevboxCreateResponse + // 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)}` + `Invalid DevboxCreateResponse: missing 'name' field. Response data: ${JSON.stringify(createResponse)}` ) } From 13d7f74d3f73093c628b7c1456668b1585a89445 Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Wed, 31 Dec 2025 17:28:19 +0800 Subject: [PATCH 08/10] test --- .../sdk/examples/daytona-full-lifecycle.ts | 513 ++++++++++++++++++ packages/sdk/examples/full-lifecycle.ts | 457 +++------------- packages/sdk/src/core/devbox-instance.ts | 71 ++- 3 files changed, 652 insertions(+), 389 deletions(-) create mode 100644 packages/sdk/examples/daytona-full-lifecycle.ts 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 index 2edcec9..0556e06 100644 --- a/packages/sdk/examples/full-lifecycle.ts +++ b/packages/sdk/examples/full-lifecycle.ts @@ -1,32 +1,6 @@ /** - * Devbox Full Lifecycle Example - * - * This example demonstrates a complete devbox lifecycle workflow: - * 1. Create and start a devbox - * 2. Fetch devbox information - * 3. Git clone a repository - * 4. Call analyze API to get entrypoint - * 5. Write entrypoint.sh file - * - * Usage: - * # From project root: - * bun packages/sdk/examples/full-lifecycle.ts - * - * # From packages/sdk directory: - * bun examples/full-lifecycle.ts - * - * # From examples directory: - * bun full-lifecycle.ts - * - * # With environment variable: - * export KUBECONFIG=/path/to/kubeconfig - * bun full-lifecycle.ts - * - * # Or inline: - * KUBECONFIG=/path/to/kubeconfig bun full-lifecycle.ts - * - * Requirements: - * - KUBECONFIG environment variable must be set (can be in .env file) + * Devbox Full Lifecycle Example - TEST VERSION + * Testing echo $HOME only */ import { config as loadEnv } from 'dotenv' @@ -38,15 +12,14 @@ import { DevboxRuntime } from '../src/api/types' import { parseKubeconfigServerUrl } from '../src/utils/kubeconfig' // 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 + resolve(__dirname, '.env'), + resolve(__dirname, '../.env'), + resolve(__dirname, '../../.env'), + resolve(process.cwd(), '.env'), ] let envLoaded = false @@ -60,60 +33,25 @@ for (const envPath of envPaths) { } 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 KUBECONFIG=...)') + console.warn('โš ๏ธ .env file not found, using system environment variables') } -// Check for KUBECONFIG - support both .env file and export command if (!process.env.KUBECONFIG) { - console.error('') console.error('โŒ Missing required environment variable: KUBECONFIG') - console.error('') - console.error('Please set it using one of these methods:') - console.error('') - console.error('1. Export file path in shell:') - console.error(' export KUBECONFIG=/path/to/kubeconfig') - console.error(' bun full-lifecycle.ts') - console.error('') - console.error('2. Export kubeconfig content (use $\' for multiline):') - console.error(' export KUBECONFIG=$\'apiVersion: v1\\n...') - console.error('') - console.error('3. Create .env file in project root:') - console.error(' echo "KUBECONFIG=/path/to/kubeconfig" > .env') - console.error('') - console.error('4. Pass inline:') - console.error(' KUBECONFIG=/path/to/kubeconfig bun full-lifecycle.ts') - console.error('') process.exit(1) } -// Handle KUBECONFIG - could be a file path or content let kubeconfigContent = process.env.KUBECONFIG -// If it looks like a file path (doesn't contain 'apiVersion' and exists as file), read it if (!kubeconfigContent.includes('apiVersion') && existsSync(kubeconfigContent)) { - console.log(`๐Ÿ“„ Reading kubeconfig from file: ${kubeconfigContent}`) kubeconfigContent = readFileSync(kubeconfigContent, 'utf-8') } else if (kubeconfigContent.includes('\\n')) { - // If it contains escaped newlines, convert them to actual newlines - console.log('๐Ÿ”„ Converting escaped newlines in kubeconfig...') kubeconfigContent = kubeconfigContent.replace(/\\n/g, '\n') } -// Parse API URL from kubeconfig const kubeconfigUrl = parseKubeconfigServerUrl(kubeconfigContent) if (!kubeconfigUrl) { - console.error('') console.error('โŒ Failed to parse API server URL from kubeconfig') - console.error('') - console.error('Please ensure:') - console.error(' 1. KUBECONFIG is a valid kubeconfig file path or YAML content') - console.error(' 2. The kubeconfig contains a valid server URL') - console.error(' 3. If using multiline content, use $\' syntax in shell') - console.error('') process.exit(1) } @@ -126,7 +64,6 @@ const SDK_CONFIG = { }, } -// Helper function: generate unique name const generateDevboxName = (prefix: string) => { const timestamp = Date.now() const random = Math.floor(Math.random() * 1000) @@ -134,124 +71,14 @@ const generateDevboxName = (prefix: string) => { return `example-${sanitizedPrefix}-${timestamp}-${random}` } -// Helper function: wait for server startup with smart detection -async function waitForServerStartup( - devbox: any, - processId: string, - port = 3000, - maxWaitTime = 180000 -): Promise { - const startTime = Date.now() - const checkInterval = 3000 // Check every 3 seconds - - // Server startup success log keywords - const successPatterns = [ - /Ready in/i, // Next.js: "โœ“ Ready in 2.4s" - /Local:.*http/i, // Next.js: "- Local: http://localhost:3000" - /started server on/i, // "started server on ..." - /Listening on/i, // "Listening on http://..." - ] - - console.log('') - console.log('โณ Waiting for server to start...') - console.log(` Checking logs, port ${port}, and process status...`) - console.log('') - - // Wait 10 seconds first to let pnpm install start running - await new Promise(resolve => setTimeout(resolve, 10000)) - - let lastLogLength = 0 - - while (Date.now() - startTime < maxWaitTime) { - try { - // 1. Check process status - const status = await devbox.getProcessStatus(processId) - - if (status.processStatus === 'failed' || status.processStatus === 'completed') { - console.log('') - console.error(`โŒ Process stopped with status: ${status.processStatus}`) - const logs = await devbox.getProcessLogs(processId) - console.log('๐Ÿ“‹ Full logs:') - console.log(logs.logs.join('\n')) - return false - } - - // 2. Check logs - const logsResponse = await devbox.getProcessLogs(processId) - const allLogs = logsResponse.logs.join('\n') - - // Display new log output - if (allLogs.length > lastLogLength) { - const newLogs = allLogs.substring(lastLogLength) - const newLines = newLogs.split('\n').filter((line: string) => line.trim()) - if (newLines.length > 0) { - console.log('๐Ÿ“‹ New logs:') - for (const line of newLines) { - console.log(` ${line}`) - } - } - lastLogLength = allLogs.length - } - - // Check for startup success indicators - const isReady = successPatterns.some(pattern => pattern.test(allLogs)) - - if (isReady) { - console.log('') - console.log('โœ… Found server ready signal in logs!') - - // 3. Verify if port is open - try { - const portsResponse = await devbox.getPorts() - if (portsResponse.ports.includes(port)) { - console.log(`โœ… Port ${port} is open`) - return true - } - } catch (error) { - console.log('โณ Port check failed, retrying...') - } - console.log(`โณ Port ${port} not yet open, waiting...`) - } - - // Display progress - const elapsed = Math.floor((Date.now() - startTime) / 1000) - process.stdout.write(`\r Status: ${status.processStatus} | 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`) - - // Display latest logs after timeout - try { - const logs = await devbox.getProcessLogs(processId) - console.log('๐Ÿ“‹ Latest logs:') - console.log(logs.logs.join('\n')) - } catch (error) { - // ignore - } - - return false -} - async function main() { const sdk = new DevboxSDK(SDK_CONFIG) - const name = generateDevboxName('full-lifecycle') - 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' + const name = generateDevboxName('test-home') try { - console.log('๐Ÿš€ Starting full lifecycle example...') + console.log('๐Ÿš€ Starting test...') console.log(`๐Ÿ“ฆ Creating devbox: ${name}`) - // 1. Create Devbox const devbox = await sdk.createDevbox({ name, runtime: DevboxRuntime.TEST_AGENT, @@ -259,227 +86,119 @@ async function main() { }) console.log(`โœ… Devbox created: ${devbox.name}`) - - // 2. Start Devbox and wait for Running status 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}`) + // 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 - } + // 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') + // 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`) + // 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) + // 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...') - - // 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, - // Add keep-alive for better connection handling - 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`) + // console.log('๐Ÿ” Calling analyze API...') + // const callAnalyzeAPI = async (retries = 3, timeout = 60000) => { ... } + // const analyzeData = await callAnalyzeAPI() // 7. Check Node.js and npm versions - console.log('') - console.log('๐Ÿ” Checking Node.js and npm versions...') - const nodeVersionResult = await currentDevbox.execSync({ - command: 'node', - args: ['-v'], - }) - console.log(`๐Ÿ“ฆ Node.js version: ${nodeVersionResult.stdout.trim()}`) - - const npmVersionResult = await currentDevbox.execSync({ - command: 'npm', - args: ['-v'], - }) - console.log(`๐Ÿ“ฆ npm version: ${npmVersionResult.stdout.trim()}`) + // const nodeVersionResult = await currentDevbox.execSync({ ... }) + // const npmVersionResult = await currentDevbox.execSync({ ... }) // 8. Enable pnpm via corepack (if needed) - console.log('') - console.log('๐Ÿ”ง Checking package manager requirements...') - - const usesPnpm = analyzeData.entrypoint.includes('pnpm') - - if (usesPnpm) { - console.log('๐Ÿ“ฆ Detected pnpm usage, enabling via corepack...') - try { - await currentDevbox.execSync({ - command: 'corepack', - args: ['enable'], - }) - console.log('โœ… pnpm enabled via corepack') - - const pnpmVersionResult = await currentDevbox.execSync({ - command: 'pnpm', - args: ['-v'], - }) - console.log(`๐Ÿ“ฆ pnpm version: ${pnpmVersionResult.stdout.trim()}`) - } catch (error) { - console.warn('โš ๏ธ Failed to enable pnpm via corepack:', error instanceof Error ? error.message : String(error)) - } - } + // if (usesPnpm) { ... } // 9. Prepare entrypoint.sh with command fixes - const entrypointPath = `${REPO_DIR}/entrypoint.sh` - console.log('') - console.log(`๐Ÿ’พ Preparing entrypoint.sh...`) + // const entrypointPath = `${REPO_DIR}/entrypoint.sh` + // await currentDevbox.writeFile(entrypointPath, entrypointScript, { mode: 0o755 }) - 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 -') + // 11. Configure npm registry + // await currentDevbox.execSync({ command: 'npm', args: ['config', 'set', 'registry', expectedRegistry] }) - await currentDevbox.writeFile(entrypointPath, entrypointScript, { - mode: 0o755, - }) - console.log('โœ… entrypoint.sh written successfully') - - // 10. Configure npm registry - console.log('') - console.log('๐Ÿ”ง Configuring npm registry...') - - const homeDir = '/home/devbox' - const expectedRegistry = 'https://registry.npmmirror.com' - - await currentDevbox.execSync({ - command: 'npm', - args: ['config', 'set', 'registry', expectedRegistry], - cwd: REPO_DIR, - env: { HOME: homeDir }, - }) - console.log(`โœ… npm registry set to: ${expectedRegistry}`) - - // 11. Start entrypoint.sh (run asynchronously in background to avoid timeout) - console.log('') - console.log('๐Ÿš€ Starting application via entrypoint.sh...') - - const serverProcess = await currentDevbox.executeCommand({ - command: 'bash', - args: [entrypointPath, 'development'], - cwd: REPO_DIR, - env: { - HOME: homeDir, - NPM_CONFIG_USERCONFIG: `${homeDir}/.npmrc`, - NPM_CONFIG_CACHE: `${homeDir}/.npm`, - }, - timeout: 600, - }) - - console.log(`โœ… Application started!`) - console.log(` Process ID: ${serverProcess.processId}`) - console.log(` PID: ${serverProcess.pid}`) - - // Intelligently wait for server startup (check logs + port + process status) - const isReady = await waitForServerStartup(currentDevbox, serverProcess.processId, 3000, 180000) - - if (!isReady) { - throw new Error('Server failed to start within timeout') - } + // 12. Start entrypoint.sh + // const serverProcess = await currentDevbox.executeCommand({ ... }) + // const isReady = await waitForServerStartup(currentDevbox, serverProcess.processId, 3000, 180000) // Get preview URL for port 3000 - console.log('') - console.log('๐Ÿ”— Getting preview URL for port 3000...') - try { - const previewLink = await currentDevbox.getPreviewLink(3000) - console.log(`โœ… Preview URL: ${previewLink.url}`) - console.log(` Protocol: ${previewLink.protocol}`) - console.log(` Port: ${previewLink.port}`) - } catch (error) { - console.warn('โš ๏ธ Failed to get preview URL:', error instanceof Error ? error.message : String(error)) - console.warn(' This might be because the devbox does not have port 3000 configured or agentServer is not available') - } + // const previewLink = await currentDevbox.getPreviewLink(3000) +*/ console.log('') - console.log('๐ŸŽ‰ Full lifecycle example completed successfully!') - console.log('') - console.log('๐Ÿ“‹ Summary:') - console.log(` Devbox: ${name}`) - console.log(` Repository: ${REPO_URL}`) - console.log(` Project Dir: ${REPO_DIR}`) - console.log(` Server: npm run dev`) - console.log('') - console.log('๐Ÿ’ก Cleanup: Delete devbox when done') - console.log(` sdk.getDevbox('${name}').then(d => d.delete())`) + console.log('๐ŸŽ‰ Test completed!') } catch (error) { console.error('โŒ Error occurred:', error) @@ -489,9 +208,7 @@ async function main() { } } -// Run the example main().catch((error) => { console.error('Failed to run example:', error) process.exit(1) }) - diff --git a/packages/sdk/src/core/devbox-instance.ts b/packages/sdk/src/core/devbox-instance.ts index 507eda1..c607411 100644 --- a/packages/sdk/src/core/devbox-instance.ts +++ b/packages/sdk/src/core/devbox-instance.ts @@ -656,6 +656,48 @@ export class DevboxInstance { // } // Process execution + /** + * Wrap command with shell execution (base64 encoding + sh -c) + * Mimics Daytona SDK behavior for consistent shell feature support + * @param options Process execution options + * @returns Modified options with shell wrapper + */ + private wrapCommandWithShell(options: ProcessExecOptions): { + command: string + cwd?: string + timeout?: number + } { + // Build full command string + let fullCommand = options.command + if (options.args && options.args.length > 0) { + fullCommand = `${options.command} ${options.args.join(' ')}` + } + + // Base64 encode the command + const base64UserCmd = Buffer.from(fullCommand).toString('base64') + let command = `echo '${base64UserCmd}' | base64 -d | sh` + + // Handle environment variables with base64 encoding + if (options.env && Object.keys(options.env).length > 0) { + const safeEnvExports = `${Object.entries(options.env) + .map(([key, value]) => { + const encodedValue = Buffer.from(value).toString('base64') + return `export ${key}=$(echo '${encodedValue}' | base64 -d)` + }) + .join(';')};` + command = `${safeEnvExports} ${command}` + } + + // Wrap with sh -c + command = `sh -c "${command}"` + + return { + command, + cwd: options.cwd, + timeout: options.timeout, + } + } + /** * Execute a process asynchronously * @param options Process execution options @@ -663,18 +705,13 @@ export class DevboxInstance { */ async executeCommand(options: ProcessExecOptions): Promise { const urlResolver = this.sdk.getUrlResolver() + const wrappedOptions = this.wrapCommandWithShell(options) + 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, - cwd: options.cwd, - env: options.env, - shell: options.shell, - timeout: options.timeout, - }, + body: wrappedOptions, } ) return response.data @@ -688,18 +725,13 @@ export class DevboxInstance { */ async execSync(options: ProcessExecOptions): Promise { const urlResolver = this.sdk.getUrlResolver() + const wrappedOptions = this.wrapCommandWithShell(options) + 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, - cwd: options.cwd, - env: options.env, - shell: options.shell, - timeout: options.timeout, - }, + body: wrappedOptions, } ) return response.data @@ -743,10 +775,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') @@ -754,10 +787,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}` } /** From 36aca3cfb1871bc0f434d0bd12562401b6fd9ea7 Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Wed, 31 Dec 2025 17:47:41 +0800 Subject: [PATCH 09/10] test --- packages/sdk/src/core/devbox-instance.ts | 74 +++++++++--------------- 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/packages/sdk/src/core/devbox-instance.ts b/packages/sdk/src/core/devbox-instance.ts index c607411..d48c004 100644 --- a/packages/sdk/src/core/devbox-instance.ts +++ b/packages/sdk/src/core/devbox-instance.ts @@ -656,62 +656,32 @@ export class DevboxInstance { // } // Process execution - /** - * Wrap command with shell execution (base64 encoding + sh -c) - * Mimics Daytona SDK behavior for consistent shell feature support - * @param options Process execution options - * @returns Modified options with shell wrapper - */ - private wrapCommandWithShell(options: ProcessExecOptions): { - command: string - cwd?: string - timeout?: number - } { - // Build full command string - let fullCommand = options.command - if (options.args && options.args.length > 0) { - fullCommand = `${options.command} ${options.args.join(' ')}` - } - - // Base64 encode the command - const base64UserCmd = Buffer.from(fullCommand).toString('base64') - let command = `echo '${base64UserCmd}' | base64 -d | sh` - - // Handle environment variables with base64 encoding - if (options.env && Object.keys(options.env).length > 0) { - const safeEnvExports = `${Object.entries(options.env) - .map(([key, value]) => { - const encodedValue = Buffer.from(value).toString('base64') - return `export ${key}=$(echo '${encodedValue}' | base64 -d)` - }) - .join(';')};` - command = `${safeEnvExports} ${command}` - } - - // Wrap with sh -c - command = `sh -c "${command}"` - - return { - command, - cwd: options.cwd, - timeout: options.timeout, - } - } - /** * Execute a process asynchronously + * All commands are automatically executed through shell for consistent behavior * @param options Process execution options * @returns Process execution response with process_id and pid */ async executeCommand(options: ProcessExecOptions): Promise { const urlResolver = this.sdk.getUrlResolver() - const wrappedOptions = this.wrapCommandWithShell(options) + + // Build command string with args + let command = options.command + if (options.args && options.args.length > 0) { + command = `${options.command} ${options.args.join(' ')}` + } return await urlResolver.executeWithConnection(this.name, async client => { const response = await client.post( API_ENDPOINTS.CONTAINER.PROCESS.EXEC, { - body: wrappedOptions, + body: { + command, + cwd: options.cwd, + env: options.env, + shell: 'sh', // Always use shell for consistent behavior + timeout: options.timeout, + }, } ) return response.data @@ -720,18 +690,30 @@ export class DevboxInstance { /** * Execute a process synchronously and wait for completion + * All commands are automatically executed through shell for consistent behavior * @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() - const wrappedOptions = this.wrapCommandWithShell(options) + + // Build command string with args + let command = options.command + if (options.args && options.args.length > 0) { + command = `${options.command} ${options.args.join(' ')}` + } return await urlResolver.executeWithConnection(this.name, async client => { const response = await client.post( API_ENDPOINTS.CONTAINER.PROCESS.EXEC_SYNC, { - body: wrappedOptions, + body: { + command, + cwd: options.cwd, + env: options.env, + shell: 'sh', // Always use shell for consistent behavior + timeout: options.timeout, + }, } ) return response.data From 6f933cfb29891362896a2f4b5476a8729b72e8ce Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Wed, 31 Dec 2025 17:54:03 +0800 Subject: [PATCH 10/10] update --- packages/sdk/src/core/devbox-instance.ts | 28 ++++++++++++++---------- packages/server-rust/docs/openapi.yaml | 8 ------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/sdk/src/core/devbox-instance.ts b/packages/sdk/src/core/devbox-instance.ts index d48c004..4ab07a2 100644 --- a/packages/sdk/src/core/devbox-instance.ts +++ b/packages/sdk/src/core/devbox-instance.ts @@ -658,28 +658,30 @@ export class DevboxInstance { // Process execution /** * Execute a process asynchronously - * All commands are automatically executed through shell for consistent behavior + * 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 command string with args - let command = options.command + // Build full command string + let fullCommand = options.command if (options.args && options.args.length > 0) { - command = `${options.command} ${options.args.join(' ')}` + 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, + command: 'sh', + args: ['-c', fullCommand], cwd: options.cwd, env: options.env, - shell: 'sh', // Always use shell for consistent behavior timeout: options.timeout, }, } @@ -690,28 +692,30 @@ export class DevboxInstance { /** * Execute a process synchronously and wait for completion - * All commands are automatically executed through shell for consistent behavior + * 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 command string with args - let command = options.command + // Build full command string + let fullCommand = options.command if (options.args && options.args.length > 0) { - command = `${options.command} ${options.args.join(' ')}` + 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, + command: 'sh', + args: ['-c', fullCommand], cwd: options.cwd, env: options.env, - shell: 'sh', // Always use shell for consistent behavior timeout: options.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