/ bin / package-manager.js
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  }