@swarmclawai/swarmclaw 0.9.9 → 1.0.2

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.
Files changed (44) hide show
  1. package/bin/doctor-cmd.js +149 -0
  2. package/bin/doctor-cmd.test.js +50 -0
  3. package/bin/install-root.js +194 -0
  4. package/bin/install-root.test.js +121 -0
  5. package/bin/server-cmd.js +90 -111
  6. package/bin/swarmclaw.js +83 -3
  7. package/bin/update-cmd.js +33 -20
  8. package/bin/update-cmd.test.js +1 -36
  9. package/bin/worker-cmd.js +23 -17
  10. package/next.config.ts +2 -0
  11. package/package.json +11 -10
  12. package/src/app/api/gateways/[id]/health/route.ts +2 -32
  13. package/src/app/api/gateways/health-route.test.ts +1 -1
  14. package/src/app/api/openclaw/dashboard-url/route.test.ts +166 -0
  15. package/src/app/api/openclaw/dashboard-url/route.ts +68 -0
  16. package/src/app/api/setup/check-provider/helpers.ts +28 -0
  17. package/src/app/api/setup/check-provider/route.test.ts +17 -1
  18. package/src/app/api/setup/check-provider/route.ts +29 -36
  19. package/src/app/api/tasks/import/github/helpers.ts +100 -0
  20. package/src/app/api/tasks/import/github/route.test.ts +1 -1
  21. package/src/app/api/tasks/import/github/route.ts +2 -92
  22. package/src/app/api/webhooks/[id]/helpers.ts +253 -0
  23. package/src/app/api/webhooks/[id]/route.ts +2 -243
  24. package/src/app/api/webhooks/route.test.ts +4 -2
  25. package/src/cli/binary.test.js +57 -0
  26. package/src/cli/index.js +14 -1
  27. package/src/cli/server-cmd.test.js +21 -20
  28. package/src/components/auth/setup-wizard/index.tsx +16 -0
  29. package/src/components/auth/setup-wizard/step-agents.tsx +34 -23
  30. package/src/components/auth/setup-wizard/step-connect.tsx +8 -0
  31. package/src/components/auth/setup-wizard/types.ts +2 -0
  32. package/src/components/auth/setup-wizard/utils.test.ts +79 -0
  33. package/src/components/chat/chat-header.tsx +45 -2
  34. package/src/lib/providers/openclaw-exports.test.ts +23 -0
  35. package/src/lib/providers/openclaw.ts +1 -1
  36. package/src/lib/server/data-dir.test.ts +35 -0
  37. package/src/lib/server/data-dir.ts +11 -0
  38. package/src/lib/server/openclaw/health.ts +30 -1
  39. package/src/lib/server/session-tools/file-send.test.ts +18 -2
  40. package/src/lib/server/session-tools/file.ts +11 -7
  41. package/src/lib/server/skills/skill-discovery.test.ts +34 -1
  42. package/src/lib/server/skills/skill-discovery.ts +9 -4
  43. package/src/lib/setup-defaults.test.ts +42 -0
  44. package/src/lib/setup-defaults.ts +1 -1
package/bin/server-cmd.js CHANGED
@@ -5,35 +5,37 @@
5
5
  const fs = require('node:fs')
6
6
  const path = require('node:path')
7
7
  const { spawn, execFileSync } = require('node:child_process')
8
- const os = require('node:os')
9
8
  const {
10
- LOCKFILE_NAMES,
11
9
  detectPackageManager,
12
10
  getInstallCommand,
13
11
  } = require('./package-manager.js')
12
+ const {
13
+ readPackageVersion,
14
+ resolvePackageRoot,
15
+ resolveStateHome,
16
+ } = require('./install-root.js')
14
17
 
15
18
  // ---------------------------------------------------------------------------
16
19
  // Paths
17
20
  // ---------------------------------------------------------------------------
18
21
 
19
- 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')
22
+ const PKG_ROOT = resolvePackageRoot({
23
+ moduleDir: __dirname,
24
+ argv1: process.argv[1],
25
+ cwd: process.cwd(),
26
+ })
27
+ const SWARMCLAW_HOME = resolveStateHome({
28
+ pkgRoot: PKG_ROOT,
29
+ moduleDir: __dirname,
30
+ argv1: process.argv[1],
31
+ cwd: process.cwd(),
32
+ env: process.env,
33
+ })
22
34
  const PID_FILE = path.join(SWARMCLAW_HOME, 'server.pid')
