| import fs from 'fs/promises'; |
| import path from 'path'; |
| import type Docker from 'dockerode'; |
| import * as tar from 'tar-fs'; |
|
|
| export interface CodeverseConfig { |
| env?: Record<string, string>; |
| packages?: { |
| apt?: string[]; |
| npm?: string[]; |
| }; |
| ios?: { |
| appetizeUrl?: string; |
| }; |
| } |
|
|
| |
| |
| |
| |
| function parseBasicNix(nixContent: string): CodeverseConfig { |
| const config: CodeverseConfig = { packages: { apt: [], npm: [] }, env: {} }; |
|
|
| |
| const pkgMatches = nixContent.matchAll(/pkgs\.([a-zA-Z0-9_\-]+)/g); |
| for (const match of pkgMatches) { |
| let pkgName = match[1]; |
| |
| if (pkgName.startsWith('nodejs')) pkgName = 'nodejs npm'; |
| else if (pkgName.startsWith('python')) pkgName = 'python3 python3-pip'; |
| else if (pkgName === 'go') pkgName = 'golang'; |
| else if (pkgName === 'rust') pkgName = 'rustc cargo'; |
| |
| const split = pkgName.split(' '); |
| for (const p of split) { |
| if (!config.packages?.apt?.includes(p)) { |
| config.packages?.apt?.push(p); |
| } |
| } |
| } |
|
|
| |
| const envBlockMatch = nixContent.match(/env\s*=\s*{([^}]+)}/); |
| if (envBlockMatch) { |
| const envLines = envBlockMatch[1].split('\n'); |
| for (const line of envLines) { |
| const kvMatch = line.trim().match(/([a-zA-Z0-9_]+)\s*=\s*['"]?([^'";]+)['"]?\s*;/); |
| if (kvMatch) { |
| if (config.env) config.env[kvMatch[1]] = kvMatch[2]; |
| } |
| } |
| } |
|
|
| return config; |
| } |
|
|
| export async function loadWorkspaceConfig(workspacePath: string): Promise<CodeverseConfig> { |
| const codeverseJsonPath = path.join( workspacePath, 'codeverse.json'); |
| const idxNixPath = path.join( workspacePath, '.idx', 'dev.nix'); |
|
|
| |
| try { |
| const nixContent = await fs.readFile(idxNixPath, 'utf-8'); |
| return parseBasicNix(nixContent); |
| } catch { |
| |
| } |
|
|
| |
| try { |
| const jsonContent = await fs.readFile(codeverseJsonPath, 'utf-8'); |
| return JSON.parse(jsonContent); |
| } catch (err: unknown) { |
| const fsErr = err as Error & { code?: string }; |
| if (fsErr.code === 'ENOENT') { |
| |
| const defaultConfig: CodeverseConfig = { |
| "env": { "PORT": "3000" }, |
| "packages": { "apt": [], "npm": [] } |
| }; |
| await fs.writeFile(codeverseJsonPath, JSON.stringify(defaultConfig, null, 2)); |
| return defaultConfig; |
| } |
| throw err; |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function buildWorkspaceImage( |
| workspaceId: string, |
| workspacePath: string, |
| onLog: (msg: string) => void, |
| docker: Docker |
| ): Promise<{ imageName: string, config: CodeverseConfig }> { |
|
|
| onLog("Loading workspace configuration (codeverse.json or dev.nix)..."); |
| const config = await loadWorkspaceConfig(workspacePath); |
|
|
| |
| let dockerfile = `FROM codercom/code-server:latest\n`; |
| dockerfile += `USER root\n`; |
|
|
| |
| if (config.packages?.apt && config.packages.apt.length > 0) { |
| const aptList = config.packages.apt.join(' '); |
| dockerfile += `RUN apt-get update && apt-get install -y ${aptList} && rm -rf /var/lib/apt/lists/*\n`; |
| } |
|
|
| |
| if (config.packages?.npm && config.packages.npm.length > 0) { |
| |
| if (!config.packages.apt?.some(p => p.includes('npm'))) { |
| dockerfile += `RUN apt-get update && apt-get install -y nodejs npm && rm -rf /var/lib/apt/lists/*\n`; |
| } |
| const npmList = config.packages.npm.join(' '); |
| dockerfile += `RUN npm install -g ${npmList}\n`; |
| } |
|
|
| |
| |
| if (config.env) { |
| for (const [k, v] of Object.entries(config.env)) { |
| dockerfile += `ENV ${k}="${v}"\n`; |
| dockerfile += `RUN echo 'export ${k}="${v}"' >> /home/coder/.bashrc\n`; |
| } |
| } |
|
|
| dockerfile += `USER coder\n`; |
| |
| dockerfile += `WORKDIR /home/coder/project\n`; |
|
|
| |
| const buildDir = path.join( workspacePath, '.codeverse'); |
| await fs.mkdir(buildDir, { recursive: true }); |
|
|
| const dockerfilePath = path.join( buildDir, 'Dockerfile'); |
| await fs.writeFile(dockerfilePath, dockerfile); |
|
|
| const imageName = `codeverse-workspace-${workspaceId}`; |
| onLog(`Building image ${imageName}...`); |
|
|
| return new Promise((resolve, reject) => { |
| |
| const pack = tar.pack(buildDir); |
| docker.buildImage(pack, { t: imageName }, (err: Error | null, stream?: NodeJS.ReadableStream) => { |
| if (err) return reject(err); |
| if (!stream) return reject(new Error("No stream returned from Docker build")); |
|
|
| docker.modem.followProgress(stream, |
| (err2: Error | null) => { |
| if (err2) return reject(err2); |
| onLog(`Image ${imageName} built successfully.`); |
| resolve({ imageName, config }); |
| }, |
| (event: { stream?: string, error?: string, status?: string }) => { |
| if (event.stream) onLog(event.stream.trim()); |
| else if (event.status) onLog(event.status.trim()); |
| else if (event.error) onLog(`Build Error: ${event.error}`); |
| } |
| ); |
| }); |
| }); |
| } |
|
|