update-cmd.js
1 #!/usr/bin/env node 2 'use strict' 3 /* eslint-disable @typescript-eslint/no-require-imports */ 4 5 const { execSync, execFileSync } = require('node:child_process') 6 const path = require('node:path') 7 const { 8 dependenciesChanged, 9 detectPackageManager, 10 getGlobalUpdateSpec, 11 getInstallCommand, 12 } = require('./package-manager.js') 13 const { 14 PACKAGE_NAME, 15 detectGlobalInstallManagerForRoot, 16 resolvePackageRoot, 17 } = require('./install-root.js') 18 19 const isWindows = process.platform === 'win32' 20 21 const PKG_ROOT = resolvePackageRoot({ 22 moduleDir: __dirname, 23 argv1: process.argv[1], 24 cwd: process.cwd(), 25 }) 26 const RELEASE_TAG_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/ 27 const FALLBACK_PACKAGE_MANAGER = detectPackageManager(PKG_ROOT) 28 29 function run(cmd) { 30 return execSync(cmd, { encoding: 'utf-8', cwd: PKG_ROOT, timeout: 60_000 }).trim() 31 } 32 33 function log(msg) { 34 process.stdout.write(`[swarmclaw] ${msg}\n`) 35 } 36 37 function logError(msg) { 38 process.stderr.write(`[swarmclaw] ${msg}\n`) 39 } 40 41 function getLatestStableTag() { 42 const tags = run("git tag --list 'v*' --sort=-v:refname") 43 .split('\n') 44 .map((l) => l.trim()) 45 .filter(Boolean) 46 return tags.find((t) => RELEASE_TAG_RE.test(t)) || null 47 } 48 49 function resolveRegistryPackageManager(execImpl = execFileSync) { 50 return detectGlobalInstallManagerForRoot(PKG_ROOT, execImpl, process.env) 51 || detectPackageManager(PKG_ROOT, process.env) 52 || FALLBACK_PACKAGE_MANAGER 53 } 54 55 function rebuildStandaloneServer( 56 execImpl = execFileSync, 57 logger = { log, logError }, 58 ) { 59 const serverCmdPath = path.join(PKG_ROOT, 'bin', 'server-cmd.js') 60 logger.log('Rebuilding the standalone server bundle...') 61 try { 62 execImpl(process.execPath, [serverCmdPath, '--build'], { 63 cwd: PKG_ROOT, 64 stdio: 'inherit', 65 timeout: 10 * 60_000, 66 }) 67 logger.log('Standalone server bundle rebuilt.') 68 return 0 69 } catch (err) { 70 logger.logError(`Standalone rebuild failed: ${err.message}`) 71 logger.logError('Retry manually with: swarmclaw server --build') 72 return 1 73 } 74 } 75 76 function runRegistrySelfUpdate( 77 packageManager = resolveRegistryPackageManager(), 78 execImpl = execFileSync, 79 logger = { log, logError }, 80 ) { 81 const update = getGlobalUpdateSpec(packageManager, PACKAGE_NAME) 82 logger.log(`No git checkout detected. Updating the global ${PACKAGE_NAME} install via ${packageManager}...`) 83 try { 84 execImpl(update.command, update.args, { 85 cwd: PKG_ROOT, 86 stdio: 'inherit', 87 timeout: 120_000, 88 ...(isWindows && { shell: true }), 89 }) 90 logger.log(`Global update complete via ${packageManager}.`) 91 } catch (err) { 92 logger.logError(`Registry update failed: ${err.message}`) 93 logger.logError(`Retry manually with: ${update.display}`) 94 return 1 95 } 96 97 logger.log('Restart the server to apply changes: swarmclaw server stop && swarmclaw server start') 98 return 0 99 } 100 101 function stopRunningServer(logger = { log, logError }) { 102 // Stop the server gracefully before updating to prevent SQLite WAL corruption. 103 try { 104 const serverCmd = path.join(PKG_ROOT, 'bin', 'swarmclaw.js') 105 const status = execFileSync(process.execPath, [serverCmd, 'status'], { 106 encoding: 'utf-8', 107 cwd: PKG_ROOT, 108 timeout: 10_000, 109 }).trim() 110 if (status.toLowerCase().includes('running')) { 111 logger.log('Stopping running server before update...') 112 execFileSync(process.execPath, [serverCmd, 'stop'], { 113 encoding: 'utf-8', 114 cwd: PKG_ROOT, 115 timeout: 15_000, 116 }) 117 logger.log('Server stopped.') 118 } 119 } catch { 120 // Server may not be running or status command unavailable — continue. 121 } 122 } 123 124 function main(args = process.argv.slice(3)) { 125 if (args.includes('-h') || args.includes('--help')) { 126 console.log(` 127 Usage: swarmclaw update 128 129 If running from a git checkout, pull the latest SwarmClaw release tag. 130 If running from a registry install, update the global package with its owning package manager. 131 `.trim()) 132 process.exit(0) 133 } 134 135 try { 136 run('git rev-parse --git-dir') 137 } catch { 138 stopRunningServer() 139 process.exit(runRegistrySelfUpdate()) 140 } 141 142 stopRunningServer() 143 144 const beforeRef = run('git rev-parse HEAD') 145 const beforeSha = run('git rev-parse --short HEAD') 146 147 log('Fetching latest releases...') 148 try { 149 run('git fetch --tags origin --quiet') 150 } catch (err) { 151 logError(`Fetch failed: ${err.message}`) 152 process.exit(1) 153 } 154 155 const latestTag = getLatestStableTag() 156 let channel = 'main' 157 let pullOutput = '' 158 159 if (latestTag) { 160 channel = 'stable' 161 const targetSha = run(`git rev-parse ${latestTag}^{commit}`) 162 if (targetSha === beforeRef) { 163 log(`Already up to date (${latestTag}, ${beforeSha}).`) 164 process.exit(0) 165 } 166 167 const dirty = run('git status --porcelain') 168 if (dirty) { 169 logError('Local changes detected. Commit or stash them first, then retry.') 170 process.exit(1) 171 } 172 173 log(`Updating to ${latestTag}...`) 174 run(`git checkout -B stable ${latestTag}^{commit}`) 175 pullOutput = `Updated to stable release ${latestTag}.` 176 } else { 177 const behindCount = parseInt(run('git rev-list HEAD..origin/main --count'), 10) || 0 178 if (behindCount === 0) { 179 log(`Already up to date (${beforeSha}).`) 180 process.exit(0) 181 } 182 183 const dirty = run('git status --porcelain') 184 if (dirty) { 185 logError('Local changes detected. Commit or stash them first, then retry.') 186 process.exit(1) 187 } 188 189 log(`Pulling ${behindCount} commit(s) from origin/main...`) 190 pullOutput = run('git pull --ff-only origin main') 191 } 192 193 const newSha = run('git rev-parse --short HEAD') 194 log(pullOutput) 195 196 try { 197 const diff = run(`git diff --name-only ${beforeSha}..HEAD`) 198 if (dependenciesChanged(diff)) { 199 const packageManager = detectPackageManager(PKG_ROOT, process.env) 200 const install = getInstallCommand(packageManager, true) 201 log(`Package files changed — running ${packageManager} install...`) 202 execFileSync(install.command, install.args, { cwd: PKG_ROOT, stdio: 'inherit', timeout: 120_000, ...(isWindows && { shell: true }) }) 203 } 204 } catch { 205 // If diff fails, skip install check. 206 } 207 208 const rebuildExitCode = rebuildStandaloneServer() 209 if (rebuildExitCode !== 0) { 210 process.exit(rebuildExitCode) 211 } 212 213 log(`Done (${beforeSha} → ${newSha}, channel: ${channel}).`) 214 log('Restart the server to apply changes: swarmclaw server stop && swarmclaw server start') 215 } 216 217 if (require.main === module) { 218 main() 219 } 220 221 module.exports = { 222 main, 223 rebuildStandaloneServer, 224 resolveRegistryPackageManager, 225 runRegistrySelfUpdate, 226 }