| import { readdir, access, constants, readFile, writeFile } from 'node:fs/promises'; |
| import { join, extname } from 'node:path'; |
|
|
| async function checkFileExists(filepath) { |
| try { |
| await access(filepath, constants.F_OK); |
| return true; |
| } catch { |
| return false; |
| } |
| } |
|
|
| async function hasInstallLock(projectPath, type) { |
| try { |
| const lockPath = join(projectPath, '.project-lock'); |
| const content = await readFile(lockPath, 'utf-8'); |
| const locks = JSON.parse(content); |
| return locks[type] === true; |
| } catch { |
| return false; |
| } |
| } |
|
|
| async function setInstallLock(projectPath, type) { |
| const lockPath = join(projectPath, '.project-lock'); |
| let locks = {}; |
| try { |
| const content = await readFile(lockPath, 'utf-8'); |
| locks = JSON.parse(content); |
| } catch {} |
| locks[type] = true; |
| await writeFile(lockPath, JSON.stringify(locks, null, 2)); |
| } |
|
|
| async function clearInstallLock(projectPath, type) { |
| const lockPath = join(projectPath, '.project-lock'); |
| let locks = {}; |
| try { |
| const content = await readFile(lockPath, 'utf-8'); |
| locks = JSON.parse(content); |
| } catch {} |
| if (locks[type]) { |
| delete locks[type]; |
| await writeFile(lockPath, JSON.stringify(locks, null, 2)); |
| } |
| } |
|
|
| async function hasStreamlit(projectPath) { |
| try { |
| const reqPath = join(projectPath, 'requirements.txt'); |
| const content = await readFile(reqPath, 'utf-8'); |
| return content.includes('streamlit'); |
| } catch { |
| return false; |
| } |
| } |
|
|
| function detectPort(output) { |
| const patterns = [ |
| /Local:\s+(?:https?:\/\/)?(?:[^:\s]+):(\d{4,5})/i, |
| /Local:\s+.*?(https?:\/\/)?[^:\s]+:(\d{4,5})/i, |
| /(?:localhost|127\.0\.0\.1|0\.0\.0\.0):(\d{4,5})/i, |
| /(?:port|listening on|running on)\s*[:=]?\s*(?:https?:\/\/)?(?:[^:\s]+:)?(\d{4,5})/i, |
| /\*\s+Running on.*:(\d{4,5})/i, |
| /You can now view your.*at.*:(\d{4,5})/i, |
| /Local URL:.*:(\d{4,5})/i, |
| /port\s*(\d{4,5})/i, |
| /PORT\s*(\d{4,5})/i, |
| /server.*(?:port|addr|address).*?(\d{4,5})/i |
| ]; |
| |
| for (const pattern of patterns) { |
| const match = output.match(pattern); |
| if (match) { |
| return match[1] || match[2] || match[3]; |
| } |
| } |
| |
| return null; |
| } |
|
|
| const LANGUAGE_CONFIGS = { |
| nodejs: { |
| files: ['package.json'], |
| setup: async (projectPath, execAsync) => { |
| const nodeModulesPath = join(projectPath, 'node_modules'); |
| const viteBin = join(projectPath, 'node_modules', '.bin', 'vite'); |
| const hasNodeModules = await checkFileExists(nodeModulesPath); |
| const hasViteBin = await checkFileExists(viteBin); |
| |
| if (!hasNodeModules || !hasViteBin) { |
| console.log(`[nodejs] Installing dependencies in ${projectPath}...`); |
| try { |
| await execAsync('npm install --include=dev', { cwd: projectPath }); |
| console.log(`[nodejs] Dependencies installed successfully`); |
| } catch (error) { |
| console.error(`[nodejs] npm install failed:`, error.message); |
| throw error; |
| } |
| } |
| }, |
| start: (projectPath, port) => { |
| const viteBin = join(projectPath, 'node_modules', '.bin', 'vite'); |
| return { |
| command: 'node', |
| args: [viteBin, '--port', port, '--host', '0.0.0.0'], |
| cwd: projectPath, |
| env: { ...process.env, PORT: port, HOST: '0.0.0.0' } |
| }; |
| }, |
| detectPort |
| }, |
| python: { |
| files: ['main.py', 'app.py', 'server.py', 'requirements.txt'], |
| setup: async (projectPath, execAsync, isStreamlit = false) => { |
| const venvPath = join(projectPath, 'venv'); |
| const venvActivate = join(projectPath, 'venv', 'bin', 'activate'); |
| const venvPython = join(projectPath, 'venv', 'bin', 'python'); |
| const venvStreamlit = join(projectPath, 'venv', 'bin', 'streamlit'); |
| const hasVenv = await checkFileExists(venvPath); |
| const hasVenvPython = await checkFileExists(venvPython); |
| const hasVenvStreamlit = await checkFileExists(venvStreamlit); |
| const reqPath = join(projectPath, 'requirements.txt'); |
| const hasReq = await checkFileExists(reqPath); |
| const hasLock = await hasInstallLock(projectPath, 'pip'); |
| |
| const shouldDetectStreamlit = isStreamlit || await hasStreamlit(projectPath); |
| const needsSetup = !hasVenv || !hasVenvPython || (shouldDetectStreamlit && !hasVenvStreamlit) || !hasLock; |
| |
| if (needsSetup) { |
| if (!hasVenv || !hasVenvPython) { |
| console.log(`[python] Creating virtual environment...`); |
| await execAsync('python3 -m venv venv', { cwd: projectPath }); |
| } |
| |
| if (hasReq) { |
| console.log(`[python] Installing requirements...`); |
| await execAsync('source venv/bin/activate && pip install --break-system-packages -r requirements.txt', { |
| cwd: projectPath, |
| shell: '/bin/bash' |
| }); |
| } else if (shouldDetectStreamlit && !hasVenvStreamlit) { |
| console.log(`[python] Installing streamlit...`); |
| await execAsync('source venv/bin/activate && pip install --break-system-packages streamlit flask fastapi', { |
| cwd: projectPath, |
| shell: '/bin/bash' |
| }); |
| } |
| |
| await setInstallLock(projectPath, 'pip'); |
| console.log(`[python] Python environment ready`); |
| } |
| }, |
| start: (projectPath, port, isStreamlit = false) => { |
| const shouldDetectStreamlit = isStreamlit; |
| let cmd; |
| |
| if (shouldDetectStreamlit) { |
| cmd = `source venv/bin/activate && streamlit run main.py --server.port ${port} --server.address 0.0.0.0 --server.headless true`; |
| } else { |
| cmd = `source venv/bin/activate && python main.py`; |
| } |
| |
| return { |
| command: 'bash', |
| args: ['-c', cmd], |
| cwd: projectPath, |
| env: { ...process.env, PORT: port } |
| }; |
| }, |
| detectPort |
| }, |
| go: { |
| files: ['go.mod'], |
| setup: async (projectPath, execAsync) => { |
| console.log(`[go] Downloading dependencies...`); |
| await execAsync('go mod download', { cwd: projectPath }); |
| }, |
| start: (projectPath, port) => ({ |
| command: 'go', |
| args: ['run', 'main.go'], |
| cwd: projectPath, |
| env: { ...process.env, PORT: port } |
| }), |
| detectPort |
| }, |
| rust: { |
| files: ['Cargo.toml'], |
| setup: async (projectPath, execAsync) => { |
| console.log(`[rust] Building dependencies...`); |
| await execAsync('cargo build', { cwd: projectPath }); |
| }, |
| start: (projectPath, port) => ({ |
| command: 'cargo', |
| args: ['run'], |
| cwd: projectPath, |
| env: { ...process.env, PORT: port } |
| }), |
| detectPort |
| }, |
| deno: { |
| files: ['deno.json', 'deno.jsonc'], |
| setup: async () => {}, |
| start: (projectPath, port) => ({ |
| command: 'deno', |
| args: ['run', '--allow-net', 'main.ts'], |
| cwd: projectPath, |
| env: { ...process.env, PORT: port } |
| }), |
| detectPort |
| }, |
| static: { |
| files: ['index.html'], |
| setup: async () => {}, |
| start: (projectPath, port) => ({ |
| command: 'npx', |
| args: ['serve', '-p', port, '-s', projectPath], |
| cwd: projectPath |
| }), |
| detectPort |
| } |
| }; |
|
|
| export async function detectLanguage(projectPath) { |
| const entries = await readdir(projectPath, { withFileTypes: true }); |
| const files = entries.map(e => e.name); |
| |
| for (const [lang, config] of Object.entries(LANGUAGE_CONFIGS)) { |
| for (const file of config.files) { |
| if (files.includes(file)) { |
| if (lang === 'python') { |
| const isStreamlit = await hasStreamlit(projectPath); |
| return { |
| language: lang, |
| config: config, |
| detectedFile: file, |
| isStreamlit |
| }; |
| } |
| return { |
| language: lang, |
| config: config, |
| detectedFile: file |
| }; |
| } |
| } |
| } |
| |
| const extMap = { |
| '.js': 'nodejs', |
| '.ts': 'nodejs', |
| '.py': 'python', |
| '.go': 'go', |
| '.rs': 'rust', |
| '.html': 'static', |
| '.htm': 'static' |
| }; |
| |
| for (const file of files) { |
| const ext = extname(file).toLowerCase(); |
| if (extMap[ext]) { |
| return { |
| language: extMap[ext], |
| config: LANGUAGE_CONFIGS[extMap[ext]], |
| detectedFile: file |
| }; |
| } |
| } |
| |
| return null; |
| } |
|
|
| export { checkFileExists, LANGUAGE_CONFIGS }; |
|
|