UNPKG

packmate

Version:

Your smart and friendly interactive assistant for dependency updates, security advisories, and cleanup.

750 lines (642 loc) โ€ข 25 kB
#!/usr/bin/env node /***************************************************************** * Packmate - Simple dependency update & unused checker * (c) 2025-present AGUMON (https://github.com/ljlm0402/packmate) * * This source code is licensed under the MIT license. * See the LICENSE file in the project root for more information. * * Made with โค๏ธ by AGUMON ๐Ÿฆ– *****************************************************************/ import { intro, outro, note, select, isCancel, cancel, confirm, spinner } from '@clack/prompts'; import chalk from 'chalk'; import depcheck from 'depcheck'; import fs from 'fs'; import { createRequire } from 'module'; import path from 'path'; import process from 'process'; import { getUpdateCandidates } from '../src/update-checker.js'; import { runUnusedCheck } from '../src/unused-checker.js'; import { detectPackageManager, getPackageManagerInfo, setDetectedPackageManager, } from '../src/detect-package-manager.js'; import { installPackages, uninstallPackages } from '../src/install-helper.js'; import { runWithWarningCapture, setProcessTracker } from '../src/warning-capture.js'; import { loadConfig, saveProjectConfig } from '../src/config-loader.js'; import { checkVulnerabilities } from '../src/security-checker.js'; import { getCacheManager } from '../src/enhanced-cache.js'; import { updateAvailableSession, unusedSession, notInstalledSession, latestSession, securitySession, } from '../src/ui-sessions.js'; const require = createRequire(import.meta.url); // ๊ธ€๋กœ๋ฒŒ ์ƒํƒœ: ์‹คํ–‰ ์ค‘์ธ ํ”„๋กœ์„ธ์Šค ์ถ”์  let runningProcesses = new Set(); let isExiting = false; async function resolvePackageManager(config) { const { found, available } = getPackageManagerInfo(); if (available.length === 0) { console.error(chalk.red('No package manager found. Please install npm, yarn, or pnpm.')); process.exit(1); } const availableLockManagers = found.filter((item) => item.available); if (config.packageManager && available.includes(config.packageManager)) { setDetectedPackageManager(config.packageManager); return config.packageManager; } if (availableLockManagers.length > 1) { const selected = await select({ message: 'Multiple lock files found. Which package manager should PackMate use?', options: availableLockManagers.map((item) => ({ label: item.name, value: item.name, hint: path.basename(item.filePath), })), initialValue: availableLockManagers[0].name, }); if (isCancel(selected)) { cancel(chalk.red('Operation cancelled.')); process.exit(0); } setDetectedPackageManager(selected); const remember = await confirm({ message: `Use ${selected} by default for this project?`, initialValue: true, }); if (!isCancel(remember) && remember) { saveProjectConfig({ packageManager: selected }); console.log(chalk.dim(`Saved package manager preference: ${selected}`)); } return selected; } if (availableLockManagers.length === 1) { setDetectedPackageManager(availableLockManagers[0].name); return availableLockManagers[0].name; } return detectPackageManager({ silent: true }); } function renderAnalysisSummary({ updateCount, unusedCount, notInstalledCount, latestCount, securitySummary, }) { const hasSecurityIssues = securitySummary.total > 0; const formatRow = (label, count, detail, color = chalk.white) => { const labelText = label.padEnd(10); const countText = String(count).padEnd(13); console.log(` ${labelText} ${color(countText)} ${chalk.dim(detail)}`); }; console.log('\n' + chalk.cyan.bold('Analysis Summary')); console.log(chalk.dim(` ${'Category'.padEnd(10)} ${'Count'.padEnd(13)} Details`)); formatRow('Updates', updateCount, 'Available package updates', chalk.cyan); formatRow('Unused', unusedCount, 'Possible cleanup candidates', chalk.yellow); formatRow('Missing', notInstalledCount, 'Declared but not installed', chalk.cyan); formatRow('Current', latestCount, 'Already up-to-date', chalk.green); formatRow( 'Security', hasSecurityIssues ? `${securitySummary.total} advisories` : '0 advisories', hasSecurityIssues ? `${securitySummary.direct || 0} direct, ${securitySummary.transitive || 0} transitive` : 'No known advisories', hasSecurityIssues ? chalk.red.bold : chalk.green, ); } function getActionPackageName(action) { return action.packageName || action.name; } async function resolveConflictingActions(actions) { const byPackage = new Map(); actions.forEach((action) => { const packageName = getActionPackageName(action); if (!packageName) return; if (!byPackage.has(packageName)) { byPackage.set(packageName, []); } byPackage.get(packageName).push(action); }); const resolved = [...actions]; for (const [packageName, packageActions] of byPackage.entries()) { const securityUpdate = packageActions.find((action) => action.action === 'update' && action.packageName); const remove = packageActions.find((action) => action.action === 'remove'); if (!securityUpdate || !remove) continue; const choice = await select({ message: `${packageName} is both vulnerable and marked unused. What should PackMate do?`, options: [ { label: 'Remove package', value: 'remove', hint: 'Best if it is truly unused', }, { label: 'Update package', value: 'update', hint: 'Use if the package is still needed', }, { label: 'Skip for now', value: 'skip', hint: 'Make no change to this package', }, ], initialValue: 'remove', }); if (isCancel(choice)) { cancel(chalk.red('Operation cancelled.')); process.exit(0); } for (let i = resolved.length - 1; i >= 0; i--) { const action = resolved[i]; if (getActionPackageName(action) !== packageName) continue; if (choice === 'remove' && action !== remove) { resolved.splice(i, 1); } if (choice === 'update' && action !== securityUpdate) { resolved.splice(i, 1); } if (choice === 'skip') { resolved.splice(i, 1); } } } return resolved; } async function confirmActions(actions) { const removes = actions.filter((action) => action.action === 'remove'); const securityUpdates = actions.filter((action) => action.action === 'update' && action.packageName); const regularUpdates = actions.filter((action) => action.action === 'update' && !action.packageName); const installs = actions.filter((action) => action.action === 'install'); console.log('\n' + chalk.cyan.bold('Review Actions')); if (removes.length > 0) { console.log(chalk.red.bold(` Remove (${removes.length})`)); removes.forEach((action) => console.log(chalk.red(` - ${action.name}`))); } if (securityUpdates.length > 0) { console.log(chalk.yellow.bold(` Security updates (${securityUpdates.length})`)); securityUpdates.forEach((action) => console.log(chalk.yellow(` - ${action.packageName} (${action.priority})`))); } if (regularUpdates.length > 0) { console.log(chalk.cyan.bold(` Updates (${regularUpdates.length})`)); regularUpdates.forEach((action) => console.log(chalk.cyan(` - ${action.name} โ†’ ${action.latestVersion}`))); } if (installs.length > 0) { console.log(chalk.cyan.bold(` Install (${installs.length})`)); installs.forEach((action) => console.log(chalk.cyan(` - ${action.name}`))); } const confirmed = await confirm({ message: `Proceed with ${actions.length} action(s)?`, initialValue: true, }); if (isCancel(confirmed) || !confirmed) { console.log(chalk.yellow('\nNo changes were made.')); return false; } return true; } /** * ๊นจ๋—ํ•œ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ */ async function gracefulExit(signal = 'SIGINT') { if (isExiting) return; // ์ด๋ฏธ ์ข…๋ฃŒ ์ค‘์ด๋ฉด ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€ isExiting = true; console.log(chalk.yellow(`\nโš ๏ธ Received ${signal}. Cleaning up...`)); // ์‹คํ–‰ ์ค‘์ธ ๋ชจ๋“  ์ž์‹ ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒ if (runningProcesses.size > 0) { console.log(chalk.yellow(`๐Ÿ›‘ Terminating ${runningProcesses.size} running process(es)...`)); for (const childProcess of runningProcesses) { try { if (childProcess && !childProcess.killed) { childProcess.kill('SIGTERM'); // ๊ฐ•์ œ ์ข…๋ฃŒ๋ฅผ ์œ„ํ•œ ํƒ€์ž„์•„์›ƒ setTimeout(() => { if (!childProcess.killed) { childProcess.kill('SIGKILL'); } }, 3000); // 3์ดˆ ํ›„ ๊ฐ•์ œ ์ข…๋ฃŒ } } catch (error) { // ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒ ์ค‘ ์—๋Ÿฌ ๋ฌด์‹œ } } runningProcesses.clear(); } // ์บ์‹œ ์ •๋ฆฌ try { const cacheManager = getCacheManager(); await cacheManager.close(); } catch (error) { // ์บ์‹œ ์ •๋ฆฌ ์‹คํŒจ๋Š” ๋ฌด์‹œ } console.log(chalk.blue('๐Ÿงน Cleanup complete. Exiting...')); outro(chalk.red('Packmate interrupted! ๐Ÿ‘‹')); process.exit(signal === 'SIGTERM' ? 0 : 130); // Ctrl+C๋Š” 130 exit code } /** * ์‹ ํ˜ธ ํ•ธ๋“ค๋Ÿฌ ์„ค์ • */ function setupSignalHandlers() { // Ctrl+C (SIGINT) process.on('SIGINT', () => gracefulExit('SIGINT')); // Termination signal (SIGTERM) process.on('SIGTERM', () => gracefulExit('SIGTERM')); // Windows์—์„œ CTRL+BREAK (SIGBREAK) if (process.platform === 'win32') { process.on('SIGBREAK', () => gracefulExit('SIGBREAK')); } // Uncaught Exception/Rejection ์ฒ˜๋ฆฌ process.on('uncaughtException', (error) => { console.error(chalk.red('๐Ÿ’ฅ Uncaught Exception:'), error); gracefulExit('uncaughtException'); }); process.on('unhandledRejection', (reason, promise) => { console.error(chalk.red('๐Ÿ’ฅ Unhandled Promise Rejection:'), reason); gracefulExit('unhandledRejection'); }); } /** * ์„ค์น˜๋œ ํŒจํ‚ค์ง€์˜ ํ˜„์žฌ ๋ฒ„์ „์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค */ function getCurrentVersion(dep) { // ๋ฐฉ๋ฒ• 1: ํ‘œ์ค€ node_modules ์œ„์น˜ ํ™•์ธ (npm, yarn, pnpm์˜ node-linker=hoisted์—์„œ ์ž‘๋™) try { const pkgPath = path.resolve(process.cwd(), 'node_modules', dep, 'package.json'); if (fs.existsSync(pkgPath)) { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); return pkg.version; } } catch (err) { // ๋‹ค์Œ ๋ฐฉ๋ฒ•์œผ๋กœ ๊ณ„์† } // ๋ฐฉ๋ฒ• 2: pnpm์˜ .pnpm ๋””๋ ‰ํ† ๋ฆฌ ํ™•์ธ (node-linker=isolated์ธ pnpm์šฉ) try { const pnpmDir = path.resolve(process.cwd(), 'node_modules', '.pnpm'); if (fs.existsSync(pnpmDir)) { const entries = fs.readdirSync(pnpmDir); // @clack/prompts ๊ฐ™์€ ์Šค์ฝ”ํ”„ ํŒจํ‚ค์ง€ ์ฒ˜๋ฆฌ const depName = dep.replace('/', '+'); const found = entries.find((f) => f.startsWith(depName + '@')); if (found) { const pkgPath = path.resolve(pnpmDir, found, 'node_modules', dep, 'package.json'); if (fs.existsSync(pkgPath)) { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); return pkg.version; } } } } catch (err) { // ๋‹ค์Œ ๋ฐฉ๋ฒ•์œผ๋กœ ๊ณ„์† } // ๋ฐฉ๋ฒ• 3: require.resolve ์‹œ๋„ (์ผ๋ถ€ ESM ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ) try { const mainPath = require.resolve(`${dep}/package.json`, { paths: [process.cwd()] }); if (mainPath && fs.existsSync(mainPath)) { const pkg = JSON.parse(fs.readFileSync(mainPath, 'utf-8')); return pkg.version; } } catch (err) { // ํŒจํ‚ค์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ } return null; } /** * ์„ ์–ธ๋˜์—ˆ์ง€๋งŒ ์„ค์น˜๋˜์ง€ ์•Š์€ ํŒจํ‚ค์ง€ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค */ function getNotInstalledPackages() { const pkgPath = path.resolve(process.cwd(), 'package.json'); if (!fs.existsSync(pkgPath)) return []; const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies }; const notInstalled = []; for (const dep of Object.keys(allDeps)) { const version = getCurrentVersion(dep); if (!version) { // null์€ ์ฐพ์„ ์ˆ˜ ์—†์Œ์„ ์˜๋ฏธ notInstalled.push(dep); } } return notInstalled; } async function main() { // ์‹ ํ˜ธ ํ•ธ๋“ค๋Ÿฌ ์„ค์ • (๊ฐ€์žฅ ๋จผ์ €) setupSignalHandlers(); // ํ”„๋กœ์„ธ์Šค ์ถ”์  ์„ค์ • setProcessTracker({ onStart: (child) => { runningProcesses.add(child); }, onEnd: (child) => { runningProcesses.delete(child); } }); intro(chalk.cyan('๐Ÿ“ฆ Packmate: Dependency Updates & Cleanup')); // ์„ค์ • ๋กœ๋“œ const config = loadConfig(); // node_modules ํ™•์ธ const nodeModulesPath = path.resolve(process.cwd(), 'node_modules'); if (!fs.existsSync(nodeModulesPath)) { note( chalk.yellow( 'โš ๏ธ The node_modules directory is missing. Please install dependencies first (npm/yarn/pnpm install).', ), 'Warning', ); process.exit(0); } const packageManager = await resolvePackageManager(config); const analysisSpinner = spinner(); // 1. ๋ฏธ์‚ฌ์šฉ ํŒจํ‚ค์ง€ ๋จผ์ € ๋ถ„์„ (์—…๋ฐ์ดํŠธ ํ•„ํ„ฐ๋ง์šฉ) analysisSpinner.start('Scanning project files...'); const unused_precinct = await runUnusedCheck({ withUsedList: true }); // depcheck๋กœ ๊ต์ฐจ ๊ฒ€์ฆ analysisSpinner.message('Checking dependency usage...'); const depcheckResult = await depcheck(process.cwd(), { ignoreDirs: ['node_modules', 'backup', 'dist', 'build', 'coverage', '.git', '.packmate'], }); const unused_depcheck = depcheckResult.dependencies || []; // ์‹ ๋ขฐ๋„๋ณ„ ๋ถ„๋ฅ˜ const bothUnused = unused_precinct.unused.filter((x) => unused_depcheck.includes(x)); const onlyPrecinct = unused_precinct.unused.filter((x) => !unused_depcheck.includes(x)); const onlyDepcheck = unused_depcheck.filter((x) => !unused_precinct.unused.includes(x)); // ํ•„ํ„ฐ๋ง์„ ์œ„ํ•œ ๋ชจ๋“  ๋ฏธ์‚ฌ์šฉ ํŒจํ‚ค์ง€ ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ const allUnusedNames = [...bothUnused, ...onlyPrecinct, ...onlyDepcheck]; // 2. ์—…๋ฐ์ดํŠธ ๊ฐ€๋Šฅํ•œ ํŒจํ‚ค์ง€ ๋ถ„์„ (๋ฏธ์‚ฌ์šฉ ํŒจํ‚ค์ง€ ์ œ์™ธ) analysisSpinner.message('Checking available updates...'); const allUpdateCandidates = await getUpdateCandidates(packageManager, { showProgress: config.ui?.showProgress === true, }); // ์—…๋ฐ์ดํŠธ ํ›„๋ณด์—์„œ ๋ฏธ์‚ฌ์šฉ ํŒจํ‚ค์ง€ ํ•„ํ„ฐ๋ง const updateCandidates = allUpdateCandidates.filter( (candidate) => !allUnusedNames.includes(candidate.name) ); const unusedPackages = [ ...bothUnused.map((dep) => ({ name: dep, current: getCurrentVersion(dep), confidence: 'high', hint: 'Detected by both precinct and depcheck', })), ...onlyPrecinct.map((dep) => ({ name: dep, current: getCurrentVersion(dep), confidence: 'medium', hint: 'Detected by precinct only', })), ...onlyDepcheck.map((dep) => ({ name: dep, current: getCurrentVersion(dep), confidence: 'medium', hint: 'Detected by depcheck only', })), ]; // 3. ๋ฏธ์„ค์น˜ ํŒจํ‚ค์ง€ ํ™•์ธ analysisSpinner.message('Checking install state...'); const notInstalled = getNotInstalledPackages(); const notInstalledPackages = notInstalled.map((dep) => ({ name: dep, current: '-', latest: '-', })); // 4. ๋ณด์•ˆ ์ทจ์•ฝ์„ฑ ๊ฒ€์‚ฌ (์„ค์ •์— ๋”ฐ๋ผ) let securityResults = { vulnerabilities: [], classified: {}, grouped: {}, summary: { total: 0, direct: 0, transitive: 0, critical: 0, high: 0, moderate: 0, low: 0, info: 0 }, }; if (config.security?.enabled !== false) { try { analysisSpinner.message('Checking security advisories...'); securityResults = await checkVulnerabilities(); } catch (error) { analysisSpinner.stop('Project analyzed'); console.warn(chalk.dim('Security scan could not be completed. Continuing...')); } } else { analysisSpinner.stop('Project analyzed'); console.log(chalk.dim('๐Ÿ”’ Security scan disabled in config')); } if (config.security?.enabled !== false) { analysisSpinner.stop('Project analyzed'); } // 5. ์ตœ์‹  ๋ฒ„์ „ ํŒจํ‚ค์ง€ const pkgJson = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), 'package.json'), 'utf-8')); const declared = { ...pkgJson.dependencies, ...pkgJson.devDependencies }; const latestPackages = []; for (const dep of Object.keys(declared)) { const isUpdatable = updateCandidates.some((c) => c.name === dep); const isUnused = unusedPackages.some((u) => u.name === dep); const isNotInstalled = notInstalledPackages.some((n) => n.name === dep); if (!isUpdatable && !isUnused && !isNotInstalled) { const current = getCurrentVersion(dep); if (current && current !== '-') { latestPackages.push({ name: dep, current, latest: current, }); } } } renderAnalysisSummary({ updateCount: updateCandidates.length, unusedCount: unusedPackages.length, notInstalledCount: notInstalledPackages.length, latestCount: latestPackages.length, securitySummary: securityResults.summary, }); let selectedActions = []; // === ๊ทธ๋ฃน๋ณ„ UI ์„ธ์…˜ ์‹คํ–‰ === if (config.ui?.groupSessions) { // 0. ๋ฏธ์‚ฌ์šฉ ํŒจํ‚ค์ง€ ์ •๋ฆฌ ํ›„๋ณด๋ฅผ ๋จผ์ € ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. // ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ํŒจํ‚ค์ง€๋Š” ์—…๋ฐ์ดํŠธ๋ณด๋‹ค ์ œ๊ฑฐ๊ฐ€ ๋” ์ ์ ˆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. if (unusedPackages.length > 0) { const unusedSelected = await unusedSession(unusedPackages, config); selectedActions.push(...unusedSelected); } const selectedRemoveNames = selectedActions .filter((action) => action.action === 'remove') .map((action) => action.name); // 1. ๋ณด์•ˆ ์ทจ์•ฝ์„ฑ ์„ธ์…˜ if (securityResults.summary.total > 0) { const securitySelected = await securitySession(securityResults, config, { excludePackages: selectedRemoveNames, }); selectedActions.push(...securitySelected); } // 2. ์—…๋ฐ์ดํŠธ ๊ฐ€๋Šฅ ์„ธ์…˜ if (updateCandidates.length > 0) { const updateSelected = await updateAvailableSession(updateCandidates, config); selectedActions.push(...updateSelected); } // 3. ๋ฏธ์„ค์น˜ ํŒจํ‚ค์ง€ ์„ธ์…˜ if (notInstalledPackages.length > 0) { const notInstalledSelected = await notInstalledSession(notInstalledPackages, config); selectedActions.push(...notInstalledSelected); } // 4. ์ตœ์‹  ๋ฒ„์ „ ํŒจํ‚ค์ง€ ์„ธ์…˜ (์„ ํƒ ์‚ฌํ•ญ) if (latestPackages.length > 0) { await latestSession(latestPackages, config); } } else { note( chalk.yellow('โš ๏ธ groupSessions is disabled in config. Refer to packmate.js.backup for legacy mode.'), 'Info', ); } // === ์ž‘์—… ์‹คํ–‰ === if (selectedActions.length === 0) { note(chalk.yellow('No actions selected.'), 'Info'); outro(chalk.bold.cyan('Packmate complete! ๐Ÿ‘‹')); return; } selectedActions = await resolveConflictingActions(selectedActions); if (selectedActions.length === 0) { note(chalk.yellow('No actions selected.'), 'Info'); outro(chalk.bold.cyan('Packmate complete! ๐Ÿ‘‹')); return; } const confirmed = await confirmActions(selectedActions); if (!confirmed) { outro(chalk.bold.cyan('Packmate complete! ๐Ÿ‘‹')); return; } // ๋ณด์•ˆ ์—…๋ฐ์ดํŠธ ์‹คํ–‰ (์šฐ์„ ์ˆœ์œ„: critical > high > moderate > low) const securityUpdates = selectedActions.filter((a) => a.action === 'update' && a.packageName); const priorityOrder = ['critical', 'high', 'moderate', 'low']; for (const priority of priorityOrder) { const priorityUpdates = securityUpdates.filter(a => a.priority === priority); for (const item of priorityUpdates) { let cmd, args; switch (packageManager) { case 'pnpm': cmd = 'pnpm'; args = ['add', `${item.packageName}@latest`]; break; case 'yarn': cmd = 'yarn'; args = ['add', `${item.packageName}@latest`]; break; case 'npm': default: cmd = 'npm'; args = ['install', `${item.packageName}@latest`]; break; } note(chalk.red(`๐Ÿ›ก๏ธ Security Update [${priority.toUpperCase()}]: ${cmd} ${args.join(' ')}`), 'Security Command'); const { code, warnings, terminated, signal } = await runWithWarningCapture(cmd, args); // ์‚ฌ์šฉ์ž๊ฐ€ ์ค‘๋‹จํ•œ ๊ฒฝ์šฐ ์ฆ‰์‹œ ์ข…๋ฃŒ if (terminated) { console.log(chalk.yellow(`\nโš ๏ธ Security update interrupted by ${signal}`)); return; // main ํ•จ์ˆ˜ ์ข…๋ฃŒ } if (code === 0) { note(chalk.green(`โœ”๏ธ Security update complete: ${item.packageName} (${priority})`), 'Security Success'); } else { note(chalk.red(`โŒ Security update failed: ${item.packageName} (${priority})`), 'Security Failed'); } if (warnings.length) { note(chalk.yellow(`โš ๏ธ Warnings:\n${warnings.map((w) => ' - ' + w).join('\n')}`), 'Warning'); } } } // ์ผ๋ฐ˜ ์—…๋ฐ์ดํŠธ ์‹คํ–‰ const toUpdate = selectedActions.filter((a) => a.action === 'update' && !a.packageName); for (const item of toUpdate) { let cmd, args; switch (packageManager) { case 'pnpm': cmd = 'pnpm'; args = ['add', `${item.name}@${item.latestVersion}`]; break; case 'yarn': cmd = 'yarn'; args = ['add', `${item.name}@${item.latestVersion}`]; break; case 'npm': default: cmd = 'npm'; args = ['install', `${item.name}@${item.latestVersion}`]; break; } note(chalk.cyan(`${cmd} ${args.join(' ')}`), 'Command'); const { code, warnings, terminated, signal } = await runWithWarningCapture(cmd, args); // ์‚ฌ์šฉ์ž๊ฐ€ ์ค‘๋‹จํ•œ ๊ฒฝ์šฐ ์ฆ‰์‹œ ์ข…๋ฃŒ if (terminated) { console.log(chalk.yellow(`\nโš ๏ธ Update interrupted by ${signal}`)); return; // main ํ•จ์ˆ˜ ์ข…๋ฃŒ } if (code === 0) { note(chalk.green(`โœ”๏ธ Update complete: ${item.name}@${item.latestVersion}`), 'Success'); } else { note(chalk.red(`โŒ Update failed: ${item.name}@${item.latestVersion}`), 'Failed'); } if (warnings.length) { note(chalk.yellow(`โš ๏ธ Warnings:\n${warnings.map((w) => ' - ' + w).join('\n')}`), 'Warning'); } } // ์ œ๊ฑฐ ์‹คํ–‰ const toRemove = selectedActions.filter((a) => a.action === 'remove').map((a) => a.name); if (toRemove.length > 0) { try { uninstallPackages(toRemove, packageManager); } catch (error) { if (error.signal === 'SIGTERM' || error.signal === 'SIGINT') { console.log(chalk.yellow(`\nโš ๏ธ Package removal interrupted by ${error.signal}`)); return; // main ํ•จ์ˆ˜ ์ข…๋ฃŒ } // ๋‹ค๋ฅธ ์—๋Ÿฌ๋Š” ๊ณ„์† ์ง„ํ–‰ } } // ์„ค์น˜ ์‹คํ–‰ const toInstall = selectedActions.filter((a) => a.action === 'install').map((a) => a.name); if (toInstall.length > 0) { try { installPackages(toInstall, packageManager); } catch (error) { if (error.signal === 'SIGTERM' || error.signal === 'SIGINT') { console.log(chalk.yellow(`\nโš ๏ธ Package installation interrupted by ${error.signal}`)); return; // main ํ•จ์ˆ˜ ์ข…๋ฃŒ } // ๋‹ค๋ฅธ ์—๋Ÿฌ๋Š” ๊ณ„์† ์ง„ํ–‰ } } // ์ตœ์ข… ์š”์•ฝ - console.log๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋” ๋‚˜์€ ํฌ๋งทํŒ… const securityUpdateCount = securityUpdates.length; console.log('\n' + chalk.green.bold('โœ… Complete:')); if (securityUpdateCount > 0) { console.log(chalk.green(` Security: ${securityUpdateCount} (vulnerabilities addressed)`)); } console.log(chalk.green(` Updated: ${toUpdate.length}`)); console.log(chalk.green(` Removed: ${toRemove.length}`)); console.log(chalk.green(` Installed: ${toInstall.length}`)); // Smart Cache ์„ฑ๋Šฅ ํ†ต๊ณ„ (์„ค์ •์— ๋”ฐ๋ผ) if (config.cache?.showStats !== false) { try { const cacheManager = getCacheManager(); const cacheStats = await cacheManager.getDetailedStats(); if (cacheStats.performance.totalRequests > 0) { console.log('\n' + chalk.blue.bold('๐Ÿ“Š Cache Performance:')); console.log(chalk.blue(` Hit Rate: ${cacheStats.performance.hitRate}`)); console.log(chalk.blue(` Memory Hits: ${cacheStats.performance.breakdown.memory}`)); console.log(chalk.blue(` Disk Hits: ${cacheStats.performance.breakdown.disk}`)); console.log(chalk.blue(` Network Hits: ${cacheStats.performance.breakdown.network}`)); console.log(chalk.dim(` Cache Size: ${cacheStats.disk.diskSize}`)); } if (config.cache?.autoCleanup !== false) { await cacheManager.cleanup(); } } catch (error) { // ์บ์‹œ ํ†ต๊ณ„ ์˜ค๋ฅ˜๋Š” ๋ฌด์‹œ } } outro(chalk.bold.cyan('Packmate complete! ๐ŸŽ‰')); } main().catch((error) => { console.error(chalk.red('Error occurred:'), error); process.exit(1); });