UNPKG

gh-legacy

Version:

A powerful GitHub CLI tool for automatic repository ownership transfer to trusted beneficiaries after periods of inactivity

198 lines (180 loc) • 7.77 kB
// commands/triggerAccess.js const fs = require('fs'); const path = require('path'); const inquirer = require('inquirer').default; const { Octokit } = require('@octokit/rest'); const CONFIG_PATH = path.join(__dirname, '../db.json'); const os = require('os'); const TOKEN_PATH = path.join(os.homedir(), '.gh-legacy', '.gh_token'); const { sendEmailNotification } = require('../lib/email'); const crypto = require('crypto'); const TRANSFERRED_PATH = path.join(os.homedir(), '.gh-legacy', 'transferred.json'); const GRACE_PERIOD_MS = 2 * 60 * 1000; // 2 minutes function decryptToken() { const { encrypted, iv } = JSON.parse(fs.readFileSync(TOKEN_PATH)); const key = crypto.createHash('sha256').update('gh-legacy-secret-key').digest(); const decipher = crypto.createDecipheriv('aes-256-cbc', key, Buffer.from(iv, 'hex')); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } function parseDuration(durationStr) { const [value, unit] = durationStr.toLowerCase().split(' '); const num = parseInt(value); switch (unit) { case 'minutes': case 'minute': return num * 60 * 1000; case 'hours': case 'hour': return num * 60 * 60 * 1000; case 'days': case 'day': return num * 24 * 60 * 60 * 1000; case 'weeks': case 'week': return num * 7 * 24 * 60 * 60 * 1000; case 'months': case 'month': return num * 30 * 24 * 60 * 60 * 1000; case 'years': case 'year': return num * 365 * 24 * 60 * 60 * 1000; default: return 0; } } function formatTimeRemaining(milliseconds) { const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); const months = Math.floor(days / 30); const years = Math.floor(months / 12); if (years > 0) return `${years} year${years > 1 ? 's' : ''}`; if (months > 0) return `${months} month${months > 1 ? 's' : ''}`; if (days > 0) return `${days} day${days > 1 ? 's' : ''}`; if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''}`; if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''}`; return `${seconds} second${seconds > 1 ? 's' : ''}`; } exports.triggerAccess = async (options) => { if (!fs.existsSync(CONFIG_PATH)) { console.error('āŒ No configuration found. Run "gh-legacy init" first.'); return; } const config = JSON.parse(fs.readFileSync(CONFIG_PATH)); let transferred = []; if (fs.existsSync(TRANSFERRED_PATH)) { transferred = JSON.parse(fs.readFileSync(TRANSFERRED_PATH)); } const token = decryptToken(); const octokit = new Octokit({ auth: token }); if (!options.auto) { const confirm = await inquirer.prompt([ { type: 'confirm', name: 'proceed', message: 'This will evaluate beneficiary access conditions and potentially transfer repository ownership. Continue?', default: false } ]); if (!confirm.proceed) return; } const now = Date.now(); const lastHeartbeat = new Date(config.lastHeartbeat).getTime(); let updated = false; let beneficiariesToKeep = []; // Filter beneficiaries if detail option is provided let beneficiaries = config.beneficiaries; if (options.detail) { beneficiaries = beneficiaries.filter(b => b.repo === options.detail); if (beneficiaries.length === 0) { console.log(`āŒ No beneficiary found for repo: ${options.detail}`); return; } console.log(`šŸ”Ž [Filter] Only processing repo: ${options.detail}`); } else { console.log(`šŸ”Ž [Filter] Processing all configured repos.`); } console.log(`\nšŸ“Š Checking access conditions...`); console.log(`ā° Last heartbeat: ${new Date(config.lastHeartbeat).toLocaleString()}`); console.log(`ā±ļø Time since last heartbeat: ${formatTimeRemaining(now - lastHeartbeat)}\n`); for (const b of beneficiaries) { if (b.granted) { console.log(`āœ… ${b.githubUsername} already has access to ${b.repo}`); continue; } const accessDelay = parseDuration(b.accessAfter); const timeSinceHeartbeat = now - lastHeartbeat; console.log(`šŸ” Checking ${b.githubUsername} for ${b.repo}:`); console.log(` Required delay: ${b.accessAfter}`); console.log(` Time since heartbeat: ${formatTimeRemaining(timeSinceHeartbeat)}`); if (timeSinceHeartbeat >= accessDelay + GRACE_PERIOD_MS) { let transferStatus = 'admin-granted'; let transferMessage = ''; try { const [owner, repo] = b.repo.split('/'); // First, try to add as collaborator try { await octokit.repos.addCollaborator({ owner, repo, username: b.githubUsername, permission: 'admin' }); transferMessage += `āœ… Added ${b.githubUsername} as admin to ${b.repo}\n`; } catch (collabError) { transferMessage += `āš ļø Could not add as collaborator: ${collabError.message}\n`; } // Try to transfer ownership try { await octokit.repos.transfer({ owner, repo, new_owner: b.githubUsername }); transferStatus = 'ownership-transferred'; transferMessage += `šŸŽ‰ Successfully transferred ownership of ${b.repo} to ${b.githubUsername}\n`; } catch (transferError) { transferMessage += `āš ļø Could not transfer ownership: ${transferError.message}\n`; transferMessage += `šŸ’” ${b.githubUsername} has been added as admin and can request ownership transfer\n`; } await sendEmailNotification(b.email, b.repo, b.githubUsername); transferMessage += `šŸ“§ Notification email sent to ${b.email}\n`; // Add to transferred.json transferred.push({ ...b, transferredAt: new Date().toISOString(), status: transferStatus, message: transferMessage.trim() }); updated = true; console.log(transferMessage); } catch (err) { console.error(`āŒ Failed to process access for ${b.githubUsername} on ${b.repo}:`, err.message); // Keep in main db if failed beneficiariesToKeep.push(b); continue; } } else if (timeSinceHeartbeat >= accessDelay) { const remaining = accessDelay + GRACE_PERIOD_MS - timeSinceHeartbeat; console.log(`ā³ Grace period active: ${formatTimeRemaining(remaining)} left before transfer for ${b.repo}`); beneficiariesToKeep.push(b); } else { const remaining = accessDelay - timeSinceHeartbeat; console.log(`ā³ ${b.githubUsername} will get access in ${formatTimeRemaining(remaining)}`); beneficiariesToKeep.push(b); } console.log(''); } // Save updated beneficiaries and transferred list if (options.detail) { // Remove only the processed repo from the main list, keep others untouched const processedRepos = new Set(beneficiaries.map(b => b.repo)); config.beneficiaries = config.beneficiaries.filter(b => !processedRepos.has(b.repo) || b.granted); } else { config.beneficiaries = beneficiariesToKeep; } fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)); fs.writeFileSync(TRANSFERRED_PATH, JSON.stringify(transferred, null, 2)); if (updated) { console.log('šŸ’¾ Configuration updated'); console.log('šŸ“¦ Transferred repos have been archived in transferred.json'); } else { console.log('ā³ No beneficiaries met the access conditions yet.'); } };