package-manager.js
1 'use strict' 2 /* eslint-disable @typescript-eslint/no-require-imports */ 3 4 const fs = require('node:fs') 5 const path = require('node:path') 6 7 const LOCKFILE_NAMES = [ 8 'package-lock.json', 9 'pnpm-lock.yaml', 10 'yarn.lock', 11 'bun.lock', 12 'bun.lockb', 13 ] 14 const INSTALL_METADATA_FILE = '.swarmclaw-install.json' 15 16 function normalizePackageManager(raw) { 17 switch (String(raw || '').trim().toLowerCase()) { 18 case 'pnpm': 19 case 'yarn': 20 case 'bun': 21 case 'npm': 22 return String(raw).trim().toLowerCase() 23 default: 24 return null 25 } 26 } 27 28 function detectPackageManagerFromUserAgent(userAgent) { 29 const normalized = String(userAgent || '').toLowerCase() 30 if (normalized.startsWith('pnpm/')) return 'pnpm' 31 if (normalized.startsWith('yarn/')) return 'yarn' 32 if (normalized.startsWith('bun/')) return 'bun' 33 if (normalized.startsWith('npm/')) return 'npm' 34 return null 35 } 36 37 function readInstallMetadata(rootDir) { 38 const metadataPath = path.join(rootDir, INSTALL_METADATA_FILE) 39 if (!fs.existsSync(metadataPath)) return null 40 try { 41 const raw = JSON.parse(fs.readFileSync(metadataPath, 'utf8')) 42 return raw && typeof raw === 'object' ? raw : null 43 } catch { 44 return null 45 } 46 } 47 48 function detectPackageManager(rootDir, env = process.env) { 49 const envOverride = normalizePackageManager(env.SWARMCLAW_PACKAGE_MANAGER) 50 if (envOverride) return envOverride 51 52 const installMetadata = readInstallMetadata(rootDir) 53 const installManager = normalizePackageManager(installMetadata?.packageManager) 54 if (installManager) return installManager 55 56 if (fs.existsSync(path.join(rootDir, 'bun.lock')) || fs.existsSync(path.join(rootDir, 'bun.lockb'))) return 'bun' 57 if (fs.existsSync(path.join(rootDir, 'pnpm-lock.yaml'))) return 'pnpm' 58 if (fs.existsSync(path.join(rootDir, 'yarn.lock'))) return 'yarn' 59 if (fs.existsSync(path.join(rootDir, 'package-lock.json'))) return 'npm' 60 61 const userAgentManager = detectPackageManagerFromUserAgent(env.npm_config_user_agent) 62 if (userAgentManager) return userAgentManager 63 return 'npm' 64 } 65 66 function getInstallCommand(packageManager, omitDev = false) { 67 switch (packageManager) { 68 case 'pnpm': 69 return omitDev 70 ? { command: 'pnpm', args: ['install', '--prod'] } 71 : { command: 'pnpm', args: ['install'] } 72 case 'yarn': 73 return omitDev 74 ? { command: 'yarn', args: ['install', '--production=true'] } 75 : { command: 'yarn', args: ['install'] } 76 case 'bun': 77 return omitDev 78 ? { command: 'bun', args: ['install', '--production'] } 79 : { command: 'bun', args: ['install'] } 80 case 'npm': 81 default: 82 return omitDev 83 ? { command: 'npm', args: ['install', '--omit=dev'] } 84 : { command: 'npm', args: ['install'] } 85 } 86 } 87 88 function getGlobalUpdateCommand(packageManager, packageName) { 89 return getGlobalUpdateSpec(packageManager, packageName).display 90 } 91 92 function getGlobalUpdateSpec(packageManager, packageName) { 93 switch (packageManager) { 94 case 'pnpm': 95 return { 96 command: 'pnpm', 97 args: ['add', '-g', `${packageName}@latest`], 98 display: `pnpm add -g ${packageName}@latest`, 99 } 100 case 'yarn': 101 return { 102 command: 'yarn', 103 args: ['global', 'add', `${packageName}@latest`], 104 display: `yarn global add ${packageName}@latest`, 105 } 106 case 'bun': 107 return { 108 command: 'bun', 109 args: ['add', '-g', `${packageName}@latest`], 110 display: `bun add -g ${packageName}@latest`, 111 } 112 case 'npm': 113 default: 114 return { 115 command: 'npm', 116 args: ['update', '-g', packageName], 117 display: `npm update -g ${packageName}`, 118 } 119 } 120 } 121 122 function getRunScriptCommand(packageManager, scriptName) { 123 switch (packageManager) { 124 case 'pnpm': 125 return { command: 'pnpm', args: [scriptName] } 126 case 'yarn': 127 return { command: 'yarn', args: [scriptName] } 128 case 'bun': 129 return { command: 'bun', args: ['run', scriptName] } 130 case 'npm': 131 default: 132 return { command: 'npm', args: ['run', scriptName] } 133 } 134 } 135 136 function dependenciesChanged(diffText) { 137 if (!diffText) return false 138 return String(diffText) 139 .split('\n') 140 .map((line) => line.trim()) 141 .filter(Boolean) 142 .some((file) => file === 'package.json' || LOCKFILE_NAMES.includes(file)) 143 } 144 145 module.exports = { 146 dependenciesChanged, 147 detectPackageManager, 148 detectPackageManagerFromUserAgent, 149 getGlobalUpdateCommand, 150 getGlobalUpdateSpec, 151 getInstallCommand, 152 getRunScriptCommand, 153 INSTALL_METADATA_FILE, 154 LOCKFILE_NAMES, 155 normalizePackageManager, 156 readInstallMetadata, 157 }