UNPKG

packmate

Version:

Your smart and friendly assistant for dependency updates and cleanup.

323 lines (277 loc) โ€ข 11 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, 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); });