/ bin / install-root.js
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  }