packmate
Version:
Your smart and friendly assistant for dependency updates and cleanup.
323 lines (277 loc) โข 11 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, 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 } from '../src/detect-package-manager.js';
import { installPackages, uninstallPackages } from '../src/install-helper.js';
import { runWithWarningCapture } from '../src/warning-capture.js';
import { loadConfig } from '../src/config-loader.js';
import {
updateAvailableSession,
unusedSession,
notInstalledSession,
latestSession,
} from '../src/ui-sessions.js';
const require = createRequire(import.meta.url);
/**
* ์ค์น๋ ํจํค์ง์ ํ์ฌ ๋ฒ์ ์ ๊ฐ์ ธ์ต๋๋ค
*/
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() {
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 = detectPackageManager();
note(chalk.dim(`Package Manager: ${packageManager}`), 'Info');
const s = spinner();
// 1. ๋ฏธ์ฌ์ฉ ํจํค์ง ๋จผ์ ๋ถ์ (์
๋ฐ์ดํธ ํํฐ๋ง์ฉ)
s.start('Analyzing unused packages...');
const unused_precinct = await runUnusedCheck({ withUsedList: true });
// depcheck๋ก ๊ต์ฐจ ๊ฒ์ฆ
const depcheckResult = await depcheck(process.cwd(), {});
const unused_depcheck = depcheckResult.dependencies || [];
s.stop('โ
Unused package analysis complete');
// ์ ๋ขฐ๋๋ณ ๋ถ๋ฅ
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. ์
๋ฐ์ดํธ ๊ฐ๋ฅํ ํจํค์ง ๋ถ์ (๋ฏธ์ฌ์ฉ ํจํค์ง ์ ์ธ)
s.start('Checking for available updates...');
const allUpdateCandidates = await getUpdateCandidates(packageManager);
// ์
๋ฐ์ดํธ ํ๋ณด์์ ๋ฏธ์ฌ์ฉ ํจํค์ง ํํฐ๋ง
const updateCandidates = allUpdateCandidates.filter(
(candidate) => !allUnusedNames.includes(candidate.name)
);
s.stop(`โ
Found ${updateCandidates.length} packages with available updates`);
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. ๋ฏธ์ค์น ํจํค์ง ํ์ธ
s.start('Checking for not installed packages...');
const notInstalled = getNotInstalledPackages();
s.stop(`โ
Found ${notInstalled.length} not installed packages`);
const notInstalledPackages = notInstalled.map((dep) => ({
name: dep,
current: '-',
latest: '-',
}));
// 4. ์ต์ ๋ฒ์ ํจํค์ง
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,
});
}
}
}
// === ๋ถ์ ๊ฒฐ๊ณผ ์์ฝ ===
// console.log๋ฅผ ์ฌ์ฉํ์ฌ ๋ ๋์ ํฌ๋งทํ
console.log('\n' + chalk.cyan.bold('๐ Analysis Results:'));
console.log(chalk.cyan(` Updates available: ${updateCandidates.length}`));
console.log(chalk.cyan(` Unused: ${unusedPackages.length}`));
console.log(chalk.cyan(` Not installed: ${notInstalledPackages.length}`));
console.log(chalk.cyan(` Up-to-date: ${latestPackages.length}`));
const selectedActions = [];
// === ๊ทธ๋ฃน๋ณ UI ์ธ์
์คํ ===
if (config.ui?.groupSessions) {
// 1. ์
๋ฐ์ดํธ ๊ฐ๋ฅ ์ธ์
if (updateCandidates.length > 0) {
const updateSelected = await updateAvailableSession(updateCandidates, config);
selectedActions.push(...updateSelected);
}
// 2. ๋ฏธ์ฌ์ฉ ํจํค์ง ์ธ์
if (unusedPackages.length > 0) {
const unusedSelected = await unusedSession(unusedPackages, config);
selectedActions.push(...unusedSelected);
}
// 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;
}
note(
chalk.cyan(
`\n๐ Actions to execute:\n${selectedActions.map((a) => ` - ${a.action}: ${a.name}${a.latestVersion ? '@' + a.latestVersion : ''}`).join('\n')}`,
),
'Actions',
);
// ์
๋ฐ์ดํธ ์คํ
const toUpdate = selectedActions.filter((a) => a.action === 'update');
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 } = await runWithWarningCapture(cmd, args);
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) {
uninstallPackages(toRemove, packageManager);
}
// ์ค์น ์คํ
const toInstall = selectedActions.filter((a) => a.action === 'install').map((a) => a.name);
if (toInstall.length > 0) {
installPackages(toInstall, packageManager);
}
// ์ต์ข
์์ฝ - console.log๋ฅผ ์ฌ์ฉํ์ฌ ๋ ๋์ ํฌ๋งทํ
console.log('\n' + chalk.green.bold('โ
Complete:'));
console.log(chalk.green(` Updated: ${toUpdate.length}`));
console.log(chalk.green(` Removed: ${toRemove.length}`));
console.log(chalk.green(` Installed: ${toInstall.length}`));
outro(chalk.bold.cyan('Packmate complete! ๐'));
}
main().catch((error) => {
console.error(chalk.red('Error occurred:'), error);
process.exit(1);
});