UNPKG

buddy-bot

Version:

Automated & optimized dependency updates for JavaScript & TypeScript projects. Like Renovate & Dependabot.

1,322 lines (1,132 loc) โ€ข 78.3 kB
#!/usr/bin/env bun /* eslint-disable no-cond-assign */ import type { BuddyBotConfig } from '../src/types' import fs from 'node:fs' import process from 'node:process' import { CAC } from 'cac' import prompts from 'prompts' import { version } from '../package.json' import { Buddy } from '../src/buddy' import { config } from '../src/config' import { analyzeProject, ConfigurationMigrator, confirmTokenSetup, createProgressTracker, detectRepository, displayProgress, displayValidationResults, generateConfigFile, generateCoreWorkflows, getWorkflowPreset, guideRepositorySettings, guideTokenCreation, PluginManager, runPreflightChecks, setupCustomWorkflow, showFinalInstructions, updateProgress, validateRepositoryAccess, validateWorkflowGeneration, } from '../src/setup' import { Logger } from '../src/utils/logger' const cli = new CAC('buddy-bot') cli.usage(`[command] [options] ๐Ÿค– Buddy Bot - Your companion dependency manager Supports npm, Bun, yarn, pnpm, Composer, pkgx, Launchpad, GitHub Actions, and Dockerfiles Automatically migrates from Renovate and Dependabot DEPENDENCY MANAGEMENT: setup ๐Ÿš€ Interactive setup for automated updates (recommended) scan ๐Ÿ” Scan for dependency updates dashboard ๐Ÿ“Š Create or update dependency dashboard issue update โฌ†๏ธ Update dependencies and create PRs rebase ๐Ÿ”„ Rebase/retry a pull request with latest updates update-check ๐Ÿ” Auto-detect and rebase PRs with checked rebase box check ๐Ÿ“‹ Check specific packages for updates schedule โฐ Run automated updates on schedule PACKAGE INFORMATION: info ๐Ÿ“ฆ Show detailed package information versions ๐Ÿ“ˆ Show all available versions of a package latest โญ Get the latest version of a package exists โœ… Check if a package exists in the registry deps ๐Ÿ”— Show package dependencies compare โš–๏ธ Compare two versions of a package search ๐Ÿ” Search for packages in the registry BRANCH MANAGEMENT: cleanup ๐Ÿงน Clean up stale buddy-bot branches list-branches ๐Ÿ“‹ List all buddy-bot branches and their status CONFIGURATION & SETUP: open-settings ๐Ÿ”ง Open GitHub repository and organization settings pages Examples: buddy-bot setup # Interactive setup with migration buddy-bot setup --non-interactive # Automated setup for CI/CD buddy-bot scan --verbose # Scan for updates (npm + Composer + Dockerfiles) buddy-bot rebase 17 # Rebase PR #17 buddy-bot update-check # Auto-rebase checked PRs buddy-bot cleanup # Clean up stale branches buddy-bot list-branches # List all buddy-bot branches buddy-bot info laravel/framework # Get Composer package info buddy-bot info react # Get npm package info buddy-bot versions react --latest 5 # Show recent versions buddy-bot search "test framework" # Search packages buddy-bot open-settings # Open GitHub settings Migration: - Automatically detects Renovate and Dependabot configurations - Converts settings to Buddy Bot format with compatibility report - Generates optimized GitHub Actions workflows - Provides migration guidance and best practices`) // Define CLI options interface to match our core types interface CLIOptions { verbose?: boolean config?: string packages?: string pattern?: string strategy?: 'major' | 'minor' | 'patch' | 'all' ignore?: string dryRun?: boolean respectLatest?: boolean } cli .command('setup', '๐Ÿš€ Interactive setup for automated dependency updates (recommended)') .option('--verbose, -v', 'Enable verbose logging') .option('--non-interactive', 'Run setup without prompts (use defaults)') .option('--preset <type>', 'Workflow preset: standard|high-frequency|security|minimal|testing', { default: 'standard' }) .option('--token-setup <type>', 'Token setup: existing-secret|new-pat|default-token', { default: 'default-token' }) .example('buddy-bot setup') .example('buddy-bot setup --verbose') .example('buddy-bot setup --non-interactive') .example('buddy-bot setup --non-interactive --preset testing --verbose') .action(async (options: CLIOptions & { nonInteractive?: boolean, preset?: string, tokenSetup?: string }) => { const logger = options.verbose ? Logger.verbose() : Logger.quiet() try { console.log('๐Ÿค– Welcome to Buddy Bot Setup!') console.log('Let\'s configure automated dependency updates for your project.\n') // Initialize progress tracking const progress = createProgressTracker(10) // Updated total steps including new features displayProgress(progress) // Configuration Migration Detection updateProgress(progress, 'Detecting existing configurations') displayProgress(progress) const migrator = new ConfigurationMigrator() const existingTools = await migrator.detectExistingTools() const migrationResults: any[] = [] if (existingTools.length > 0 && !options.nonInteractive) { console.log(`\n๐Ÿ” Configuration Migration Detection:`) console.log(`Found ${existingTools.length} existing dependency management tool(s):`) existingTools.forEach(tool => console.log(` โ€ข ${tool.name} (${tool.configFile})`)) const migrateResponse = await prompts({ type: 'confirm', name: 'migrate', message: 'Would you like to migrate existing configurations to Buddy Bot?', initial: true, }) if (migrateResponse.migrate) { console.log('\n๐Ÿ“‹ Migrating configurations...') for (const tool of existingTools) { try { let result if (tool.name === 'renovate') { result = await migrator.migrateFromRenovate(tool.configFile) } else if (tool.name === 'dependabot') { result = await migrator.migrateFromDependabot(tool.configFile) } else { continue } migrationResults.push(result) console.log(`โœ… Migrated ${tool.name} configuration`) } catch (error) { console.log(`โš ๏ธ Failed to migrate ${tool.name}: ${error instanceof Error ? error.message : 'Unknown error'}`) } } if (migrationResults.length > 0) { const report = await migrator.generateMigrationReport(migrationResults) console.log(`\n${report}`) } } } else if (existingTools.length > 0 && options.nonInteractive) { console.log(`\n๐Ÿ” Found ${existingTools.length} existing tool(s), skipping migration in non-interactive mode`) } // Plugin Discovery updateProgress(progress, 'Discovering integrations', true) displayProgress(progress) const pluginManager = new PluginManager() const availablePlugins = await pluginManager.discoverPlugins() if (availablePlugins.length > 0 && !options.nonInteractive) { console.log(`\n๐Ÿ”Œ Integration Discovery:`) console.log(`Found ${availablePlugins.length} available integration(s):`) availablePlugins.forEach(plugin => console.log(` โ€ข ${plugin.name} v${plugin.version}`)) const pluginResponse = await prompts({ type: 'confirm', name: 'enablePlugins', message: 'Would you like to enable these integrations?', initial: true, }) if (pluginResponse.enablePlugins) { for (const plugin of availablePlugins) { await pluginManager.loadPlugin(plugin) } } } else if (availablePlugins.length > 0 && options.nonInteractive) { console.log(`\n๐Ÿ”Œ Found ${availablePlugins.length} integration(s), skipping in non-interactive mode`) } // Pre-flight checks updateProgress(progress, 'Running pre-flight checks', true) displayProgress(progress) const preflightResults = await runPreflightChecks() displayValidationResults(preflightResults, '๐Ÿ” Pre-flight Validation') if (!preflightResults.success) { console.log('\nโŒ Pre-flight checks failed. Please fix the errors above and try again.') process.exit(1) } // Project analysis updateProgress(progress, 'Analyzing project', true) displayProgress(progress) const projectAnalysis = await analyzeProject() console.log(`\n๐Ÿ” Project Analysis:`) console.log(`๐Ÿ“ฆ Project Type: ${projectAnalysis.type}`) console.log(`โš™๏ธ Package Manager: ${projectAnalysis.packageManager}`) console.log(`๐Ÿ”’ Lock File: ${projectAnalysis.hasLockFile ? 'Found' : 'Not found'}`) console.log(`๐Ÿ“„ Dependency Files: ${projectAnalysis.hasDependencyFiles ? 'Found' : 'None'}`) console.log(`๐Ÿ”„ GitHub Actions: ${projectAnalysis.hasGitHubActions ? 'Found' : 'None'}`) console.log(`๐Ÿ’ก Recommended Preset: ${projectAnalysis.recommendedPreset}`) if (projectAnalysis.recommendations.length > 0) { console.log('\n๐Ÿ“‹ Recommendations:') projectAnalysis.recommendations.forEach((rec: string) => console.log(` โ€ข ${rec}`)) } // Step 1: Repository Detection updateProgress(progress, 'Repository Detection', true) displayProgress(progress) console.log('\n๐Ÿ“ Repository Detection') const repoInfo = await detectRepository() if (!repoInfo) { console.log('โŒ Could not detect repository. Please ensure you\'re in a Git repository.') process.exit(1) } console.log(`โœ… Detected repository: ${repoInfo.owner}/${repoInfo.name}`) console.log(`๐Ÿ”— GitHub URL: https://github.com/${repoInfo.owner}/${repoInfo.name}`) // Validate repository access const repoValidation = await validateRepositoryAccess(repoInfo) if (repoValidation.warnings.length > 0 || repoValidation.suggestions.length > 0) { displayValidationResults(repoValidation, '๐Ÿ” Repository Validation') } // Step 2: Token Setup Guide updateProgress(progress, 'GitHub Token Setup', true) displayProgress(progress) console.log('\n๐Ÿ”‘ GitHub Token Setup') console.log('For full functionality, Buddy Bot needs appropriate GitHub permissions.') console.log('This enables workflow file updates and advanced GitHub Actions features.\n') let tokenSetup if (options.nonInteractive) { // Use default token setup based on flag switch (options.tokenSetup) { case 'existing-secret': tokenSetup = { hasCustomToken: true, needsGuide: false } console.log('โœ… Using existing organization/repository secrets') break case 'new-pat': tokenSetup = { hasCustomToken: true, needsGuide: true } console.log('โš ๏ธ Non-interactive mode: Will use custom token but skip setup guide') break case 'default-token': default: tokenSetup = { hasCustomToken: false, needsGuide: false } console.log('โœ… Using default GITHUB_TOKEN (limited functionality)') break } } else { tokenSetup = await confirmTokenSetup() if (tokenSetup.needsGuide) { await guideTokenCreation(repoInfo) } } // Step 3: Repository Settings updateProgress(progress, 'Repository Settings', true) displayProgress(progress) console.log('\n๐Ÿ”ง Repository Settings') await guideRepositorySettings(repoInfo) // Step 4: Workflow Configuration updateProgress(progress, 'Workflow Configuration', true) displayProgress(progress) console.log('\nโš™๏ธ Workflow Configuration') let workflowResponse if (options.nonInteractive) { workflowResponse = { useCase: options.preset } console.log(`โœ… Using ${options.preset} preset for workflow configuration`) } else { workflowResponse = await prompts([ { type: 'select', name: 'useCase', message: 'What type of update schedule would you like?', choices: [ { title: 'Standard Setup (Recommended)', description: 'Dashboard updates 3x/week, dependency updates on schedule', value: 'standard', }, { title: 'High Frequency', description: 'Check for updates multiple times per day', value: 'high-frequency', }, { title: 'Security Focused', description: 'Frequent patch updates with security-first approach', value: 'security', }, { title: 'Minimal Updates', description: 'Weekly checks, lower frequency', value: 'minimal', }, { title: 'Development/Testing', description: 'Manual triggers + frequent checks for testing', value: 'testing', }, { title: 'Custom Configuration', description: 'Create your own schedule', value: 'custom', }, ], }, ]) if (!workflowResponse.useCase) { console.log('Setup cancelled.') return } } // Step 5: Generate Configuration File updateProgress(progress, 'Configuration File', true) displayProgress(progress) console.log('\n๐Ÿ“ Configuration File') await generateConfigFile(repoInfo, tokenSetup.hasCustomToken) // Step 6: Generate Workflows updateProgress(progress, 'Workflow Generation', true) displayProgress(progress) console.log('\n๐Ÿ”„ Workflow Generation') const preset = getWorkflowPreset(workflowResponse.useCase) if (workflowResponse.useCase === 'custom' && !options.nonInteractive) { await setupCustomWorkflow(preset, logger) } else { console.log(`โœจ Setting up ${preset.name}...`) console.log(`๐Ÿ“‹ ${preset.description}`) } // Generate the core workflows based on the provided templates await generateCoreWorkflows(preset, repoInfo, tokenSetup.hasCustomToken, logger) // Step 7: Workflow Validation updateProgress(progress, 'Workflow Validation', true) displayProgress(progress) console.log('\n๐Ÿ” Validating Generated Workflows') // Validate each generated workflow const workflowFiles = [ { name: 'buddy-dashboard.yml', content: '' }, { name: 'buddy-check.yml', content: '' }, { name: 'buddy-update.yml', content: '' }, ] let validationPassed = true for (const workflowFile of workflowFiles) { try { const workflowPath = `.github/workflows/${workflowFile.name}` if (fs.existsSync(workflowPath)) { const content = fs.readFileSync(workflowPath, 'utf8') const validation = await validateWorkflowGeneration(content) if (!validation.success) { console.log(`โŒ ${workflowFile.name} validation failed`) displayValidationResults(validation, `${workflowFile.name} Issues`) validationPassed = false } else { console.log(`โœ… ${workflowFile.name} validated successfully`) } } } catch { console.log(`โš ๏ธ Could not validate ${workflowFile.name}`) } } if (!validationPassed) { console.log('\nโš ๏ธ Some workflows have validation issues. Please review the warnings above.') } // Step 8: Final Setup Instructions & Plugin Execution updateProgress(progress, 'Setup Complete', true) displayProgress(progress) console.log('\n๐ŸŽ‰ Setup Complete!') await showFinalInstructions(repoInfo, tokenSetup.hasCustomToken) // Execute plugin hooks for setup completion if (availablePlugins.length > 0) { console.log('\n๐Ÿ”Œ Executing integration hooks...') const setupContext = { step: 'setup_complete', progress, config: migrationResults.length > 0 ? migrationResults[0].migratedSettings : {}, repository: repoInfo, analysis: projectAnalysis, plugins: availablePlugins, } pluginManager.setContext(setupContext) await pluginManager.executePluginHooks({ event: 'setup_complete' }) } } catch (error) { logger.error('Setup failed:', error) process.exit(1) } }) cli .command('scan', 'Scan for dependency updates') .option('--verbose, -v', 'Enable verbose logging') .option('--packages <names>', 'Comma-separated list of packages to check') .option('--pattern <pattern>', 'Glob pattern to match packages') .option('--strategy <type>', 'Update strategy: major|minor|patch|all', { default: 'all' }) .option('--ignore <names>', 'Comma-separated list of packages to ignore') .option('--respect-latest', 'Respect "latest", "*", and other dynamic version indicators (default: true)') .option('--no-respect-latest', 'Allow updating "latest", "*", and other dynamic version indicators') .example('buddy-bot scan') .example('buddy-bot scan --verbose') .example('buddy-bot scan --packages "react,typescript,laravel/framework"') .example('buddy-bot scan --pattern "@types/*"') .example('buddy-bot scan --strategy minor') .example('buddy-bot scan --no-respect-latest') .action(async (options: CLIOptions) => { const logger = options.verbose ? Logger.verbose() : Logger.quiet() try { logger.info('Loading configuration...') // Parse packages from string if provided let packages: string[] | undefined if (options.packages) { packages = options.packages.split(',').map(p => p.trim()) } // Parse ignore list from string if provided let ignore: string[] | undefined if (options.ignore) { ignore = options.ignore.split(',').map(p => p.trim()) } // Override config with CLI options const finalConfig: BuddyBotConfig = { ...config, verbose: options.verbose ?? config.verbose, packages: { ...config.packages, strategy: options.strategy ?? config.packages?.strategy ?? 'all', ignore: ignore ?? config.packages?.ignore, respectLatest: options.respectLatest ?? config.packages?.respectLatest ?? true, }, } const buddy = new Buddy(finalConfig) if (packages?.length) { logger.info(`Checking specific packages: ${packages.join(', ')}`) const updates = await buddy.checkPackages(packages) if (updates.length === 0) { logger.success('All specified packages are up to date!') } else { logger.info(`Found ${updates.length} updates:`) for (const update of updates) { logger.info(` ${update.name}: ${update.currentVersion} โ†’ ${update.newVersion} (${update.updateType})`) } } return } if (options.pattern) { logger.info(`Checking packages with pattern: ${options.pattern}`) const updates = await buddy.checkPackagesWithPattern(options.pattern) if (updates.length === 0) { logger.success('All matching packages are up to date!') } else { logger.info(`Found ${updates.length} updates:`) for (const update of updates) { logger.info(` ${update.name}: ${update.currentVersion} โ†’ ${update.newVersion} (${update.updateType})`) } } return } // Full project scan const scanResult = await buddy.scanForUpdates() if (scanResult.updates.length === 0) { logger.success('All dependencies are up to date!') return } logger.info(`\nScan Results:`) logger.info(`๐Ÿ“ฆ Total packages: ${scanResult.totalPackages}`) logger.info(`๐Ÿ”„ Available updates: ${scanResult.updates.length}`) logger.info(`โฑ๏ธ Scan duration: ${scanResult.duration}ms`) // Group updates by type const majorUpdates = scanResult.updates.filter(u => u.updateType === 'major') const minorUpdates = scanResult.updates.filter(u => u.updateType === 'minor') const patchUpdates = scanResult.updates.filter(u => u.updateType === 'patch') if (majorUpdates.length > 0) { logger.warn(`\n๐Ÿšจ Major updates (${majorUpdates.length}):`) for (const update of majorUpdates) { logger.info(` ${update.name}: ${update.currentVersion} โ†’ ${update.newVersion}`) } } if (minorUpdates.length > 0) { logger.info(`\nโœจ Minor updates (${minorUpdates.length}):`) for (const update of minorUpdates) { logger.info(` ${update.name}: ${update.currentVersion} โ†’ ${update.newVersion}`) } } if (patchUpdates.length > 0) { logger.info(`\n๐Ÿ”ง Patch updates (${patchUpdates.length}):`) for (const update of patchUpdates) { logger.info(` ${update.name}: ${update.currentVersion} โ†’ ${update.newVersion}`) } } if (scanResult.groups.length > 0) { logger.info(`\n๐Ÿ“‹ Update groups (${scanResult.groups.length}):`) for (const group of scanResult.groups) { logger.info(` ${group.name}: ${group.updates.length} updates`) } } } catch (error) { logger.error('Scan failed:', error) process.exit(1) } }) cli .command('dashboard', 'Create or update dependency dashboard issue') .option('--verbose, -v', 'Enable verbose logging') .option('--title <title>', 'Custom dashboard title') .option('--issue-number <number>', 'Update specific issue number') .example('buddy-bot dashboard') .example('buddy-bot dashboard --title "My Dependencies"') .example('buddy-bot dashboard --issue-number 42') .action(async (options: CLIOptions & { pin?: boolean, title?: string, issueNumber?: string }) => { const logger = options.verbose ? Logger.verbose() : Logger.quiet() try { logger.info('Creating or updating dependency dashboard...') // Check if repository is configured if (!config.repository) { logger.error('โŒ Repository configuration required for dashboard') logger.info('Configure repository.provider, repository.owner, repository.name in buddy-bot.config.ts') process.exit(1) } // Override config with CLI options const finalConfig: BuddyBotConfig = { ...config, verbose: options.verbose ?? config.verbose, dashboard: { ...config.dashboard, enabled: true, title: options.title ?? config.dashboard?.title, issueNumber: options.issueNumber ? Number.parseInt(options.issueNumber) : config.dashboard?.issueNumber, }, } const buddy = new Buddy(finalConfig) const issue = await buddy.createOrUpdateDashboard() logger.success(`โœ… Dashboard updated: ${issue.url}`) logger.info(`๐Ÿ“Š Issue #${issue.number}: ${issue.title}`) } catch (error) { logger.error('Dashboard creation failed:', error) process.exit(1) } }) cli .command('update', 'Update dependencies and create PRs') .option('--verbose, -v', 'Enable verbose logging') .option('--strategy <type>', 'Update strategy: major|minor|patch|all', { default: 'all' }) .option('--ignore <names>', 'Comma-separated list of packages to ignore') .option('--dry-run', 'Preview changes without making them') .option('--respect-latest', 'Respect "latest", "*", and other dynamic version indicators (default: true)') .option('--no-respect-latest', 'Allow updating "latest", "*", and other dynamic version indicators') .example('buddy-bot update') .example('buddy-bot update --dry-run') .example('buddy-bot update --strategy patch') .example('buddy-bot update --verbose') .example('buddy-bot update --no-respect-latest') .action(async (options: CLIOptions) => { const logger = options.verbose ? Logger.verbose() : Logger.quiet() try { logger.info('Starting dependency update process...') // Parse ignore list from string if provided let ignore: string[] | undefined if (options.ignore) { ignore = options.ignore.split(',').map(p => p.trim()) } const finalConfig: BuddyBotConfig = { ...config, verbose: options.verbose ?? config.verbose, packages: { ...config.packages, strategy: options.strategy ?? config.packages?.strategy ?? 'all', ignore: ignore ?? config.packages?.ignore, respectLatest: options.respectLatest ?? config.packages?.respectLatest ?? true, }, } const buddy = new Buddy(finalConfig) const scanResult = await buddy.scanForUpdates() if (scanResult.updates.length === 0) { logger.success('All dependencies are up to date!') return } if (options.dryRun) { logger.info('๐Ÿ” Dry run mode - no changes will be made') logger.info(`Would create ${scanResult.groups.length} pull request(s):`) for (const group of scanResult.groups) { logger.info(` ๐Ÿ“ ${group.title} (${group.updates.length} updates)`) } return } // Create pull requests await buddy.createPullRequests(scanResult) logger.success('Update process completed!') } catch (error) { logger.error('Update failed:', error) process.exit(1) } }) cli .command('rebase <pr-number>', 'Rebase/retry a pull request by recreating it with latest updates') .option('--verbose, -v', 'Enable verbose logging') .option('--force', 'Force rebase even if PR appears to be up to date') .example('buddy-bot rebase 17') .example('buddy-bot rebase 17 --verbose') .example('buddy-bot rebase 17 --force') .action(async (prNumber: string, options: CLIOptions & { force?: boolean }) => { const logger = options.verbose ? Logger.verbose() : Logger.quiet() try { logger.info(`๐Ÿ”„ Rebasing/retrying PR #${prNumber}...`) // Check if repository is configured if (!config.repository) { logger.error('โŒ Repository configuration required for PR operations') logger.info('Configure repository.provider, repository.owner, repository.name in buddy-bot.config.ts') process.exit(1) } // Get GitHub token from environment (prefer BUDDY_BOT_TOKEN for full permissions) const token = process.env.BUDDY_BOT_TOKEN || process.env.GITHUB_TOKEN if (!token) { logger.error('โŒ GITHUB_TOKEN or BUDDY_BOT_TOKEN environment variable required for PR operations') process.exit(1) } const { GitHubProvider } = await import('../src/git/github-provider') const hasWorkflowPermissions = !!process.env.BUDDY_BOT_TOKEN const gitProvider = new GitHubProvider( token, config.repository.owner, config.repository.name, hasWorkflowPermissions, ) const prNum = Number.parseInt(prNumber) if (Number.isNaN(prNum)) { logger.error('โŒ Invalid PR number provided') process.exit(1) } // Get the PR to rebase const prs = await gitProvider.getPullRequests('open') const pr = prs.find(p => p.number === prNum) if (!pr) { logger.error(`โŒ Could not find open PR #${prNum}`) process.exit(1) } if (!pr.head.startsWith('buddy-bot/')) { logger.error(`โŒ PR #${prNum} is not a buddy-bot PR (branch: ${pr.head})`) process.exit(1) } logger.info(`๐Ÿ“‹ Found PR: ${pr.title}`) logger.info(`๐ŸŒฟ Branch: ${pr.head}`) // Extract package updates from PR body to determine what to update const packageUpdates = await extractPackageUpdatesFromPRBody(pr.body) if (packageUpdates.length === 0) { logger.error('โŒ Could not extract package updates from PR body') process.exit(1) } logger.info(`๐Ÿ“ฆ Found ${packageUpdates.length} packages to update`) // Check if we need to rebase by scanning for current updates if (!options.force) { logger.info('๐Ÿ” Checking if rebase is needed...') const buddy = new Buddy(config) const scanResult = await buddy.scanForUpdates() // Check if current scan matches PR content const currentUpdates = scanResult.updates.filter(u => packageUpdates.some(pu => pu.name === u.name), ) const upToDate = packageUpdates.every(pu => currentUpdates.some(cu => cu.name === pu.name && cu.newVersion === pu.newVersion, ), ) if (upToDate) { logger.success('โœ… PR is already up to date, no rebase needed') logger.info('๐Ÿ’ก Use --force to rebase anyway') return } } // Update the existing PR with latest updates (true rebase) logger.info('๐Ÿ”„ Updating PR with latest updates...') // Get latest updates const buddy = new Buddy({ ...config, verbose: options.verbose ?? config.verbose, }) const scanResult = await buddy.scanForUpdates() if (scanResult.updates.length === 0) { logger.success('โœ… All dependencies are now up to date!') return } // Find the matching update group - must match exactly const group = scanResult.groups.find(g => g.updates.length === packageUpdates.length && g.updates.every(u => packageUpdates.some(pu => pu.name === u.name)) && packageUpdates.every(pu => g.updates.some(u => u.name === pu.name)), ) if (!group) { logger.error('โŒ Could not find matching update group. This likely means the package grouping has changed.') logger.info(`๐Ÿ“‹ PR packages: ${packageUpdates.map(p => p.name).join(', ')}`) logger.info(`๐Ÿ“‹ Available groups: ${scanResult.groups.map(g => `${g.name} (${g.updates.length} packages)`).join(', ')}`) logger.info(`๐Ÿ’ก Close this PR manually and let buddy-bot create new ones with correct grouping`) return } // Generate new file changes (package.json, dependency files, GitHub Actions) const packageJsonUpdates = await buddy.generateAllFileUpdates(group.updates) // Update the branch with new commits await gitProvider.commitChanges(pr.head, group.title, packageJsonUpdates) logger.info(`โœ… Updated branch ${pr.head} with latest changes`) // Generate new PR content const { PullRequestGenerator } = await import('../src/pr/pr-generator') const prGenerator = new PullRequestGenerator({ verbose: options.verbose }) const newBody = await prGenerator.generateBody(group) // Update the PR with new title/body (and uncheck the rebase box) const updatedBody = newBody.replace( /- \[x\] <!-- rebase-check -->/g, '- [ ] <!-- rebase-check -->', ) await gitProvider.updatePullRequest(prNum, { title: group.title, body: updatedBody, }) logger.success('๐Ÿ”„ PR rebase completed! Updated existing PR in place.') } catch (error) { logger.error('Rebase failed:', error) process.exit(1) } }) // Helper function to extract package updates from PR body async function extractPackageUpdatesFromPRBody(body: string): Promise<Array<{ name: string, currentVersion: string, newVersion: string }>> { const updates: Array<{ name: string, currentVersion: string, newVersion: string }> = [] // Match table rows with package updates - handles both npm and Composer formats // npm format: | [package] | [`version` -> `version`] | // Composer format: | [package](link) | `version` -> `version` | file | status | const tableRowRegex = /\|\s*\[([^\]]+)\][^|]*\|\s*\[?`\^?([^`]+)`\s*->\s*`\^?([^`]+)`\]?/g let match while ((match = tableRowRegex.exec(body)) !== null) { const [, packageName, currentVersion, newVersion] = match updates.push({ name: packageName, currentVersion, newVersion, }) } return updates } cli .command('update-check', 'Check all open buddy-bot PRs for rebase checkbox and auto-rebase if checked') .option('--verbose, -v', 'Enable verbose logging') .option('--dry-run', 'Check but don\'t actually rebase') .example('buddy-bot update-check') .example('buddy-bot update-check --verbose') .example('buddy-bot update-check --dry-run') .action(async (options: CLIOptions) => { const logger = options.verbose ? Logger.verbose() : Logger.quiet() try { // Check if repository is configured if (!config.repository) { logger.error('โŒ Repository configuration required for PR operations') logger.info('Configure repository.provider, repository.owner, repository.name in buddy-bot.config.ts') process.exit(1) } // Get GitHub token from environment (prefer BUDDY_BOT_TOKEN for full permissions) const token = process.env.BUDDY_BOT_TOKEN || process.env.GITHUB_TOKEN if (!token) { logger.error('โŒ GITHUB_TOKEN or BUDDY_BOT_TOKEN environment variable required for PR operations') process.exit(1) } const { GitHubProvider } = await import('../src/git/github-provider') const hasWorkflowPermissions = !!process.env.BUDDY_BOT_TOKEN const gitProvider = new GitHubProvider( token, config.repository.owner, config.repository.name, hasWorkflowPermissions, ) // Step 1: Check for rebase checkboxes using HTTP (no API auth needed) logger.info('๐Ÿ” Checking for PRs with rebase checkbox enabled...') let rebasedCount = 0 let checkedPRs = 0 try { // Get PR numbers from git refs const prRefsOutput = await gitProvider.runCommand('git', ['ls-remote', 'origin', 'refs/pull/*/head']) const prNumbers: number[] = [] for (const line of prRefsOutput.split('\n')) { if (line.trim()) { const parts = line.trim().split('\t') if (parts.length === 2) { const ref = parts[1] // refs/pull/123/head const prMatch = ref.match(/refs\/pull\/(\d+)\/head/) if (prMatch) { prNumbers.push(Number.parseInt(prMatch[1])) } } } } logger.info(`๐Ÿ“‹ Found ${prNumbers.length} PRs to check for rebase requests`) // Check each PR for rebase checkbox (in small batches) const batchSize = 3 // Smaller batches for PR content fetching for (let i = 0; i < prNumbers.length && i < 20; i += batchSize) { // Limit to first 20 PRs const batch = prNumbers.slice(i, i + batchSize) for (const prNumber of batch) { try { // Fetch PR page HTML to check for rebase checkbox const url = `https://github.com/${config.repository.owner}/${config.repository.name}/pull/${prNumber}` const response = await fetch(url, { headers: { 'User-Agent': 'buddy-bot/1.0' }, }) if (response.ok) { const html = await response.text() checkedPRs++ // Check if PR is open and has rebase checkbox checked const isOpen = html.includes('State--open') && !html.includes('State--closed') && !html.includes('State--merged') const hasRebaseChecked = checkRebaseCheckbox(html) if (isOpen && hasRebaseChecked) { logger.info(`๐Ÿ”„ PR #${prNumber} has rebase checkbox checked`) if (options.dryRun) { logger.info(`๐Ÿ” [DRY RUN] Would rebase PR #${prNumber}`) rebasedCount++ } else { logger.info(`๐Ÿ”„ Rebasing PR #${prNumber} via rebase command...`) try { // Use the existing rebase command logic const { spawn } = await import('node:child_process') const rebaseProcess = spawn('bunx', ['buddy-bot', 'rebase', prNumber.toString()], { stdio: 'inherit', cwd: process.cwd(), }) await new Promise((resolve, reject) => { rebaseProcess.on('close', (code) => { if (code === 0) { rebasedCount++ resolve(code) } else { reject(new Error(`Rebase failed with code ${code}`)) } }) }) logger.success(`โœ… Successfully rebased PR #${prNumber}`) } catch (rebaseError) { logger.error(`โŒ Failed to rebase PR #${prNumber}:`, rebaseError) } } } } // Small delay between requests await new Promise(resolve => setTimeout(resolve, 200)) } catch (error) { logger.warn(`โš ๏ธ Could not check PR #${prNumber}:`, error) } } // Delay between batches if (i + batchSize < Math.min(prNumbers.length, 20)) { await new Promise(resolve => setTimeout(resolve, 1000)) } } if (rebasedCount > 0) { logger.success(`โœ… ${options.dryRun ? 'Would rebase' : 'Successfully rebased'} ${rebasedCount} PR(s)`) } else if (checkedPRs > 0) { logger.info('๐Ÿ“‹ No PRs have rebase checkbox enabled') } } catch (error) { logger.warn('โš ๏ธ Could not check for rebase requests:', error) } // Step 2: Check for obsolete PRs (composer files removed, etc.) logger.info('\n๐Ÿ” Checking for obsolete PRs due to removed dependency files...') try { const { Buddy } = await import('../src/buddy') const buddy = new Buddy(config) await buddy.checkAndCloseObsoletePRs(gitProvider, !!options.dryRun) } catch (error) { logger.warn('โš ๏ธ Could not check for obsolete PRs:', error) } // Step 3: Run branch cleanup (uses local git commands, no API calls) logger.info('\n๐Ÿงน Running branch cleanup...') const result = await gitProvider.cleanupStaleBranches(2, !!options.dryRun) if (options.dryRun) { logger.info(`๐Ÿ” [DRY RUN] Would delete ${result.deleted.length} stale branches`) } else { logger.success(`๐ŸŽ‰ Cleanup complete: ${result.deleted.length} branches deleted, ${result.failed.length} failed`) } // Summary if (rebasedCount > 0 || result.deleted.length > 0) { logger.success(`\n๐ŸŽ‰ Update-check complete: ${rebasedCount} PR(s) rebased, ${result.deleted.length} branches cleaned`) } } catch (error) { logger.error('update-check failed:', error) process.exit(1) } }) // Helper function to check if rebase checkbox is checked function checkRebaseCheckbox(body: string): boolean { // Look for the checked rebase checkbox pattern - handle both "rebase/retry" and "update/retry" const checkedPattern = /- \[x\] <!-- rebase-check -->If you want to (?:rebase|update)\/retry this PR, check this box/i return checkedPattern.test(body) } cli .command('check <packages...>', 'Check specific packages for updates') .option('--verbose, -v', 'Enable verbose logging') .option('--strategy <type>', 'Update strategy: major|minor|patch|all', { default: 'all' }) .example('buddy-bot check react typescript') .example('buddy-bot check react --verbose') .action(async (...args: any[]) => { // CAC passes individual arguments, then options as the last parameter const options: CLIOptions = args[args.length - 1] const packages: string[] = args.slice(0, -1) const checkLogger = options.verbose ? Logger.verbose() : Logger.quiet() if (!packages.length) { checkLogger.error('No packages specified to check') process.exit(1) } try { checkLogger.info(`Checking specific packages: ${packages.join(', ')}`) const finalConfig: BuddyBotConfig = { ...config, verbose: options.verbose ?? config.verbose, packages: { ...config.packages, strategy: options.strategy ?? config.packages?.strategy ?? 'all', }, } const buddy = new Buddy(finalConfig) const updates = await buddy.checkPackages(packages) if (updates.length === 0) { checkLogger.success('All specified packages are up to date!') } else { checkLogger.info(`Found ${updates.length} updates:`) for (const update of updates) { checkLogger.info(` ${update.name}: ${update.currentVersion} โ†’ ${update.newVersion} (${update.updateType})`) } } } catch (error) { checkLogger.error('Check failed:', error) process.exit(1) } }) cli .command('schedule', 'Run automated dependency updates on schedule') .option('--verbose, -v', 'Enable verbose logging') .option('--strategy <type>', 'Update strategy: major|minor|patch|all', { default: 'all' }) .example('buddy-bot schedule') .example('buddy-bot schedule --verbose') .example('buddy-bot schedule --strategy patch') .action(async (options: CLIOptions) => { const { Scheduler } = await import('../src/scheduler/scheduler') const logger = options.verbose ? Logger.verbose() : Logger.quiet() try { logger.info('๐Ÿ•’ Starting Buddy Scheduler...') // Parse ignore list from string if provided let ignore: string[] | undefined if (options.ignore) { ignore = options.ignore.split(',').map(p => p.trim()) } const finalConfig: BuddyBotConfig = { ...config, verbose: options.verbose ?? config.verbose, packages: { ...config.packages, strategy: options.strategy ?? config.packages?.strategy ?? 'all', ignore: ignore ?? config.packages?.ignore, }, } // Validate that repository is configured for scheduling if (!finalConfig.repository?.provider || !finalConfig.repository?.owner || !finalConfig.repository?.name) { logger.error('โŒ Repository configuration required for scheduling. Please configure:') logger.info(' - repository.provider (github, gitlab, etc.)') logger.info(' - repository.owner') logger.info(' - repository.name') logger.info(' - repository.token (via environment variable)') process.exit(1) } const scheduler = new Scheduler(options.verbose) const job = Scheduler.createJobFromConfig(finalConfig, 'cli-schedule') // Override cron if provided if (options.strategy) { switch (options.strategy) { case 'major': job.schedule.cron = Scheduler.PRESETS.WEEKLY break case 'minor': job.schedule.cron = Scheduler.PRESETS.TWICE_WEEKLY break case 'patch': job.schedule.cron = Scheduler.PRESETS.DAILY break default: job.schedule.cron = Scheduler.PRESETS.WEEKLY } } scheduler.addJob(job) scheduler.start() logger.success(`โœ… Scheduler started with cron: ${job.schedule.cron}`) logger.info('๐Ÿ“… Next run:', job.nextRun?.toISOString() || 'Unknown') logger.info('๐Ÿ›‘ Press Ctrl+C to stop the scheduler') // Keep process alive process.stdin.resume() } catch (error) { logger.error('Scheduler failed:', error) process.exit(1) } }) cli .command('generate-workflows', 'Generate GitHub Actions workflow templates (deprecated - use "setup" instead)') .option('--verbose, -v', 'Enable verbose logging') .example('buddy-bot generate-workflows') .example('buddy-bot generate-workflows --verbose') .action(async (options: CLIOptions) => { const { GitHubActionsTemplate } = await import('../src/templates/github-actions') const { writeFileSync, mkdirSync } = await import('node:fs') const { resolve } = await import('node:path') const logger = options.verbose ? Logger.verbose() : Logger.quiet() console.log('โš ๏ธ The "generate-workflows" command is deprecated.') console.log('๐Ÿ’ก Use "buddy-bot setup" for a better interactive experience.\n') try { const finalConfig: BuddyBotConfig = { ...config, verbose: options.verbose ?? config.verbose, } const outputDir = finalConfig.workflows?.outputDir ? resolve(process.cwd(), finalConfig.workflows.outputDir) : resolve(process.cwd(), '.github', 'workflows') logger.info('๐Ÿš€ Generating GitHub Actions workflow templates...') // Create output directory try { mkdirSync(outputDir, { recursive: true }) } catch { // Directory already exists } // Generate workflows based on configuration const templates = finalConfig.workflows?.templates || { comprehensive: true, daily: true, weekly: true, monthly: true, docker: true, monorepo: true, } let generatedCount = 0 // Generate standard scheduled workflows if (templates.daily || templates.weekly || templates.monthly) { const workflows = GitHubActionsTemplate.generateScheduledWorkflows(finalConfig) for (const [filename, content] of Object.entries(workflows)) { const shouldGenerate = ( (filename.includes('daily') && templates.daily) || (filename.includes('weekly') && templates.weekly) || (filename.includes('monthly') && templates.monthly) ) if (shouldGenerate) { const filepath = resolve(outputDir, filename) writeFileSync(filepath, content) logger.success(`Generated: ${filename}`) generatedCount++ } } } // Generate comprehensive workflow if (templates.comprehensive) { const comprehensiveWorkflow = GitHubActionsTemplate.generateComprehensiveWorkflow(finalConfig) writeFileSync(resolve(outputDir, 'buddy-comprehensive.yml'), comprehensiveWorkflow) logger.success('Generated: buddy-comprehensive.yml') generatedCount++ } // Generate specialized workflows if (templates.docker) { const dockerWorkflow = GitHubActionsTemplate.generateDockerWorkflow(finalConfig) writeFileSync(resolve(outputDir, 'buddy-docker.yml'), dockerWorkflow) logger.success('Generated: buddy-docker.yml') generatedCount++ } if (templates.monorepo) { const monorepoWorkflow = GitHubActionsTemplate.generateMonorepoWorkflow(finalConfig) writeFileSync(resolve(outputDir, 'buddy-monorepo.yml'), monorepoWorkflow) logger.success('Generated: buddy-monorepo.yml') generatedCount++ } // Generate custom workflows if (finalConfig.workflows?.custom?.length) { for (const customWorkflow of finalConfig.workflows.custom) { const workflowContent = GitHubActionsTemplate.generateCustomWorkflow(customWorkflow, finalConfig) const filename = `buddy-${customWorkflow.name.toLowerCase().replace(/\s+/g, '-')}.yml` writeFileSync(resolve(outputDir, filename), workflowContent) logger.success(`Generated: ${filename}`) generatedCount++ } } if (generatedCount === 0) { logger.warn('No workflows were generated. Check your configuration in buddy-bot.config.ts') logger.info('Set workflows.templates to enable specific templates or add custom workflows.') return } logger.success(`\n๐ŸŽ‰ ${generatedCount} GitHub Actions workflow(s) generated!`) logger.info(`๐Ÿ“ Location: ${outputDir}`) logger.info('\n๐Ÿ’ก Next steps:') logger.info(' 1. Review and customize the workflows for your project') logger.info(' 2. Ensure GITHUB_TOKEN is available as a secret') logger.info(' 3. Configure buddy-bot.config.ts with your repository settings') logger.info(' 4. Enable GitHub Actions in your repository settings') logger.info('\n๐Ÿ”— Learn more: https://docs.github.com/en/actions') } catch (error) { logger.error('Failed to generate workflows:', error) process.exit(1) } }) cli .command('info <package>', 'Show detailed package information') .option('--verbose, -v', 'Enable verbose logging') .option('--json', 'Output in JSON format') .example('buddy-bot info react') .example('buddy-bot info react --json') .example('buddy-bot info typescript@latest') .action(async (packageName: string, options: CLIOptions & { json?: boolean }) => { const { RegistryClient } = await import('../src/registry/registry-client') const logger = options.verbose ? Logger.verbose() : Logger.quiet() try { const registryClient = new RegistryClient(process.cwd(), logger, undefined) if (options.json) { // Output raw JSON from bun info const { spawn } = await import('node:child_process') const child = spawn('bun', ['info', packageName, '--json'], { stdio: 'inherit', }) child.on('close', (code) => { process.exit(code || 0) }) return } // Get package metadata and display in a nice format const metadata = await