UNPKG

cerebella

Version:
320 lines (269 loc) 10.1 kB
#!/usr/bin/env node import { spawn, execSync } from 'child_process'; import chalk from 'chalk'; import ora from 'ora'; import open from 'open'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { existsSync, mkdirSync, copyFileSync, readFileSync } from 'fs'; import { homedir } from 'os'; import process from 'process'; import https from 'https'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const MAIN_SERVER_PORT = 8421; const CEREBELLA_LOGO = [ ' ..... ...... ..... ...... ..... ...... .. .. .', ' .. .. .. .. .. .. .. .. .. .. .. ..', ' ++ ++.... ++.... ++.... ++... ++.... ++ ++ + .', ' ++ ++ ++ + ++ ++ + ++ ++ ++ ++++', '** ** ** ** * ** ** * ** ** ** ** *', '##### ###### ## # ###### ###### ###### ###### ###### ## #', ]; // Get current version from package.json function getCurrentVersion() { try { const packagePath = join(__dirname, 'package.json'); const packageData = JSON.parse(readFileSync(packagePath, 'utf8')); return packageData.version; } catch (e) { return null; } } // Fetch latest version from npm registry with timeout function fetchLatestVersion() { return new Promise((resolve) => { const timeout = setTimeout(() => { resolve(null); }, 3000); // 3 second timeout https.get('https://registry.npmjs.org/cerebella/latest', (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { clearTimeout(timeout); try { const info = JSON.parse(data); resolve(info.version); } catch (e) { resolve(null); } }); }).on('error', () => { clearTimeout(timeout); resolve(null); }); }); } // Compare semantic versions function isNewerVersion(current, latest) { if (!current || !latest) return false; const currentParts = current.split('.').map(Number); const latestParts = latest.split('.').map(Number); for (let i = 0; i < 3; i++) { if (latestParts[i] > currentParts[i]) return true; if (latestParts[i] < currentParts[i]) return false; } return false; } // Auto-update cerebella async function checkAndUpdate() { // Skip update check in CI environments if (process.env.CI || process.env.CEREBELLA_NO_UPDATE) { return false; } const currentVersion = getCurrentVersion(); if (!currentVersion) return false; const updateSpinner = ora('Checking for updates...').start(); try { const latestVersion = await fetchLatestVersion(); if (!latestVersion) { // Silently fail if we can't fetch the latest version updateSpinner.stop(); return false; } if (isNewerVersion(currentVersion, latestVersion)) { updateSpinner.text = `Updating cerebella from v${currentVersion} to v${latestVersion}...`; // Check if npm or yarn is available let packageManager = 'npm'; try { execSync('yarn --version', { stdio: 'ignore' }); // Check if we're in a yarn global directory const globalBin = execSync('yarn global bin', { encoding: 'utf8' }).trim(); const cerebellaPath = execSync('which cerebella', { encoding: 'utf8' }).trim(); if (cerebellaPath.startsWith(globalBin)) { packageManager = 'yarn'; } } catch (e) { // npm is the fallback } // Update using the appropriate package manager const updateCommand = packageManager === 'yarn' ? 'yarn global add cerebella@latest' : 'npm install -g cerebella@latest'; execSync(updateCommand, { stdio: 'pipe' }); updateSpinner.succeed(chalk.green(`Updated to cerebella v${latestVersion}!`)); console.log(chalk.yellow('\nPlease run cerebella again to use the new version.\n')); process.exit(0); } else { // Clear the spinner without any message when already up to date updateSpinner.stop(); return false; } } catch (error) { // Silently fail on update errors updateSpinner.stop(); if (process.env.DEBUG) { console.error(chalk.gray('Update check error:'), error.message); } return false; } } async function animateLogo() { console.clear(); console.log('\n'); const logoLines = new Array(CEREBELLA_LOGO.length).fill(''); const totalDuration = 4000; const totalChars = CEREBELLA_LOGO.join('').length; const charDelay = totalDuration / totalChars; let charIndex = 0; for (let lineIndex = 0; lineIndex < CEREBELLA_LOGO.length; lineIndex++) { for (let charPos = 0; charPos < CEREBELLA_LOGO[lineIndex].length; charPos++) { logoLines[lineIndex] += CEREBELLA_LOGO[lineIndex][charPos]; process.stdout.write('\x1b[H'); console.log('\n'); logoLines.forEach(line => { console.log(chalk.cyan(line)); }); for (let i = logoLines.length; i < CEREBELLA_LOGO.length; i++) { console.log(''); } charIndex++; await new Promise(resolve => setTimeout(resolve, charDelay)); } } await new Promise(resolve => setTimeout(resolve, 300)); } const args = process.argv.slice(2); const showHelp = args.includes('--help') || args.includes('-h'); const skipUpdate = args.includes('--skip-update') || args.includes('--no-update'); if (showHelp) { const currentVersion = getCurrentVersion(); console.log(` ${chalk.cyan.bold('Cerebella')} ${currentVersion ? chalk.gray(`v${currentVersion}`) : ''} - this is how coding with AI should feel ${chalk.yellow('Usage:')} cerebella [options] ${chalk.yellow('Options:')} --skip-update Skip automatic update check --help, -h Show this help message `); process.exit(0); } // Check for updates unless skipped if (!skipUpdate) { await checkAndUpdate(); } try { execSync('uv --version', { stdio: 'ignore' }); } catch (e) { console.log(chalk.yellow('Installing uv package manager...')); const installSpinner = ora('Installing uv...').start(); try { execSync('curl --version', { stdio: 'ignore' }); } catch (curlError) { installSpinner.fail(chalk.red('curl is not installed')); console.error(chalk.red('\nError: curl is required to install uv.')); console.error(chalk.yellow('\nPlease install curl first:')); console.error(chalk.gray(' macOS: brew install curl')); console.error(chalk.gray(' Ubuntu/Debian: sudo apt-get install curl')); console.error(chalk.gray(' CentOS/RHEL: sudo yum install curl')); console.error(chalk.yellow('\nThen run cerebella again.\n')); process.exit(1); } try { execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', { stdio: 'pipe', shell: true }); const uvPath = `${process.env.HOME}/.cargo/bin`; process.env.PATH = `${uvPath}:${process.env.PATH}`; try { execSync(`${uvPath}/uv --version`, { stdio: 'ignore' }); installSpinner.succeed(chalk.green('uv installed successfully!')); } catch (verifyError) { installSpinner.warn(chalk.yellow('uv installed but not found in PATH')); console.log(chalk.yellow('\nPlease restart your terminal or run:')); console.log(chalk.gray(' source ~/.bashrc (or ~/.zshrc on macOS)')); console.log(chalk.yellow('\nThen run cerebella again.\n')); process.exit(0); } } catch (installError) { installSpinner.fail(chalk.red('Failed to install uv')); console.error(chalk.red('\nError installing uv. Please install it manually:')); console.error(chalk.gray(' curl -LsSf https://astral.sh/uv/install.sh | sh\n')); process.exit(1); } } const cerebellaHome = join(homedir(), '.cerebella'); const venvPath = join(cerebellaHome, '.venv'); const pyprojectPath = join(cerebellaHome, 'pyproject.toml'); if (!existsSync(cerebellaHome)) { mkdirSync(cerebellaHome, { recursive: true }); } const sourcePyproject = join(__dirname, 'pyproject.toml'); if (!existsSync(pyprojectPath) || !existsSync(venvPath)) { try { copyFileSync(sourcePyproject, pyprojectPath); } catch (err) { console.error(chalk.red('Failed to set up ~/.cerebella directory')); console.error(err.message); process.exit(1); } } const isFirstRun = !existsSync(venvPath); if (isFirstRun) { console.log(chalk.yellow('First time setup: Creating Python environment in ~/.cerebella')); console.log(chalk.gray('This may take a minute while dependencies are installed.\n')); } else { console.log(chalk.cyan('Starting Cerebella...\n')); } // Use ~/.cerebella as the project directory, but run main.py from the npm installation const mainPyPath = join(__dirname, 'main.py'); const pythonArgs = ['run', '--project', cerebellaHome, 'python', mainPyPath]; const mainProcess = spawn('uv', pythonArgs, { stdio: 'inherit', shell: true, env: { ...process.env, CEREBELLA_INSTALL_DIR: __dirname } }); setTimeout(() => displayRunningMessage().catch(console.error), 100); async function displayRunningMessage() { await animateLogo(); console.log(chalk.cyan('\nCerebella is running!\n')); console.log(chalk.gray(` Server: http://localhost:${MAIN_SERVER_PORT}`)); console.log(chalk.yellow('\nPress Ctrl+C to stop\n')); const url = `http://localhost:${MAIN_SERVER_PORT}`; open(url).catch(err => { console.log(chalk.gray(`Could not auto-open browser: ${err.message}`)); console.log(chalk.gray(`Please manually open: ${url}`)); }); } const cleanup = (signal) => { if (mainProcess && !mainProcess.killed) { mainProcess.kill(signal); } setTimeout(() => process.exit(0), 1000); }; ['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(signal => { process.on(signal, () => cleanup(signal)); }); mainProcess.on('error', (err) => { console.error(chalk.red('Failed to start main server:'), err.message); process.exit(1); }); mainProcess.on('exit', (code, signal) => { if (code !== null && code !== 0) { cleanup('SIGTERM'); } });