packmate
Version:
Your smart and friendly interactive assistant for dependency updates, security advisories, and cleanup.
750 lines (642 loc) โข 25 kB
JavaScript
/*****************************************************************
* 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);
});