UNPKG

lsh-framework

Version:

A powerful, extensible shell with advanced job management, database persistence, and modern CLI features

319 lines (318 loc) 12.6 kB
/** * Self-management commands for LSH * Provides utilities for updating and maintaining the CLI */ import { Command } from 'commander'; import * as https from 'https'; import { spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import chalk from 'chalk'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const selfCommand = new Command('self'); selfCommand.description('Manage and update the LSH application'); /** * Parse version string to tuple for comparison */ function parseVersion(version) { return version .replace(/^v/, '') .split('.') .map(x => parseInt(x) || 0); } /** * Compare two version strings * Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2 */ function compareVersions(v1, v2) { const parts1 = parseVersion(v1); const parts2 = parseVersion(v2); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const p1 = parts1[i] || 0; const p2 = parts2[i] || 0; if (p1 < p2) return -1; if (p1 > p2) return 1; } return 0; } /** * Get current version from package.json */ function getCurrentVersion() { try { const packageJsonPath = path.join(__dirname, '../../package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); return packageJson.version || 'unknown'; } catch { return 'unknown'; } } /** * Fetch latest version from npm registry */ async function fetchLatestVersion() { return new Promise((resolve, reject) => { const options = { hostname: 'registry.npmjs.org', port: 443, path: '/gwicho38-lsh', method: 'GET', headers: { 'User-Agent': 'lsh-cli', }, }; https.get(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { if (res.statusCode === 200) { const npmData = JSON.parse(data); const latestVersion = npmData['dist-tags']?.latest; if (latestVersion) { const publishedAt = npmData.time?.[latestVersion]; resolve({ version: latestVersion, publishedAt: publishedAt || undefined, }); } else { resolve(null); } } else { console.error(chalk.red(`✗ npm registry returned status ${res.statusCode}`)); resolve(null); } } catch (error) { reject(error); } }); }).on('error', (error) => { reject(error); }); }); } /** * Check GitHub Actions CI status for a specific version tag */ async function checkCIStatus(_version) { return new Promise((resolve) => { const options = { hostname: 'api.github.com', port: 443, path: `/repos/gwicho38/lsh/actions/runs?per_page=5`, method: 'GET', headers: { 'User-Agent': 'lsh-cli', 'Accept': 'application/vnd.github.v3+json', }, }; https.get(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { if (res.statusCode === 200) { const ghData = JSON.parse(data); const runs = ghData.workflow_runs || []; // Find the most recent workflow run for main branch const mainRuns = runs.filter((run) => run.head_branch === 'main' && run.status === 'completed'); if (mainRuns.length > 0) { const latestRun = mainRuns[0]; const passing = latestRun.conclusion === 'success'; resolve({ passing, url: latestRun.html_url, }); } else { // No completed runs found, assume passing resolve({ passing: true }); } } else { // If we can't check CI, don't block the update resolve({ passing: true }); } } catch (_error) { // On error, don't block the update resolve({ passing: true }); } }); }).on('error', () => { // On network error, don't block the update resolve({ passing: true }); }); }); } /** * Update command - check for and install updates from npm */ selfCommand .command('update') .description('Check for and install LSH updates from npm') .option('--check', 'Only check for updates, don\'t install') .option('-y, --yes', 'Skip confirmation prompt') .option('--skip-ci-check', 'Skip CI status check and install anyway') .action(async (options) => { try { const currentVersion = getCurrentVersion(); console.log(chalk.cyan('Current version:'), currentVersion); console.log(chalk.cyan('Checking npm for updates...')); // Fetch latest version from npm const latestInfo = await fetchLatestVersion(); if (!latestInfo) { console.log(chalk.red('✗ Failed to fetch version information from npm')); console.log(chalk.yellow('⚠ Make sure you have internet connectivity')); return; } const { version: latestVersion, publishedAt } = latestInfo; console.log(chalk.cyan('Latest version:'), latestVersion); if (publishedAt) { const date = new Date(publishedAt); console.log(chalk.dim(` Published: ${date.toLocaleDateString()}`)); } // Compare versions const comparison = compareVersions(currentVersion, latestVersion); if (comparison === 0) { console.log(chalk.green('✓ You\'re already on the latest version!')); return; } if (comparison > 0) { console.log(chalk.green(`✓ Your version (${currentVersion}) is newer than npm`)); console.log(chalk.dim(' You may be using a development version')); return; } // Update available console.log(chalk.yellow(`⬆ Update available: ${currentVersion}${latestVersion}`)); if (options.check) { console.log(chalk.cyan('ℹ Run \'lsh self update\' to install the update')); return; } // Ask for confirmation unless --yes flag is used if (!options.yes) { const readline = await import('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const answer = await new Promise((resolve) => { rl.question(chalk.yellow(`Install lsh ${latestVersion}? (y/N) `), (ans) => { rl.close(); resolve(ans); }); }); if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') { console.log(chalk.yellow('Update cancelled')); return; } } // Check CI status before installing (unless skipped) if (!options.skipCiCheck) { console.log(chalk.cyan('🔍 Checking CI status...')); const ciStatus = await checkCIStatus(latestVersion); if (!ciStatus.passing) { console.log(chalk.red('✗ CI build is failing for the latest version')); if (ciStatus.url) { console.log(chalk.yellow(` View CI status: ${ciStatus.url}`)); } console.log(chalk.yellow('⚠ Update blocked to prevent installing a broken version')); console.log(chalk.dim(' Use --skip-ci-check to install anyway (not recommended)')); return; } else { console.log(chalk.green('✓ CI build is passing')); } } // Install update console.log(chalk.cyan(`📦 Installing lsh ${latestVersion}...`)); const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const updateProcess = spawn(npmCmd, ['install', '-g', 'gwicho38-lsh@latest'], { stdio: 'inherit', }); updateProcess.on('close', (code) => { if (code === 0) { console.log(chalk.green(`✓ Successfully updated to lsh ${latestVersion}!`)); console.log(chalk.yellow('ℹ Restart your terminal or run \'hash -r\' to use the new version')); } else { console.log(chalk.red('✗ Update failed')); console.log(chalk.yellow('ℹ Try running with sudo: sudo npm install -g gwicho38-lsh@latest')); } }); } catch (error) { console.error(chalk.red('✗ Error during update:'), error); } }); /** * Version command - show detailed version information */ selfCommand .command('version') .description('Show detailed version information') .action(() => { const currentVersion = getCurrentVersion(); console.log(chalk.cyan('╔════════════════════════════════════╗')); console.log(chalk.cyan('║ LSH Version Info ║')); console.log(chalk.cyan('╚════════════════════════════════════╝')); console.log(); console.log(chalk.cyan('Version:'), currentVersion); console.log(chalk.cyan('Node:'), process.version); console.log(chalk.cyan('Platform:'), `${process.platform} (${process.arch})`); console.log(); console.log(chalk.dim('Run \'lsh self update --check\' to check for updates')); }); /** * Info command - show installation and configuration info */ selfCommand .command('info') .description('Show installation and configuration information') .action(() => { const currentVersion = getCurrentVersion(); console.log(chalk.cyan('╔════════════════════════════════════╗')); console.log(chalk.cyan('║ LSH Installation Info ║')); console.log(chalk.cyan('╚════════════════════════════════════╝')); console.log(); // Version info console.log(chalk.yellow('Version Information:')); console.log(' LSH Version:', currentVersion); console.log(' Node.js:', process.version); console.log(' Platform:', `${process.platform} (${process.arch})`); console.log(); // Installation paths console.log(chalk.yellow('Installation:')); console.log(' Executable:', process.execPath); console.log(' Working Dir:', process.cwd()); console.log(); // Environment console.log(chalk.yellow('Environment:')); console.log(' NODE_ENV:', process.env.NODE_ENV || 'not set'); console.log(' HOME:', process.env.HOME || 'not set'); console.log(' USER:', process.env.USER || 'not set'); console.log(); // Configuration const envFile = path.join(process.cwd(), '.env'); const envExists = fs.existsSync(envFile); console.log(chalk.yellow('Configuration:')); console.log(' .env file:', envExists ? chalk.green('Found') : chalk.red('Not found')); if (!envExists) { console.log(chalk.dim(' Copy .env.example to .env to configure')); } console.log(); console.log(chalk.dim('For more info, visit: https://github.com/gwicho38/lsh')); }); export default selfCommand;