@swarmclawai/swarmclaw 0.9.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ const fs = require('node:fs')
5
+ const os = require('node:os')
6
+ const path = require('node:path')
7
+ const { execFileSync } = require('node:child_process')
8
+
9
+ const PACKAGE_NAME = '@swarmclawai/swarmclaw'
10
+ const CORE_PACKAGE_NAMES = new Set([PACKAGE_NAME])
11
+
12
+ function normalizeDir(value) {
13
+ if (!value) return null
14
+ const trimmed = String(value).trim()
15
+ if (!trimmed) return null
16
+ return path.resolve(trimmed)
17
+ }
18
+
19
+ function readPackageJson(rootDir) {
20
+ try {
21
+ return JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'))
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ function readPackageName(rootDir) {
28
+ return readPackageJson(rootDir)?.name?.trim() || null
29
+ }
30
+
31
+ function readPackageVersion(rootDir) {
32
+ const version = readPackageJson(rootDir)?.version
33
+ return typeof version === 'string' && version.trim() ? version.trim() : null
34
+ }
35
+
36
+ function* iterAncestorDirs(startDir, maxDepth = 12) {
37
+ let current = path.resolve(startDir)
38
+ for (let i = 0; i < maxDepth; i += 1) {
39
+ yield current
40
+ const parent = path.dirname(current)
41
+ if (parent === current) break
42
+ current = parent
43
+ }
44
+ }
45
+
46
+ function findPackageRoot(startDir, maxDepth = 12) {
47
+ for (const current of iterAncestorDirs(startDir, maxDepth)) {
48
+ const name = readPackageName(current)
49
+ if (name && CORE_PACKAGE_NAMES.has(name)) return current
50
+ }
51
+ return null
52
+ }
53
+
54
+ function candidateDirsFromArgv1(argv1) {
55
+ const normalized = normalizeDir(argv1)
56
+ if (!normalized) return []
57
+
58
+ const candidates = [path.dirname(normalized)]
59
+ try {
60
+ const resolved = fs.realpathSync(normalized)
61
+ if (resolved !== normalized) candidates.push(path.dirname(resolved))
62
+ } catch {}
63
+
64
+ const parts = normalized.split(path.sep)
65
+ const binIndex = parts.lastIndexOf('.bin')
66
+ if (binIndex > 0 && parts[binIndex - 1] === 'node_modules') {
67
+ const binName = path.basename(normalized)
68
+ const nodeModulesDir = parts.slice(0, binIndex).join(path.sep)
69
+ candidates.push(path.join(nodeModulesDir, binName))
70
+ }
71
+
72
+ return candidates
73
+ }
74
+
75
+ function resolvePackageRoot(opts = {}) {
76
+ const candidates = []
77
+ const moduleDir = normalizeDir(opts.moduleDir)
78
+ if (moduleDir) candidates.push(moduleDir)
79
+ const argv1 = opts.argv1 === undefined ? process.argv[1] : opts.argv1
80
+ candidates.push(...candidateDirsFromArgv1(argv1))
81
+ const cwd = opts.cwd === undefined ? process.cwd() : opts.cwd
82
+ if (normalizeDir(cwd)) candidates.push(path.resolve(cwd))
83
+
84
+ for (const candidate of candidates) {
85
+ const found = findPackageRoot(candidate)
86
+ if (found) return found
87
+ }
88
+
89
+ return moduleDir ? path.resolve(moduleDir, '..') : null
90
+ }
91
+
92
+ function tryRealpath(targetPath) {
93
+ try {
94
+ return fs.realpathSync(targetPath)
95
+ } catch {
96
+ return path.resolve(targetPath)
97
+ }
98
+ }
99
+
100
+ function runRootCommand(command, args, execImpl = execFileSync) {
101
+ try {
102
+ return String(execImpl(command, args, {
103
+ encoding: 'utf8',
104
+ stdio: ['ignore', 'pipe', 'pipe'],
105
+ })).trim()
106
+ } catch {
107
+ return null
108
+ }
109
+ }
110
+
111
+ function resolveGlobalRoot(manager, execImpl = execFileSync, env = process.env) {
112
+ if (manager === 'bun') {
113
+ const bunInstall = String(env.BUN_INSTALL || '').trim() || path.join(os.homedir(), '.bun')
114
+ return path.join(bunInstall, 'install', 'global', 'node_modules')
115
+ }
116
+
117
+ if (manager === 'pnpm') {
118
+ return runRootCommand('pnpm', ['root', '-g'], execImpl)
119
+ }
120
+
121
+ return runRootCommand('npm', ['root', '-g'], execImpl)
122
+ }
123
+
124
+ function detectGlobalInstallManagerForRoot(pkgRoot, execImpl = execFileSync, env = process.env) {
125
+ const pkgReal = tryRealpath(pkgRoot)
126
+
127
+ for (const manager of ['npm', 'pnpm']) {
128
+ const globalRoot = resolveGlobalRoot(manager, execImpl, env)
129
+ if (!globalRoot) continue
130
+
131
+ for (const name of CORE_PACKAGE_NAMES) {
132
+ const expectedReal = tryRealpath(path.join(globalRoot, name))
133
+ if (path.resolve(expectedReal) === path.resolve(pkgReal)) return manager
134
+ }
135
+ }
136
+
137
+ const bunRoot = resolveGlobalRoot('bun', execImpl, env)
138
+ for (const name of CORE_PACKAGE_NAMES) {
139
+ const expectedReal = tryRealpath(path.join(bunRoot, name))
140
+ if (path.resolve(expectedReal) === path.resolve(pkgReal)) return 'bun'
141
+ }
142
+
143
+ return null
144
+ }
145
+
146
+ module.exports = {
147
+ PACKAGE_NAME,
148
+ candidateDirsFromArgv1,
149
+ detectGlobalInstallManagerForRoot,
150
+ findPackageRoot,
151
+ readPackageName,
152
+ readPackageVersion,
153
+ resolveGlobalRoot,
154
+ resolvePackageRoot,
155
+ }
@@ -0,0 +1,61 @@
1
+ 'use strict'
2
+ /* eslint-disable @typescript-eslint/no-require-imports */
3
+
4
+ const test = require('node:test')
5
+ const assert = require('node:assert/strict')
6
+ const fs = require('node:fs')
7
+ const os = require('node:os')
8
+ const path = require('node:path')
9
+
10
+ const {
11
+ candidateDirsFromArgv1,
12
+ detectGlobalInstallManagerForRoot,
13
+ resolvePackageRoot,
14
+ } = require('./install-root.js')
15
+
16
+ test('candidateDirsFromArgv1 includes the package directory for node_modules/.bin launchers', () => {
17
+ const launcher = path.join('/tmp', 'example', 'node_modules', '.bin', 'swarmclaw')
18
+ const candidates = candidateDirsFromArgv1(launcher)
19
+ assert.deepEqual(candidates, [
20
+ path.join('/tmp', 'example', 'node_modules', '.bin'),
21
+ path.join('/tmp', 'example', 'node_modules', 'swarmclaw'),
22
+ ])
23
+ })
24
+
25
+ test('resolvePackageRoot finds the package root from argv1 candidates', () => {
26
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-install-root-'))
27
+ const pkgRoot = path.join(rootDir, 'node_modules', '@swarmclawai', 'swarmclaw')
28
+ const binPath = path.join(rootDir, 'node_modules', '.bin', 'swarmclaw')
29
+ const actualBin = path.join(pkgRoot, 'bin', 'swarmclaw.js')
30
+
31
+ fs.mkdirSync(path.join(pkgRoot, 'bin'), { recursive: true })
32
+ fs.mkdirSync(path.dirname(binPath), { recursive: true })
33
+ fs.writeFileSync(path.join(pkgRoot, 'package.json'), JSON.stringify({ name: '@swarmclawai/swarmclaw' }), 'utf8')
34
+ fs.writeFileSync(actualBin, '#!/usr/bin/env node\n', 'utf8')
35
+ fs.symlinkSync(actualBin, binPath)
36
+
37
+ assert.equal(resolvePackageRoot({ argv1: binPath, cwd: rootDir }), fs.realpathSync(pkgRoot))
38
+
39
+ fs.rmSync(rootDir, { recursive: true, force: true })
40
+ })
41
+
42
+ test('detectGlobalInstallManagerForRoot matches the owning global root by realpath', () => {
43
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-global-root-'))
44
+ const npmGlobalRoot = path.join(rootDir, 'npm-global')
45
+ const pnpmGlobalRoot = path.join(rootDir, 'pnpm-global')
46
+ const pkgRoot = path.join(pnpmGlobalRoot, '@swarmclawai', 'swarmclaw')
47
+
48
+ fs.mkdirSync(path.join(npmGlobalRoot, '@swarmclawai'), { recursive: true })
49
+ fs.mkdirSync(path.join(pnpmGlobalRoot, '@swarmclawai'), { recursive: true })
50
+ fs.mkdirSync(pkgRoot, { recursive: true })
51
+
52
+ const execImpl = (command, args) => {
53
+ if (command === 'npm' && args.join(' ') === 'root -g') return npmGlobalRoot
54
+ if (command === 'pnpm' && args.join(' ') === 'root -g') return pnpmGlobalRoot
55
+ throw new Error(`unexpected command: ${command} ${args.join(' ')}`)
56
+ }
57
+
58
+ assert.equal(detectGlobalInstallManagerForRoot(pkgRoot, execImpl), 'pnpm')
59
+
60
+ fs.rmSync(rootDir, { recursive: true, force: true })
61
+ })
package/bin/server-cmd.js CHANGED
@@ -7,34 +7,28 @@ const path = require('node:path')
7
7
  const { spawn, execFileSync } = require('node:child_process')
8
8
  const os = require('node:os')
9
9
  const {
10
- LOCKFILE_NAMES,
11
10
  detectPackageManager,
12
11
  getInstallCommand,
13
12
  } = require('./package-manager.js')
13
+ const {
14
+ readPackageVersion,
15
+ resolvePackageRoot,
16
+ } = require('./install-root.js')
14
17
 
15
18
  // ---------------------------------------------------------------------------
16
19
  // Paths
17
20
  // ---------------------------------------------------------------------------
18
21
 
19
22
  const SWARMCLAW_HOME = process.env.SWARMCLAW_HOME || path.join(os.homedir(), '.swarmclaw')
20
- const PKG_ROOT = path.resolve(__dirname, '..')
21
- const BUILT_MARKER = path.join(SWARMCLAW_HOME, '.built')
23
+ const PKG_ROOT = resolvePackageRoot({
24
+ moduleDir: __dirname,
25
+ argv1: process.argv[1],
26
+ cwd: process.cwd(),
27
+ })
22
28
  const PID_FILE = path.join(SWARMCLAW_HOME, 'server.pid')
23
29
  const LOG_FILE = path.join(SWARMCLAW_HOME, 'server.log')
24
30
  const DATA_DIR = path.join(SWARMCLAW_HOME, 'data')
25
31
 
26
- // Files/directories to copy from the npm package into SWARMCLAW_HOME
27
- const BUILD_COPY_ENTRIES = [
28
- 'src',
29
- 'public',
30
- 'scripts',
31
- 'next.config.ts',
32
- 'tsconfig.json',
33
- 'postcss.config.mjs',
34
- 'package.json',
35
- ...LOCKFILE_NAMES,
36
- ]
37
-
38
32
  // ---------------------------------------------------------------------------
39
33
  // Helpers
40
34
  // ---------------------------------------------------------------------------
@@ -69,116 +63,69 @@ function isProcessRunning(pid) {
69
63
  }
70
64
  }
71
65
 
72
- function copyPath(src, dest, { dereference = true } = {}) {
73
- fs.rmSync(dest, { recursive: true, force: true })
74
- fs.cpSync(src, dest, { recursive: true, dereference })
66
+ function resolveStandaloneBase(pkgRoot = PKG_ROOT) {
67
+ return path.join(pkgRoot, '.next', 'standalone')
75
68
  }
76
69
 
77
- function symlinkPath(src, dest) {
78
- fs.rmSync(dest, { recursive: true, force: true })
79
- fs.symlinkSync(src, dest)
70
+ function getVersion() {
71
+ return readPackageVersion(PKG_ROOT) || 'unknown'
80
72
  }
81
73
 
82
- function readBuiltInfo() {
83
- if (!fs.existsSync(BUILT_MARKER)) return null
84
- try {
85
- const raw = JSON.parse(fs.readFileSync(BUILT_MARKER, 'utf8'))
86
- return raw && typeof raw === 'object' ? raw : null
87
- } catch {
88
- return null
89
- }
74
+ function ensurePackageDependencies(pkgRoot = PKG_ROOT) {
75
+ const nextCli = path.join(pkgRoot, 'node_modules', 'next', 'dist', 'bin', 'next')
76
+ if (fs.existsSync(nextCli)) return nextCli
77
+
78
+ const packageManager = detectPackageManager(pkgRoot, process.env)
79
+ const install = getInstallCommand(packageManager)
80
+ log(`Installing dependencies with ${packageManager}...`)
81
+ execFileSync(install.command, install.args, { cwd: pkgRoot, stdio: 'inherit' })
82
+ return nextCli
90
83
  }
91
84
 
92
85
  // ---------------------------------------------------------------------------
93
86
  // Build
94
87
  // ---------------------------------------------------------------------------
95
88
 
96
- function needsBuild(forceBuild) {
89
+ function needsBuild(forceBuild, { pkgRoot = PKG_ROOT } = {}) {
97
90
  if (forceBuild) return true
98
- const info = readBuiltInfo()
99
- if (!info) return true
100
- if (info.version !== getVersion()) return true
101
- if (!findStandaloneServer()) return true
102
- return false
91
+ return !findStandaloneServer({ pkgRoot })
103
92
  }
104
93
 
105
- function runBuild() {
94
+ function runBuild({ pkgRoot = PKG_ROOT } = {}) {
106
95
  log('Preparing build environment...')
107
96
  ensureDir(SWARMCLAW_HOME)
108
97
  ensureDir(DATA_DIR)
109
98
 
110
- // Copy source/config into SWARMCLAW_HOME. Turbopack build currently rejects
111
- // app source symlinks that point outside the workspace root.
112
- for (const entry of BUILD_COPY_ENTRIES) {
113
- const src = path.join(PKG_ROOT, entry)
114
- const dest = path.join(SWARMCLAW_HOME, entry)
115
-
116
- if (!fs.existsSync(src)) {
117
- log(`Warning: ${entry} not found in package, skipping`)
118
- continue
119
- }
120
-
121
- copyPath(src, dest)
122
- }
99
+ const nextCli = ensurePackageDependencies(pkgRoot)
123
100
 
124
- // Reuse package dependencies via symlink to avoid multi-GB duplication in
125
- // SWARMCLAW_HOME. Build runs with webpack mode for symlink compatibility.
126
- const nmSrc = path.join(PKG_ROOT, 'node_modules')
127
- const nmDest = path.join(SWARMCLAW_HOME, 'node_modules')
128
- if (fs.existsSync(nmSrc)) {
129
- symlinkPath(nmSrc, nmDest)
130
- } else {
131
- // If node_modules doesn't exist at PKG_ROOT, install
132
- const packageManager = detectPackageManager(SWARMCLAW_HOME, process.env)
133
- const install = getInstallCommand(packageManager)
134
- log(`Installing dependencies with ${packageManager}...`)
135
- execFileSync(install.command, install.args, { cwd: SWARMCLAW_HOME, stdio: 'inherit' })
136
- }
137
-
138
- // Run Next.js build
139
101
  log('Building Next.js application (this may take a minute)...')
140
- const nextCli = path.join(SWARMCLAW_HOME, 'node_modules', 'next', 'dist', 'bin', 'next')
141
- execFileSync(process.execPath, [nextCli, 'build', '--no-turbopack'], {
142
- cwd: SWARMCLAW_HOME,
102
+ execFileSync(process.execPath, [nextCli, 'build'], {
103
+ cwd: pkgRoot,
143
104
  stdio: 'inherit',
144
105
  env: {
145
106
  ...process.env,
107
+ DATA_DIR,
146
108
  SWARMCLAW_BUILD_MODE: '1',
147
109
  },
148
110
  })
149
111
 
150
- // Write built marker
151
- fs.writeFileSync(BUILT_MARKER, JSON.stringify({ builtAt: new Date().toISOString(), version: getVersion() }))
152
112
  log('Build complete.')
153
113
  }
154
114
 
155
- function getVersion() {
156
- try {
157
- const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'))
158
- return pkg.version || 'unknown'
159
- } catch {
160
- return 'unknown'
161
- }
162
- }
163
-
164
115
  // ---------------------------------------------------------------------------
165
116
  // Find standalone server.js
166
117
  // ---------------------------------------------------------------------------
167
118
 
168
- function findStandaloneServer() {
169
- // Next.js standalone output creates .next/standalone/ with server.js
170
- // The path mirrors the build machine's directory structure
171
- const standaloneBase = path.join(SWARMCLAW_HOME, '.next', 'standalone')
119
+ function findStandaloneServer({ pkgRoot = PKG_ROOT } = {}) {
120
+ const standaloneBase = resolveStandaloneBase(pkgRoot)
172
121
 
173
122
  if (!fs.existsSync(standaloneBase)) {
174
123
  return null
175
124
  }
176
125
 
177
- // Try direct server.js first
178
126
  const direct = path.join(standaloneBase, 'server.js')
179
127
  if (fs.existsSync(direct)) return direct
180
128
 
181
- // Recursively search for server.js (handles nested paths from build machine)
182
129
  function search(dir) {
183
130
  const entries = fs.readdirSync(dir, { withFileTypes: true })
184
131
  for (const entry of entries) {
@@ -199,34 +146,38 @@ function findStandaloneServer() {
199
146
  // Start server
200
147
  // ---------------------------------------------------------------------------
201
148
 
202
- function startServer(opts) {
203
- const serverJs = findStandaloneServer()
149
+ function startServer(opts, { pkgRoot = PKG_ROOT } = {}) {
150
+ const serverJs = findStandaloneServer({ pkgRoot })
204
151
  if (!serverJs) {
205
- logError('Standalone server.js not found. Try running: swarmclaw server --build')
152
+ logError('Standalone server.js not found in the installed package. Try running: swarmclaw server --build')
206
153
  process.exit(1)
207
154
  }
208
155
 
156
+ ensureDir(SWARMCLAW_HOME)
157
+ ensureDir(DATA_DIR)
158
+
209
159
  const port = opts.port || '3456'
210
160
  const wsPort = opts.wsPort || String(Number(port) + 1)
211
161
  const host = opts.host || '0.0.0.0'
212
162
 
213
163
  const env = {
214
164
  ...process.env,
165
+ DATA_DIR,
166
+ HOSTNAME: host,
215
167
  PORT: port,
216
168
  WS_PORT: wsPort,
217
- HOSTNAME: host,
218
- DATA_DIR,
219
169
  }
220
170
 
221
171
  log(`Starting server on ${host}:${port} (WebSocket: ${wsPort})...`)
172
+ log(`Package root: ${pkgRoot}`)
222
173
  log(`Data directory: ${DATA_DIR}`)
223
174
 
224
175
  if (opts.detach) {
225
- // Detached mode — run in background
226
176
  const logStream = fs.openSync(LOG_FILE, 'a')
227
177
  const child = spawn(process.execPath, [serverJs], {
228
- env,
178
+ cwd: pkgRoot,
229
179
  detached: true,
180
+ env,
230
181
  stdio: ['ignore', logStream, logStream],
231
182
  })
232
183
 
@@ -236,8 +187,8 @@ function startServer(opts) {
236
187
  log(`Logs: ${LOG_FILE}`)
237
188
  process.exit(0)
238
189
  } else {
239
- // Foreground mode
240
190
  const child = spawn(process.execPath, [serverJs], {
191
+ cwd: pkgRoot,
241
192
  env,
242
193
  stdio: 'inherit',
243
194
  })
@@ -246,7 +197,6 @@ function startServer(opts) {
246
197
  process.exit(code || 0)
247
198
  })
248
199
 
249
- // Forward signals
250
200
  for (const sig of ['SIGINT', 'SIGTERM']) {
251
201
  process.on(sig, () => child.kill(sig))
252
202
  }
@@ -288,27 +238,21 @@ function showStatus() {
288
238
  const pid = readPid()
289
239
  if (!pid) {
290
240
  log('Server: not running (no PID file)')
291
- return
292
- }
293
-
294
- if (isProcessRunning(pid)) {
241
+ } else if (isProcessRunning(pid)) {
295
242
  log(`Server: running (PID: ${pid})`)
296
243
  } else {
297
244
  log(`Server: not running (stale PID: ${pid})`)
298
245
  try { fs.unlinkSync(PID_FILE) } catch {}
299
246
  }
300
247
 
248
+ log(`Package: ${PKG_ROOT}`)
301
249
  log(`Home: ${SWARMCLAW_HOME}`)
302
250
  log(`Data: ${DATA_DIR}`)
303
251
  log(`WebSocket port: ${process.env.WS_PORT || '(PORT + 1)'}`)
304
252
 
305
- if (fs.existsSync(BUILT_MARKER)) {
306
- try {
307
- const info = JSON.parse(fs.readFileSync(BUILT_MARKER, 'utf8'))
308
- log(`Built: ${info.builtAt || 'unknown'} (v${info.version || '?'})`)
309
- } catch {
310
- log('Built: yes')
311
- }
253
+ const serverJs = findStandaloneServer()
254
+ if (serverJs) {
255
+ log(`Built: yes (${serverJs})`)
312
256
  } else {
313
257
  log('Built: no')
314
258
  }
@@ -339,7 +283,7 @@ Options:
339
283
  }
340
284
 
341
285
  function main() {
342
- const args = process.argv.slice(3) // skip node, bin, 'server'
286
+ const args = process.argv.slice(3)
343
287
  let command = 'start'
344
288
  let forceBuild = false
345
289
  let detach = false
@@ -385,7 +329,6 @@ function main() {
385
329
  return
386
330
  }
387
331
 
388
- // command === 'start'
389
332
  if (needsBuild(forceBuild)) {
390
333
  runBuild()
391
334
  }
@@ -398,8 +341,13 @@ if (require.main === module) {
398
341
  }
399
342
 
400
343
  module.exports = {
344
+ DATA_DIR,
345
+ PKG_ROOT,
346
+ SWARMCLAW_HOME,
347
+ findStandaloneServer,
401
348
  getVersion,
402
349
  main,
403
350
  needsBuild,
404
- readBuiltInfo,
351
+ resolveStandaloneBase,
352
+ runBuild,
405
353
  }
package/bin/update-cmd.js CHANGED
@@ -10,11 +10,19 @@ const {
10
10
  getGlobalUpdateSpec,
11
11
  getInstallCommand,
12
12
  } = require('./package-manager.js')
13
-
14
- const PKG_ROOT = path.resolve(__dirname, '..')
15
- const PACKAGE_NAME = '@swarmclawai/swarmclaw'
13
+ const {
14
+ PACKAGE_NAME,
15
+ detectGlobalInstallManagerForRoot,
16
+ resolvePackageRoot,
17
+ } = require('./install-root.js')
18
+
19
+ const PKG_ROOT = resolvePackageRoot({
20
+ moduleDir: __dirname,
21
+ argv1: process.argv[1],
22
+ cwd: process.cwd(),
23
+ })
16
24
  const RELEASE_TAG_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/
17
- const PACKAGE_MANAGER = detectPackageManager(PKG_ROOT)
25
+ const FALLBACK_PACKAGE_MANAGER = detectPackageManager(PKG_ROOT)
18
26
 
19
27
  function run(cmd) {
20
28
  return execSync(cmd, { encoding: 'utf-8', cwd: PKG_ROOT, timeout: 60_000 }).trim()
@@ -36,6 +44,12 @@ function getLatestStableTag() {
36
44
  return tags.find((t) => RELEASE_TAG_RE.test(t)) || null
37
45
  }
38
46
 
47
+ function resolveRegistryPackageManager(execImpl = execFileSync) {
48
+ return detectGlobalInstallManagerForRoot(PKG_ROOT, execImpl, process.env)
49
+ || detectPackageManager(PKG_ROOT, process.env)
50
+ || FALLBACK_PACKAGE_MANAGER
51
+ }
52
+
39
53
  function rebuildStandaloneServer(
40
54
  execImpl = execFileSync,
41
55
  logger = { log, logError },
@@ -58,7 +72,7 @@ function rebuildStandaloneServer(
58
72
  }
59
73
 
60
74
  function runRegistrySelfUpdate(
61
- packageManager = PACKAGE_MANAGER,
75
+ packageManager = resolveRegistryPackageManager(),
62
76
  execImpl = execFileSync,
63
77
  logger = { log, logError },
64
78
  rebuildImpl = execFileSync,
@@ -92,16 +106,15 @@ function main() {
92
106
  Usage: swarmclaw update
93
107
 
94
108
  If running from a git checkout, pull the latest SwarmClaw release tag.
95
- If running from a registry install, update the global package with ${PACKAGE_MANAGER}.
109
+ If running from a registry install, update the global package with its owning package manager.
96
110
  `.trim())
97
111
  process.exit(0)
98
112
  }
99
113
 
100
- // Verify we're in a git repo
101
114
  try {
102
115
  run('git rev-parse --git-dir')
103
116
  } catch {
104
- process.exit(runRegistrySelfUpdate(PACKAGE_MANAGER))
117
+ process.exit(runRegistrySelfUpdate())
105
118
  }
106
119
 
107
120
  const beforeRef = run('git rev-parse HEAD')
@@ -127,7 +140,6 @@ If running from a registry install, update the global package with ${PACKAGE_MAN
127
140
  process.exit(0)
128
141
  }
129
142
 
130
- // Check for uncommitted changes
131
143
  const dirty = run('git status --porcelain')
132
144
  if (dirty) {
133
145
  logError('Local changes detected. Commit or stash them first, then retry.')
@@ -138,7 +150,6 @@ If running from a registry install, update the global package with ${PACKAGE_MAN
138
150
  run(`git checkout -B stable ${latestTag}^{commit}`)
139
151
  pullOutput = `Updated to stable release ${latestTag}.`
140
152
  } else {
141
- // Fallback: pull from origin/main
142
153
  const behindCount = parseInt(run('git rev-list HEAD..origin/main --count'), 10) || 0
143
154
  if (behindCount === 0) {
144
155
  log(`Already up to date (${beforeSha}).`)
@@ -158,16 +169,21 @@ If running from a registry install, update the global package with ${PACKAGE_MAN
158
169
  const newSha = run('git rev-parse --short HEAD')
159
170
  log(pullOutput)
160
171
 
161
- // Install deps if package files changed
162
172
  try {
163
173
  const diff = run(`git diff --name-only ${beforeSha}..HEAD`)
164
174
  if (dependenciesChanged(diff)) {
165
- const install = getInstallCommand(PACKAGE_MANAGER, true)
166
- log(`Package files changed running ${PACKAGE_MANAGER} install...`)
175
+ const packageManager = detectPackageManager(PKG_ROOT, process.env)
176
+ const install = getInstallCommand(packageManager, true)
177
+ log(`Package files changed — running ${packageManager} install...`)
167
178
  execFileSync(install.command, install.args, { cwd: PKG_ROOT, stdio: 'inherit', timeout: 120_000 })
168
179
  }
169
180
  } catch {
170
- // If diff fails, skip install check
181
+ // If diff fails, skip install check.
182
+ }
183
+
184
+ const rebuildExitCode = rebuildStandaloneServer()
185
+ if (rebuildExitCode !== 0) {
186
+ process.exit(rebuildExitCode)
171
187
  }
172
188
 
173
189
  log(`Done (${beforeSha} → ${newSha}, channel: ${channel}).`)
@@ -180,5 +196,7 @@ if (require.main === module) {
180
196
 
181
197
  module.exports = {
182
198
  main,
199
+ rebuildStandaloneServer,
200
+ resolveRegistryPackageManager,
183
201
  runRegistrySelfUpdate,
184
202
  }
package/bin/worker-cmd.js CHANGED
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
3
 
4
- const fs = require('node:fs')
5
- const path = require('node:path')
6
- const os = require('node:os')
7
4
  const { spawn } = require('node:child_process')
8
5
 
6
+ const {
7
+ DATA_DIR,
8
+ PKG_ROOT,
9
+ SWARMCLAW_HOME,
10
+ findStandaloneServer,
11
+ } = require('./server-cmd.js')
12
+
9
13
  function printHelp() {
10
14
  const help = `
11
15
  Usage: swarmclaw worker [options]
@@ -32,27 +36,23 @@ function main() {
32
36
  }
33
37
  }
34
38
 
35
- const SWARMCLAW_HOME = process.env.SWARMCLAW_HOME || path.join(os.homedir(), '.swarmclaw')
36
- const DATA_DIR = path.join(SWARMCLAW_HOME, 'data')
37
-
38
39
  process.env.DATA_DIR = DATA_DIR
39
40
  process.env.SWARMCLAW_DAEMON_BACKGROUND_SERVICES = '1'
40
- // Flag that tells Next.js NOT to start the HTTP/Websocket listener, just boot the daemon.
41
41
  process.env.SWARMCLAW_WORKER_ONLY = '1'
42
42
 
43
- console.log(`[swarmclaw] Starting dedicated background worker...`)
43
+ console.log('[swarmclaw] Starting dedicated background worker...')
44
+ console.log(`[swarmclaw] Package root: ${PKG_ROOT}`)
45
+ console.log(`[swarmclaw] Home: ${SWARMCLAW_HOME}`)
44
46
  console.log(`[swarmclaw] Data directory: ${DATA_DIR}`)
45
47
 
46
- // We reuse the built server.js but signal it to only run the daemon
47
- const standaloneBase = path.join(SWARMCLAW_HOME, '.next', 'standalone')
48
- let serverJs = path.join(standaloneBase, 'server.js')
49
-
50
- if (!fs.existsSync(serverJs)) {
51
- console.error('Standalone server.js not found. Try running: swarmclaw server --build')
52
- process.exit(1)
48
+ const serverJs = findStandaloneServer()
49
+ if (!serverJs) {
50
+ console.error('[swarmclaw] Standalone server.js not found in the installed package. Try running: swarmclaw server --build')
51
+ process.exit(1)
53
52
  }
54
53
 
55
54
  const child = spawn(process.execPath, [serverJs], {
55
+ cwd: PKG_ROOT,
56
56
  env: process.env,
57
57
  stdio: 'inherit',
58
58
  })