diff --git a/.changeset/config.json b/.changeset/config.json index 0fa7607..abafb0c 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -10,7 +10,7 @@ "fixed": [], "linked": [ [ - "devbox-sdk", + "@labring/devbox-sdk", "devbox-shared" ] ], diff --git a/.github/workflows/links-checker-schedule.yml b/.github/workflows/links-checker-schedule.yml deleted file mode 100644 index ec66ca2..0000000 --- a/.github/workflows/links-checker-schedule.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Links Checker (On Schedule) - -on: - repository_dispatch: - workflow_dispatch: - schedule: - - cron: "00 18 * * *" - -jobs: - linkChecker: - runs-on: ubuntu-latest - steps: - - name: Check out repo - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Link Checker - id: lychee - uses: lycheeverse/lychee-action@2b973e86fc7b1f6b36a93795fe2c9c6ae1118621 # v1.10.0 - - - name: Create Issue From File - if: env.lychee_exit_code != 0 - uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0 - with: - title: Link Checker Report - content-filepath: ./lychee/out.md - labels: report, automated issue \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1ac294..0863f88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,13 @@ name: release on: push: branches: [main, master] + paths: + - 'packages/sdk/**' + - 'packages/shared/**' + - '.changeset/**' + - 'package.json' + - 'pnpm-lock.yaml' + - '.github/workflows/release.yml' workflow_dispatch: concurrency: ${{ github.workflow }}-${{ github.ref }} diff --git a/apps/docs/BUILD.md b/apps/docs/BUILD.md index e5da6d5..9e823b6 100644 --- a/apps/docs/BUILD.md +++ b/apps/docs/BUILD.md @@ -13,8 +13,6 @@ docker build --platform linux/amd64 -f apps/docs/Dockerfile -t devbox-docs:lates docker run -p 3000:3000 devbox-docs:latest ``` -Then visit http://localhost:3000 - --- ## πŸ”§ Advanced Usage @@ -111,8 +109,8 @@ docker build --no-cache --platform linux/amd64 -f apps/docs/Dockerfile -t devbox ## 🎯 Why Build from Root? -This project uses npm workspaces: -- `package-lock.json` is only in the root directory +This project uses pnpm workspaces: +- `pnpm-lock.yaml` and `pnpm-workspace.yaml` are only in the root directory - Dependencies are hoisted to root `node_modules` - Workspace resolution requires the full monorepo context diff --git a/apps/docs/Dockerfile b/apps/docs/Dockerfile index ab6e817..d6fbc0b 100644 --- a/apps/docs/Dockerfile +++ b/apps/docs/Dockerfile @@ -1,20 +1,23 @@ -# Dockerfile for Next.js app in npm workspaces monorepo +# Dockerfile for Next.js app in pnpm workspaces monorepo # MUST be built from the monorepo root directory: # docker build --platform linux/amd64 -f apps/docs/Dockerfile -t devbox-docs:latest . FROM node:22-alpine AS base +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + # Install dependencies only when needed FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app # Copy root package files for workspace resolution -COPY package.json package-lock.json ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/docs/package.json ./apps/docs/ # Install all dependencies (respects workspaces) -RUN npm ci +RUN pnpm install --frozen-lockfile # Rebuild the source code only when needed FROM base AS builder @@ -25,12 +28,12 @@ COPY --from=deps /app/apps/docs/node_modules ./apps/docs/node_modules # Copy only the docs app source COPY apps/docs ./apps/docs -COPY package.json package-lock.json ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ # Build the docs app ENV NEXT_TELEMETRY_DISABLED=1 WORKDIR /app/apps/docs -RUN npm run build +RUN pnpm run build # Production image FROM base AS runner diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 2ad50bb..ab2caf2 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -353,9 +353,6 @@ Represents a single sandbox instance with methods for code execution, file opera - `moveFile(options: MoveFileOptions): Promise` - `renameFile(options: RenameFileOptions): Promise` -**File Watching:** -- `watchFiles(path: string, callback: (event: FileChangeEvent) => void): Promise` - **Git Operations:** - `git.clone(options: GitCloneOptions): Promise` - `git.pull(options: GitPullOptions): Promise` diff --git a/packages/sdk/examples/full-lifecycle.ts b/packages/sdk/examples/full-lifecycle.ts index 0556e06..31a68d5 100644 --- a/packages/sdk/examples/full-lifecycle.ts +++ b/packages/sdk/examples/full-lifecycle.ts @@ -1,6 +1,27 @@ /** - * Devbox Full Lifecycle Example - TEST VERSION - * Testing echo $HOME only + * 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 + * 6. Configure npm registry + * 7. Start application server + * + * 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 + * + * Requirements: + * - KUBECONFIG environment variable must be set (can be in .env file) */ import { config as loadEnv } from 'dotenv' @@ -71,134 +92,337 @@ const generateDevboxName = (prefix: string) => { return `example-${sanitizedPrefix}-${timestamp}-${random}` } +// Helper function: wait for server startup with smart detection +async function waitForServerStartup( + devbox: any, // eslint-disable-line @typescript-eslint/no-explicit-any + processId: string, + port: number, + maxWaitTime = 180000 +): Promise<{ success: boolean; duration: number }> { + const startTime = Date.now() + const checkInterval = 3000 // Check every 3 seconds + + console.log('') + console.log('⏳ Waiting for server to start...') + console.log(` Checking process ${processId} and port ${port}...`) + 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 process status + const processStatus = await devbox.getProcessStatus(processId) + const isRunning = processStatus.status === 'running' + + // Check if port is listening + const ports = await devbox.getPorts() + const isPortListening = ports.ports?.includes(port) || false + + // Display progress + const elapsed = Math.floor((Date.now() - startTime) / 1000) + process.stdout.write( + `\r Process: ${processStatus.status} | Port ${port}: ${isPortListening ? 'listening' : 'not listening'} | Elapsed: ${elapsed}s` + ) + + if (isRunning && isPortListening) { + const duration = Date.now() - startTime + console.log('') + console.log(`βœ… Server is ready! (${(duration / 1000).toFixed(2)}s)`) + return { success: true, duration } + } + + // Check if process has failed + if (processStatus.status === 'exited' && processStatus.exitCode !== 0) { + const duration = Date.now() - startTime + console.log('') + console.error(`❌ Process exited with code ${processStatus.exitCode}`) + return { success: false, duration } + } + + 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)) + } + } + + const duration = Date.now() - startTime + console.log('') + console.warn(`⚠️ Server did not start within ${maxWaitTime / 1000}s`) + return { success: false, duration } +} + async function main() { const sdk = new DevboxSDK(SDK_CONFIG) - const name = generateDevboxName('test-home') + 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' try { - console.log('πŸš€ Starting test...') + const overallStartTime = Date.now() + console.log('πŸš€ Starting Devbox full lifecycle example...') console.log(`πŸ“¦ Creating devbox: ${name}`) + // 1. Create Devbox + const createStartTime = Date.now() const devbox = await sdk.createDevbox({ name, runtime: DevboxRuntime.TEST_AGENT, resource: { cpu: 1, memory: 2 }, }) + const createDuration = Date.now() - createStartTime + console.log(`βœ… Devbox created: ${devbox.name} (${(createDuration / 1000).toFixed(2)}s)`) - console.log(`βœ… Devbox created: ${devbox.name}`) + // 2. Start devbox 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) { + while (currentDevbox.status !== 'Running' && Date.now() - startTime < 60000) { await new Promise(resolve => setTimeout(resolve, 2000)) currentDevbox = await sdk.getDevbox(name) process.stdout.write('.') } + const waitDuration = Date.now() - startTime + const totalStartupTime = Date.now() - overallStartTime console.log('') console.log(`βœ… Devbox is ${currentDevbox.status}`) + console.log(` ⏱️ Startup time: ${(waitDuration / 1000).toFixed(2)}s (wait) + ${(createDuration / 1000).toFixed(2)}s (create) = ${(totalStartupTime / 1000).toFixed(2)}s (total)`) + + // 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('') + console.log('🧹 Cleaning up directory...') + try { + await currentDevbox.execSync({ + command: 'rm', + args: ['-rf', REPO_DIR], + }) + } catch { + // Ignore errors if directory doesn't exist + } - // TEST: Check HOME environment variable - // Now both methods work because all commands are wrapped with sh -c by default + // 5. Git clone repository console.log('') - console.log('πŸ” Testing echo $HOME...') + console.log(`πŸ“₯ Cloning repository: ${REPO_URL}`) + const cloneStartTime = Date.now() + await currentDevbox.git.clone({ + url: REPO_URL, + targetDir: REPO_DIR, + }) + const cloneDuration = Date.now() - cloneStartTime + console.log(`βœ… Repository cloned successfully (${(cloneDuration / 1000).toFixed(2)}s)`) + + // 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`) - // Method 1: Direct command (now automatically wrapped with sh -c) - const homeResult1 = await currentDevbox.execSync({ - command: 'echo', - args: ['$HOME'], + // List directory contents using ls command + console.log('πŸ“‹ Listing directory contents:') + const lsResult = await currentDevbox.execSync({ + command: 'ls', + args: ['-la', REPO_DIR], }) - console.log('Method 1 (echo $HOME):', homeResult1) - console.log(`πŸ“ HOME: ${homeResult1.stdout.trim()}`) + console.log(lsResult.stdout) + + // 6. Call analyze API using fetch with retry logic + console.log('') + console.log('πŸ” Calling analyze API...') + const analyzeStartTime = Date.now() + + 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)) + } + } + } - // Method 2: Using environment variable in command - const homeResult2 = await currentDevbox.execSync({ - command: 'echo', - args: ['My home is $HOME'], + const analyzeData = await callAnalyzeAPI() + const analyzeDuration = Date.now() - analyzeStartTime + console.log(`βœ… Analyze API response received (${(analyzeDuration / 1000).toFixed(2)}s)`) + console.log('analyzeData:', analyzeData); + console.log(`πŸ“ Entrypoint length: ${analyzeData.entrypoint?.length || 0} characters`) + + // Extract port from analyze API response + const appPort = analyzeData.port || 3000 // Default to 3000 if not specified + console.log(`πŸ”Œ Application port: ${appPort}`) + + // 7. Check Node.js and npm versions + console.log('') + console.log('πŸ” Checking Node.js and npm versions...') + const versionCheckStartTime = Date.now() + const nodeVersionResult = await currentDevbox.execSync({ + command: 'node', + args: ['-v'], }) - console.log('Method 2 (echo My home is $HOME):', homeResult2) - console.log(`πŸ“ Result: ${homeResult2.stdout.trim()}`) + console.log(`πŸ“¦ Node.js version: ${nodeVersionResult.stdout.trim() || 'N/A'}`) - // Method 3: Using pipes (shell feature) - const homeResult3 = await currentDevbox.execSync({ - command: 'echo $HOME | wc -c', + const npmVersionResult = await currentDevbox.execSync({ + command: 'npm', + args: ['-v'], }) - console.log('Method 3 (pipe test):', homeResult3) - console.log(`πŸ“ HOME length: ${homeResult3.stdout.trim()} characters`) + console.log(`πŸ“¦ npm version: ${npmVersionResult.stdout.trim() || 'N/A'}`) -/* - // εŽŸζœ‰ηš„ε…Άδ»–ζ“δ½œιƒ½ε·²ζ³¨ι‡Š - // 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' + // 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 currentDevbox.execSync({ + command: 'pnpm', + args: ['-v'], + }) + console.log(`πŸ“¦ pnpm version: ${pnpmVersionResult.stdout.trim() || 'N/A'}`) + } catch (error) { + console.warn('⚠️ pnpm not available:', error instanceof Error ? error.message : String(error)) + console.log('πŸ“¦ Enabling pnpm via corepack...') + await currentDevbox.execSync({ + command: 'corepack', + args: ['enable'], + }) + await currentDevbox.execSync({ + command: 'corepack', + args: ['prepare', 'pnpm@latest', '--activate'], + }) + } + } + const versionCheckDuration = Date.now() - versionCheckStartTime - // 3. Fetch devbox info to verify it's ready - // const fetchedDevbox = await sdk.getDevbox(name) - // console.log(`πŸ“‹ Devbox info: ${fetchedDevbox.name} - ${fetchedDevbox.status}`) + // 9. Prepare entrypoint.sh with command fixes + const entrypointPath = `${REPO_DIR}/entrypoint.sh` + console.log('') + console.log(`πŸ’Ύ Preparing entrypoint.sh...`) + const entrypointStartTime = Date.now() - // 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 - // } + 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 -') - // 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') + await currentDevbox.writeFile(entrypointPath, entrypointScript, { mode: 0o755 }) + const entrypointDuration = Date.now() - entrypointStartTime + console.log(`βœ… entrypoint.sh written successfully (${(entrypointDuration / 1000).toFixed(2)}s)`) - // 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`) + // 10. Configure npm registry + console.log('') + console.log('πŸ”§ Configuring npm registry...') + const registryStartTime = Date.now() - // 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) + const expectedRegistry = 'https://registry.npmmirror.com' - // 6. Call analyze API using fetch with retry logic - // console.log('πŸ” Calling analyze API...') - // const callAnalyzeAPI = async (retries = 3, timeout = 60000) => { ... } - // const analyzeData = await callAnalyzeAPI() + const registryResult = await currentDevbox.execSync({ + command: 'npm', + args: ['config', 'set', 'registry', expectedRegistry], + }) + const registryDuration = Date.now() - registryStartTime + if (registryResult.exitCode !== 0) { + console.warn(`⚠️ Failed to set npm registry: ${registryResult.stderr}`) + } else { + console.log(`βœ… npm registry set to: ${expectedRegistry} (${(registryDuration / 1000).toFixed(2)}s)`) + } - // 7. Check Node.js and npm versions - // const nodeVersionResult = await currentDevbox.execSync({ ... }) - // const npmVersionResult = await currentDevbox.execSync({ ... }) + // 11. Start entrypoint.sh (run asynchronously in background) + console.log('') + console.log('πŸš€ Starting application via entrypoint.sh...') + const devStartStartTime = Date.now() - // 8. Enable pnpm via corepack (if needed) - // if (usesPnpm) { ... } + const serverProcess = await currentDevbox.executeCommand({ + command: `bash ${entrypointPath} development`, + cwd: REPO_DIR, + }) + console.log(`βœ… Application started!`) + console.log(` Process ID: ${serverProcess.processId}`) - // 9. Prepare entrypoint.sh with command fixes - // const entrypointPath = `${REPO_DIR}/entrypoint.sh` - // await currentDevbox.writeFile(entrypointPath, entrypointScript, { mode: 0o755 }) + // Wait for server startup (this is the slowest part - dev server startup) + const startupResult = await waitForServerStartup(currentDevbox, serverProcess.processId, appPort, 180000) + const devStartDuration = Date.now() - devStartStartTime - // 11. Configure npm registry - // await currentDevbox.execSync({ command: 'npm', args: ['config', 'set', 'registry', expectedRegistry] }) + if (!startupResult.success) { + console.warn('⚠️ Server may not have started within timeout, but continuing...') + } - // 12. Start entrypoint.sh - // const serverProcess = await currentDevbox.executeCommand({ ... }) - // const isReady = await waitForServerStartup(currentDevbox, serverProcess.processId, 3000, 180000) + // Get preview URL for the application port + console.log('') + console.log(`πŸ”— Getting preview URL for port ${appPort}...`) + try { + const previewLink = await currentDevbox.getPreviewLink(appPort) + console.log(`βœ… Preview URL: ${previewLink.url}`) + } 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 ${appPort} configured`) + } - // Get preview URL for port 3000 - // const previewLink = await currentDevbox.getPreviewLink(3000) -*/ + const totalDuration = Date.now() - overallStartTime console.log('') - console.log('πŸŽ‰ Test completed!') + console.log('πŸŽ‰ Devbox 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('⏱️ Time Statistics:') + console.log(` β€’ Devbox creation: ${(createDuration / 1000).toFixed(2)}s`) + console.log(` β€’ Devbox startup: ${(waitDuration / 1000).toFixed(2)}s`) + console.log(` β€’ Git clone: ${(cloneDuration / 1000).toFixed(2)}s`) + console.log(` β€’ Analyze API: ${(analyzeDuration / 1000).toFixed(2)}s`) + console.log(` β€’ Version check: ${(versionCheckDuration / 1000).toFixed(2)}s`) + console.log(` β€’ Entrypoint prep: ${(entrypointDuration / 1000).toFixed(2)}s`) + console.log(` β€’ Registry config: ${(registryDuration / 1000).toFixed(2)}s`) + console.log(` β€’ Dev server startup: ${(devStartDuration / 1000).toFixed(2)}s ⏳ (slowest step)`) + console.log(` ─────────────────────────────`) + console.log(` β€’ Total time: ${(totalDuration / 1000).toFixed(2)}s`) + console.log('') } catch (error) { console.error('❌ Error occurred:', error) diff --git a/packages/sdk/tests/base/devbox-sdk-core.test.ts b/packages/sdk/tests/base/devbox-sdk-core.test.ts index 42e790a..7e16912 100644 --- a/packages/sdk/tests/base/devbox-sdk-core.test.ts +++ b/packages/sdk/tests/base/devbox-sdk-core.test.ts @@ -39,7 +39,6 @@ describe('DevboxSDK', () => { it('should accept valid configuration', () => { const validConfig: DevboxSDKConfig = { kubeconfig: 'test-kubeconfig', - baseUrl: 'http://localhost:3000', http: { timeout: 10000, }, @@ -54,7 +53,6 @@ describe('DevboxSDK', () => { it('should use default timeout value', () => { const config: DevboxSDKConfig = { kubeconfig: 'test', - baseUrl: 'http://localhost:3000', } const testSdk = new DevboxSDK(config) @@ -65,7 +63,6 @@ describe('DevboxSDK', () => { it('should use custom timeout value', () => { const config: DevboxSDKConfig = { kubeconfig: 'test', - baseUrl: 'http://localhost:3000', http: { timeout: 60000, },