23
35
  const LOG_FILE = path.join(SWARMCLAW_HOME, 'server.log')
24
36
  const DATA_DIR = path.join(SWARMCLAW_HOME, 'data')
25
-
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
+ const WORKSPACE_DIR = path.join(SWARMCLAW_HOME, 'workspace')
38
+ const BROWSER_PROFILES_DIR = path.join(SWARMCLAW_HOME, 'browser-profiles')
37
39
 
38
40
  // ---------------------------------------------------------------------------
39
41
  // Helpers
@@ -69,116 +71,74 @@ function isProcessRunning(pid) {
69
71
  }
70
72
  }
71
73
 
72
- function copyPath(src, dest, { dereference = true } = {}) {
73
- fs.rmSync(dest, { recursive: true, force: true })
74
- fs.cpSync(src, dest, { recursive: true, dereference })
74
+ function resolveStandaloneBase(pkgRoot = PKG_ROOT) {
75
+ return path.join(pkgRoot, '.next', 'standalone')
75
76
  }
76
77
 
77
- function symlinkPath(src, dest) {
78
- fs.rmSync(dest, { recursive: true, force: true })
79
- fs.symlinkSync(src, dest)
78
+ function isGitCheckout(pkgRoot = PKG_ROOT) {
79
+ return fs.existsSync(path.join(pkgRoot, '.git'))
80
80
  }
81
81
 
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
- }
82
+ function getVersion() {
83
+ return readPackageVersion(PKG_ROOT) || 'unknown'
84
+ }
85
+
86
+ function ensurePackageDependencies(pkgRoot = PKG_ROOT) {
87
+ const nextCli = path.join(pkgRoot, 'node_modules', 'next', 'dist', 'bin', 'next')
88
+ if (fs.existsSync(nextCli)) return nextCli
89
+
90
+ const packageManager = detectPackageManager(pkgRoot, process.env)
91
+ const install = getInstallCommand(packageManager)
92
+ log(`Installing dependencies with ${packageManager}...`)
93
+ execFileSync(install.command, install.args, { cwd: pkgRoot, stdio: 'inherit' })
94
+ return nextCli
90
95
  }
91
96
 
92
97
  // ---------------------------------------------------------------------------
93
98
  // Build
94
99
  // ---------------------------------------------------------------------------
95
100
 
96
- function needsBuild(forceBuild) {
101
+ function needsBuild(forceBuild, { pkgRoot = PKG_ROOT } = {}) {
97
102
  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
103
+ return !findStandaloneServer({ pkgRoot })
103
104
  }
104
105
 
105
- function runBuild() {
106
+ function runBuild({ pkgRoot = PKG_ROOT } = {}) {
106
107
  log('Preparing build environment...')
107
108
  ensureDir(SWARMCLAW_HOME)
108
109
  ensureDir(DATA_DIR)
109
110
 
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
- }
123
-
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
- }
111
+ const nextCli = ensurePackageDependencies(pkgRoot)
137
112
 
138
- // Run Next.js build
139
113
  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'], {
142
- cwd: SWARMCLAW_HOME,
114
+ execFileSync(process.execPath, [nextCli, 'build', '--webpack'], {
115
+ cwd: pkgRoot,
143
116
  stdio: 'inherit',
144
117
  env: {
145
118
  ...process.env,
119
+ SWARMCLAW_HOME,
120
+ DATA_DIR,
146
121
  SWARMCLAW_BUILD_MODE: '1',
147
122
  },
148
123
  })
149
124
 
150
- // Write built marker
151
- fs.writeFileSync(BUILT_MARKER, JSON.stringify({ builtAt: new Date().toISOString(), version: getVersion() }))
152
125
  log('Build complete.')
153
126
  }
154
127
 
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
128
  // ---------------------------------------------------------------------------
165
129
  // Find standalone server.js
166
130
  // ---------------------------------------------------------------------------
167
131
 
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')
132
+ function findStandaloneServer({ pkgRoot = PKG_ROOT } = {}) {
133
+ const standaloneBase = resolveStandaloneBase(pkgRoot)
172
134
 
173
135
  if (!fs.existsSync(standaloneBase)) {
174
136
  return null
175
137
  }
176
138
 
177
- // Try direct server.js first
178
139
  const direct = path.join(standaloneBase, 'server.js')
179
140
  if (fs.existsSync(direct)) return direct
180
141
 
181
- // Recursively search for server.js (handles nested paths from build machine)
182
142
  function search(dir) {
183
143
  const entries = fs.readdirSync(dir, { withFileTypes: true })
184
144
  for (const entry of entries) {
@@ -199,34 +159,42 @@ function findStandaloneServer() {
199
159
  // Start server
200
160
  // ---------------------------------------------------------------------------
201
161
 
202
- function startServer(opts) {
203
- const serverJs = findStandaloneServer()
162
+ function startServer(opts, { pkgRoot = PKG_ROOT } = {}) {
163
+ const serverJs = findStandaloneServer({ pkgRoot })
204
164
  if (!serverJs) {
205
- logError('Standalone server.js not found. Try running: swarmclaw server --build')
165
+ logError('Standalone server.js not found in the installed package. Try running: swarmclaw server --build')
206
166
  process.exit(1)
207
167
  }
208
168
 
169
+ ensureDir(SWARMCLAW_HOME)
170
+ ensureDir(DATA_DIR)
171
+
209
172
  const port = opts.port || '3456'
210
173
  const wsPort = opts.wsPort || String(Number(port) + 1)
211
174
  const host = opts.host || '0.0.0.0'
212
175
 
213
176
  const env = {
214
177
  ...process.env,
178
+ SWARMCLAW_HOME,
179
+ DATA_DIR,
180
+ WORKSPACE_DIR,
181
+ BROWSER_PROFILES_DIR,
182
+ HOSTNAME: host,
215
183
  PORT: port,
216
184
  WS_PORT: wsPort,
217
- HOSTNAME: host,
218
- DATA_DIR,
219
185
  }
220
186
 
221
187
  log(`Starting server on ${host}:${port} (WebSocket: ${wsPort})...`)
188
+ log(`Package root: ${pkgRoot}`)
189
+ log(`Home: ${SWARMCLAW_HOME}`)
222
190
  log(`Data directory: ${DATA_DIR}`)
223
191
 
224
192
  if (opts.detach) {
225
- // Detached mode — run in background
226
193
  const logStream = fs.openSync(LOG_FILE, 'a')
227
194
  const child = spawn(process.execPath, [serverJs], {
228
- env,
195
+ cwd: pkgRoot,
229
196
  detached: true,
197
+ env,
230
198
  stdio: ['ignore', logStream, logStream],
231
199
  })
232
200
 
@@ -236,8 +204,8 @@ function startServer(opts) {
236
204
  log(`Logs: ${LOG_FILE}`)
237
205
  process.exit(0)
238
206
  } else {
239
- // Foreground mode
240
207
  const child = spawn(process.execPath, [serverJs], {
208
+ cwd: pkgRoot,
241
209
  env,
242
210
  stdio: 'inherit',
243
211
  })
@@ -246,7 +214,6 @@ function startServer(opts) {
246
214
  process.exit(code || 0)
247
215
  })
248
216
 
249
- // Forward signals
250
217
  for (const sig of ['SIGINT', 'SIGTERM']) {
251
218
  process.on(sig, () => child.kill(sig))
252
219
  }
@@ -288,27 +255,23 @@ function showStatus() {
288
255
  const pid = readPid()
289
256
  if (!pid) {
290
257
  log('Server: not running (no PID file)')
291
- return
292
- }
293
-
294
- if (isProcessRunning(pid)) {
258
+ } else if (isProcessRunning(pid)) {
295
259
  log(`Server: running (PID: ${pid})`)
296
260
  } else {
297
261
  log(`Server: not running (stale PID: ${pid})`)
298
262
  try { fs.unlinkSync(PID_FILE) } catch {}
299
263
  }
300
264
 
265
+ log(`Package: ${PKG_ROOT}`)
301
266
  log(`Home: ${SWARMCLAW_HOME}`)
302
267
  log(`Data: ${DATA_DIR}`)
268
+ log(`Workspace: ${WORKSPACE_DIR}`)
269
+ log(`Browser profiles: ${BROWSER_PROFILES_DIR}`)
303
270
  log(`WebSocket port: ${process.env.WS_PORT || '(PORT + 1)'}`)
304
271
 
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
- }
272
+ const serverJs = findStandaloneServer()
273
+ if (serverJs) {
274
+ log(`Built: yes (${serverJs})`)
312
275
  } else {
313
276
  log('Built: no')
314
277
  }
@@ -338,8 +301,7 @@ Options:
338
301
  console.log(help)
339
302
  }
340
303
 
341
- function main() {
342
- const args = process.argv.slice(3) // skip node, bin, 'server'
304
+ function main(args = process.argv.slice(3)) {
343
305
  let command = 'start'
344
306
  let forceBuild = false
345
307
  let detach = false
@@ -385,9 +347,18 @@ function main() {
385
347
  return
386
348
  }
387
349
 
388
- // command === 'start'
389
350
  if (needsBuild(forceBuild)) {
390
- runBuild()
351
+ if (!forceBuild) {
352
+ const installKind = isGitCheckout() ? 'checkout' : 'installed package'
353
+ log(`Standalone server bundle not found in this ${installKind}. Building locally...`)
354
+ }
355
+ try {
356
+ runBuild()
357
+ } catch (err) {
358
+ logError(`Build failed: ${err.message}`)
359
+ logError('Retry manually with: swarmclaw server --build')
360
+ process.exit(1)
361
+ }
391
362
  }
392
363
 
393
364
  startServer({ port, wsPort, host, detach })
@@ -398,8 +369,16 @@ if (require.main === module) {
398
369
  }
399
370
 
400
371
  module.exports = {
372
+ DATA_DIR,
373
+ BROWSER_PROFILES_DIR,
374
+ PKG_ROOT,
375
+ SWARMCLAW_HOME,
376
+ WORKSPACE_DIR,
377
+ findStandaloneServer,
401
378
  getVersion,
379
+ isGitCheckout,
402
380
  main,
403
381
  needsBuild,
404
- readBuiltInfo,
382
+ resolveStandaloneBase,
383
+ runBuild,
405
384
  }
package/bin/swarmclaw.js CHANGED
@@ -96,6 +96,19 @@ function normalizeLegacyCliEnv(env) {
96
96
  return nextEnv
97
97
  }
98
98
 
99
+ function printPackageVersion() {
100
+ const pkg = require('../package.json')
101
+ process.stdout.write(`${pkg.name || 'swarmclaw'} ${pkg.version || '0.0.0'}\n`)
102
+ }
103
+
104
+ function printVersionHelp() {
105
+ process.stdout.write(`
106
+ Usage: swarmclaw version
107
+
108
+ Show the installed SwarmClaw package version.
109
+ `.trim() + '\n')
110
+ }
111
+
99
112
  async function runMappedCli(argv) {
100
113
  const cliPath = path.join(__dirname, '..', 'src', 'cli', 'index.js')
101
114
  const cliModule = await import(cliPath)
@@ -106,25 +119,91 @@ async function runMappedCli(argv) {
106
119
  return runCli(argv)
107
120
  }
108
121
 
122
+ async function runHelp(argv) {
123
+ const [target, ...rest] = argv
124
+ if (!target) {
125
+ const code = await runMappedCli(['--help'])
126
+ process.exitCode = typeof code === 'number' ? code : 1
127
+ return
128
+ }
129
+
130
+ if (target === 'run' || target === 'start' || target === 'stop' || target === 'status' || target === 'server') {
131
+ require('./server-cmd.js').main(['--help'])
132
+ return
133
+ }
134
+ if (target === 'worker') {
135
+ require('./worker-cmd.js').main(['--help'])
136
+ return
137
+ }
138
+ if (target === 'doctor') {
139
+ require('./doctor-cmd.js').main(['--help'])
140
+ return
141
+ }
142
+ if (target === 'update') {
143
+ require('./update-cmd.js').main(['--help'])
144
+ return
145
+ }
146
+ if (target === 'version') {
147
+ printVersionHelp()
148
+ return
149
+ }
150
+
151
+ const forwarded = rest.includes('--help') || rest.includes('-h')
152
+ ? [target, ...rest]
153
+ : [target, ...rest, '--help']
154
+ const code = shouldUseLegacyTsCli(forwarded)
155
+ ? runLegacyTsCli(forwarded)
156
+ : await runMappedCli(forwarded)
157
+
158
+ process.exitCode = typeof code === 'number' ? code : 1
159
+ }
160
+
109
161
  async function main() {
110
162
  const argv = process.argv.slice(2)
111
163
  const top = argv[0]
112
164
 
113
165
  // Default to 'server' when invoked with no arguments.
114
166
  if (!top) {
115
- require('./server-cmd.js').main()
167
+ require('./server-cmd.js').main([])
168
+ return
169
+ }
170
+
171
+ if (top === '-v') {
172
+ printPackageVersion()
173
+ return
174
+ }
175
+
176
+ if (top === 'version' && argv.length === 1) {
177
+ printPackageVersion()
116
178
  return
117
179
  }
118
180
 
119
- // Route 'server', 'worker', and 'update' subcommands to CJS scripts (no TS dependency).
181
+ if (top === 'help') {
182
+ await runHelp(argv.slice(1))
183
+ return
184
+ }
185
+
186
+ // Route local lifecycle/maintenance commands to CJS scripts (no TS dependency).
120
187
  if (top === 'server') {
121
- require('./server-cmd.js').main()
188
+ require('./server-cmd.js').main(argv.slice(1))
189
+ return
190
+ }
191
+ if (top === 'run' || top === 'start') {
192
+ require('./server-cmd.js').main(argv.slice(1))
193
+ return
194
+ }
195
+ if (top === 'status' || top === 'stop') {
196
+ require('./server-cmd.js').main([top, ...argv.slice(1)])
122
197
  return
123
198
  }
124
199
  if (top === 'worker') {
125
200
  require('./worker-cmd.js').main()
126
201
  return
127
202
  }
203
+ if (top === 'doctor') {
204
+ require('./doctor-cmd.js').main(argv.slice(1))
205
+ return
206
+ }
128
207
  if (top === 'update') {
129
208
  require('./update-cmd.js').main()
130
209
  return
@@ -146,6 +225,7 @@ module.exports = {
146
225
  hasTsxRuntime,
147
226
  TS_CLI_ACTIONS,
148
227
  normalizeLegacyCliEnv,
228
+ printPackageVersion,
149
229
  supportsStripTypes,
150
230
  shouldUseLegacyTsCli,
151
231
  }
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,10 +72,9 @@ 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
- rebuildImpl = execFileSync,
65
78
  ) {
66
79
  const update = getGlobalUpdateSpec(packageManager, PACKAGE_NAME)
67
80
  logger.log(`No git checkout detected. Updating the global ${PACKAGE_NAME} install via ${packageManager}...`)
@@ -78,30 +91,25 @@ function runRegistrySelfUpdate(
78
91
  return 1
79
92
  }
80
93
 
81
- const rebuildExitCode = rebuildStandaloneServer(rebuildImpl, logger)
82
- if (rebuildExitCode !== 0) return rebuildExitCode
83
-
84
94
  logger.log('Restart the server to apply changes: swarmclaw server stop && swarmclaw server start')
85
95
  return 0
86
96
  }
87
97
 
88
- function main() {
89
- const args = process.argv.slice(3)
98
+ function main(args = process.argv.slice(3)) {
90
99
  if (args.includes('-h') || args.includes('--help')) {
91
100
  console.log(`
92
101
  Usage: swarmclaw update
93
102
 
94
103
  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}.
104
+ If running from a registry install, update the global package with its owning package manager.
96
105
  `.trim())
97
106
  process.exit(0)
98
107
  }
99
108
 
100
- // Verify we're in a git repo
101
109
  try {
102
110
  run('git rev-parse --git-dir')
103
111
  } catch {
104
- process.exit(runRegistrySelfUpdate(PACKAGE_MANAGER))
112
+ process.exit(runRegistrySelfUpdate())
105
113
  }
106
114
 
107
115
  const beforeRef = run('git rev-parse HEAD')
@@ -127,7 +135,6 @@ If running from a registry install, update the global package with ${PACKAGE_MAN
127
135
  process.exit(0)
128
136
  }
129
137
 
130
- // Check for uncommitted changes
131
138
  const dirty = run('git status --porcelain')
132
139
  if (dirty) {
133
140
  logError('Local changes detected. Commit or stash them first, then retry.')
@@ -138,7 +145,6 @@ If running from a registry install, update the global package with ${PACKAGE_MAN
138
145
  run(`git checkout -B stable ${latestTag}^{commit}`)
139
146
  pullOutput = `Updated to stable release ${latestTag}.`
140
147
  } else {
141
- // Fallback: pull from origin/main
142
148
  const behindCount = parseInt(run('git rev-list HEAD..origin/main --count'), 10) || 0
143
149
  if (behindCount === 0) {
144
150
  log(`Already up to date (${beforeSha}).`)
@@ -158,16 +164,21 @@ If running from a registry install, update the global package with ${PACKAGE_MAN
158
164
  const newSha = run('git rev-parse --short HEAD')
159
165
  log(pullOutput)
160
166
 
161
- // Install deps if package files changed
162
167
  try {
163
168
  const diff = run(`git diff --name-only ${beforeSha}..HEAD`)
164
169
  if (dependenciesChanged(diff)) {
165
- const install = getInstallCommand(PACKAGE_MANAGER, true)
166
- log(`Package files changed running ${PACKAGE_MANAGER} install...`)
170
+ const packageManager = detectPackageManager(PKG_ROOT, process.env)
171
+ const install = getInstallCommand(packageManager, true)
172
+ log(`Package files changed — running ${packageManager} install...`)
167
173
  execFileSync(install.command, install.args, { cwd: PKG_ROOT, stdio: 'inherit', timeout: 120_000 })
168
174
  }
169
175
  } catch {
170
- // If diff fails, skip install check
176
+ // If diff fails, skip install check.
177
+ }
178
+
179
+ const rebuildExitCode = rebuildStandaloneServer()
180
+ if (rebuildExitCode !== 0) {
181
+ process.exit(rebuildExitCode)
171
182
  }
172
183
 
173
184
  log(`Done (${beforeSha} → ${newSha}, channel: ${channel}).`)
@@ -180,5 +191,7 @@ if (require.main === module) {
180
191
 
181
192
  module.exports = {
182
193
  main,
194
+ rebuildStandaloneServer,
195
+ resolveRegistryPackageManager,
183
196
  runRegistrySelfUpdate,
184
197
  }
@@ -3,11 +3,10 @@
3
3
 
4
4
  const test = require('node:test')
5
5
  const assert = require('node:assert/strict')
6
- const path = require('node:path')
7
6
 
8
7
  const { runRegistrySelfUpdate } = require('./update-cmd.js')
9
8
 
10
- test('runRegistrySelfUpdate executes the manager-specific global update command and rebuilds the standalone server', () => {
9
+ test('runRegistrySelfUpdate executes the manager-specific global update command', () => {
11
10
  const messages = []
12
11
  const captured = []
13
12
 
@@ -20,9 +19,6 @@ test('runRegistrySelfUpdate executes the manager-specific global update command
20
19
  log: (message) => messages.push(`log:${message}`),
21
20
  logError: (message) => messages.push(`err:${message}`),
22
21
  },
23
- (command, args, options) => {
24
- captured.push({ command, args, options })
25
- },
26
22
  )
27
23
 
28
24
  assert.equal(exitCode, 0)
@@ -36,20 +32,9 @@ test('runRegistrySelfUpdate executes the manager-specific global update command
36
32
  timeout: 120_000,
37
33
  },
38
34
  },
39
- {
40
- command: process.execPath,
41
- args: [path.join(process.cwd(), 'bin', 'server-cmd.js'), '--build'],
42
- options: {
43
- cwd: process.cwd(),
44
- stdio: 'inherit',
45
- timeout: 600_000,
46
- },
47
- },
48
35
  ])
49
36
  assert.match(messages.join('\n'), /updating the global @swarmclawai\/swarmclaw install via pnpm/i)
50
37
  assert.match(messages.join('\n'), /global update complete via pnpm/i)
51
- assert.match(messages.join('\n'), /rebuilding the standalone server bundle/i)
52
- assert.match(messages.join('\n'), /standalone server bundle rebuilt/i)
53
38
  })
54
39
 
55
40
  test('runRegistrySelfUpdate reports a manual retry command when the registry update fails', () => {
@@ -70,23 +55,3 @@ test('runRegistrySelfUpdate reports a manual retry command when the registry upd
70
55
  assert.match(messages.join('\n'), /registry update failed: spawn bun ENOENT/i)
71
56
  assert.match(messages.join('\n'), /retry manually with: bun add -g @swarmclawai\/swarmclaw@latest/i)
72
57
  })
73
-
74
- test('runRegistrySelfUpdate reports a manual rebuild command when the rebuild step fails', () => {
75
- const messages = []
76
-
77
- const exitCode = runRegistrySelfUpdate(
78
- 'npm',
79
- () => {},
80
- {
81
- log: (message) => messages.push(`log:${message}`),
82
- logError: (message) => messages.push(`err:${message}`),
83
- },
84
- () => {
85
- throw new Error('build failed')
86
- },
87
- )
88
-
89
- assert.equal(exitCode, 1)
90
- assert.match(messages.join('\n'), /standalone rebuild failed: build failed/i)
91
- assert.match(messages.join('\n'), /retry manually with: swarmclaw server --build/i)
92
- })