install-root.js
1 #!/usr/bin/env node 2 'use strict' 3 4 /* eslint-disable @typescript-eslint/no-require-imports */ 5 const fs = require('node:fs') 6 const os = require('node:os') 7 const path = require('node:path') 8 const { execFileSync } = require('node:child_process') 9 10 const PACKAGE_NAME = '@swarmclawai/swarmclaw' 11 const CORE_PACKAGE_NAMES = new Set([PACKAGE_NAME]) 12 13 function normalizeDir(value) { 14 if (!value) return null 15 const trimmed = String(value).trim() 16 if (!trimmed) return null 17 return path.resolve(trimmed) 18 } 19 20 function readPackageJson(rootDir) { 21 try { 22 return JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8')) 23 } catch { 24 return null 25 } 26 } 27 28 function readPackageName(rootDir) { 29 return readPackageJson(rootDir)?.name?.trim() || null 30 } 31 32 function readPackageVersion(rootDir) { 33 const version = readPackageJson(rootDir)?.version 34 return typeof version === 'string' && version.trim() ? version.trim() : null 35 } 36 37 function* iterAncestorDirs(startDir, maxDepth = 12) { 38 let current = path.resolve(startDir) 39 for (let i = 0; i < maxDepth; i += 1) { 40 yield current 41 const parent = path.dirname(current) 42 if (parent === current) break 43 current = parent 44 } 45 } 46 47 function findPackageRoot(startDir, maxDepth = 12) { 48 for (const current of iterAncestorDirs(startDir, maxDepth)) { 49 const name = readPackageName(current) 50 if (name && CORE_PACKAGE_NAMES.has(name)) return current 51 } 52 return null 53 } 54 55 function candidateDirsFromArgv1(argv1) { 56 const normalized = normalizeDir(argv1) 57 if (!normalized) return [] 58 59 const candidates = [path.dirname(normalized)] 60 try { 61 const resolved = fs.realpathSync(normalized) 62 if (resolved !== normalized) candidates.push(path.dirname(resolved)) 63 } catch {} 64 65 const parts = normalized.split(path.sep) 66 const binIndex = parts.lastIndexOf('.bin') 67 if (binIndex > 0 && parts[binIndex - 1] === 'node_modules') { 68 const binName = path.basename(normalized) 69 const nodeModulesDir = parts.slice(0, binIndex).join(path.sep) 70 candidates.push(path.join(nodeModulesDir, binName)) 71 } 72 73 return candidates 74 } 75 76 function resolvePackageRoot(opts = {}) { 77 const candidates = [] 78 const moduleDir = normalizeDir(opts.moduleDir) 79 if (moduleDir) candidates.push(moduleDir) 80 const argv1 = opts.argv1 === undefined ? process.argv[1] : opts.argv1 81 candidates.push(...candidateDirsFromArgv1(argv1)) 82 const cwd = opts.cwd === undefined ? process.cwd() : opts.cwd 83 if (normalizeDir(cwd)) candidates.push(path.resolve(cwd)) 84 85 for (const candidate of candidates) { 86 const found = findPackageRoot(candidate) 87 if (found) return found 88 } 89 90 return moduleDir ? path.resolve(moduleDir, '..') : null 91 } 92 93 function tryRealpath(targetPath) { 94 try { 95 return fs.realpathSync(targetPath) 96 } catch { 97 return path.resolve(targetPath) 98 } 99 } 100 101 const isWindows = process.platform === 'win32' 102 103 function runRootCommand(command, args, execImpl = execFileSync) { 104 try { 105 return String(execImpl(command, args, { 106 encoding: 'utf8', 107 stdio: ['ignore', 'pipe', 'pipe'], 108 ...(isWindows && { shell: true }), 109 })).trim() 110 } catch { 111 return null 112 } 113 } 114 115 function resolveGlobalRoot(manager, execImpl = execFileSync, env = process.env) { 116 if (manager === 'bun') { 117 const bunInstall = String(env.BUN_INSTALL || '').trim() || path.join(os.homedir(), '.bun') 118 return path.join(bunInstall, 'install', 'global', 'node_modules') 119 } 120 121 if (manager === 'pnpm') { 122 return runRootCommand('pnpm', ['root', '-g'], execImpl) 123 } 124 125 return runRootCommand('npm', ['root', '-g'], execImpl) 126 } 127 128 function detectGlobalInstallManagerForRoot(pkgRoot, execImpl = execFileSync, env = process.env) { 129 const pkgReal = tryRealpath(pkgRoot) 130 131 for (const manager of ['npm', 'pnpm']) { 132 const globalRoot = resolveGlobalRoot(manager, execImpl, env) 133 if (!globalRoot) continue 134 135 for (const name of CORE_PACKAGE_NAMES) { 136 const expectedReal = tryRealpath(path.join(globalRoot, name)) 137 if (path.resolve(expectedReal) === path.resolve(pkgReal)) return manager 138 } 139 } 140 141 const bunRoot = resolveGlobalRoot('bun', execImpl, env) 142 for (const name of CORE_PACKAGE_NAMES) { 143 const expectedReal = tryRealpath(path.join(bunRoot, name)) 144 if (path.resolve(expectedReal) === path.resolve(pkgReal)) return 'bun' 145 } 146 147 return null 148 } 149 150 function findLocalInstallProjectRoot(pkgRoot) { 151 const normalized = normalizeDir(pkgRoot) 152 if (!normalized) return null 153 154 const marker = `${path.sep}node_modules${path.sep}` 155 const idx = normalized.indexOf(marker) 156 if (idx === -1) return null 157 158 const projectRoot = normalized.slice(0, idx) 159 return projectRoot ? path.resolve(projectRoot) : path.parse(normalized).root 160 } 161 162 function resolveStateHome(opts = {}) { 163 const env = opts.env || process.env 164 const explicitHome = normalizeDir(env.SWARMCLAW_HOME) 165 if (explicitHome) return explicitHome 166 167 const pkgRoot = normalizeDir(opts.pkgRoot) 168 || resolvePackageRoot({ 169 moduleDir: opts.moduleDir, 170 argv1: opts.argv1, 171 cwd: opts.cwd, 172 }) 173 if (!pkgRoot) return path.join(os.homedir(), '.swarmclaw') 174 175 const execImpl = opts.execImpl || execFileSync 176 if (detectGlobalInstallManagerForRoot(pkgRoot, execImpl, env)) { 177 return path.join(os.homedir(), '.swarmclaw') 178 } 179 180 const projectRoot = findLocalInstallProjectRoot(pkgRoot) 181 if (projectRoot) return path.join(projectRoot, '.swarmclaw') 182 183 return path.join(os.homedir(), '.swarmclaw') 184 } 185 186 module.exports = { 187 PACKAGE_NAME, 188 candidateDirsFromArgv1, 189 detectGlobalInstallManagerForRoot, 190 findPackageRoot, 191 findLocalInstallProjectRoot, 192 readPackageName, 193 readPackageVersion, 194 resolveGlobalRoot, 195 resolvePackageRoot, 196 resolveStateHome, 197 }