@swarmclawai/swarmclaw 1.0.3 → 1.0.4

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.
package/bin/server-cmd.js CHANGED
@@ -3,6 +3,7 @@
3
3
  /* eslint-disable @typescript-eslint/no-require-imports */
4
4
 
5
5
  const fs = require('node:fs')
6
+ const http = require('node:http')
6
7
  const path = require('node:path')
7
8
  const { spawn, execFileSync } = require('node:child_process')
8
9
  const {
@@ -72,6 +73,62 @@ function isProcessRunning(pid) {
72
73
  }
73
74
  }
74
75
 
76
+ function resolveReadyCheckHost(host) {
77
+ if (host === '0.0.0.0') return '127.0.0.1'
78
+ if (host === '::') return '::1'
79
+ return host
80
+ }
81
+
82
+ function probeHttpReady(host, port, timeoutMs = 1_000) {
83
+ return new Promise((resolve) => {
84
+ const req = http.request(
85
+ {
86
+ host,
87
+ port: Number(port),
88
+ path: '/api/auth',
89
+ method: 'GET',
90
+ timeout: timeoutMs,
91
+ },
92
+ (res) => {
93
+ res.resume()
94
+ resolve(res.statusCode >= 200 && res.statusCode < 500)
95
+ },
96
+ )
97
+
98
+ req.once('timeout', () => {
99
+ req.destroy()
100
+ resolve(false)
101
+ })
102
+ req.once('error', () => resolve(false))
103
+ req.end()
104
+ })
105
+ }
106
+
107
+ async function waitForPortReady({
108
+ host,
109
+ port,
110
+ timeoutMs = 30_000,
111
+ intervalMs = 250,
112
+ pid = null,
113
+ isProcessRunningFn = isProcessRunning,
114
+ probeFn = probeHttpReady,
115
+ } = {}) {
116
+ const readyHost = resolveReadyCheckHost(host)
117
+ const deadline = Date.now() + timeoutMs
118
+
119
+ while (Date.now() < deadline) {
120
+ if (pid && !isProcessRunningFn(pid)) {
121
+ throw new Error(`Detached server process ${pid} exited before becoming ready.`)
122
+ }
123
+
124
+ if (await probeFn(readyHost, port)) return
125
+
126
+ await new Promise((resolve) => setTimeout(resolve, intervalMs))
127
+ }
128
+
129
+ throw new Error(`Timed out waiting for ${readyHost}:${port} to become ready.`)
130
+ }
131
+
75
132
  function resolveStandaloneBase(pkgRoot = PKG_ROOT) {
76
133
  return path.join(pkgRoot, '.next', 'standalone')
77
134
  }
@@ -241,7 +298,7 @@ function findStandaloneServer({ pkgRoot = PKG_ROOT } = {}) {
241
298
  // Start server
242
299
  // ---------------------------------------------------------------------------
243
300
 
244
- function startServer(opts, { pkgRoot = PKG_ROOT } = {}) {
301
+ async function startServer(opts, { pkgRoot = PKG_ROOT } = {}) {
245
302
  const standalone = locateStandaloneServer({ pkgRoot })
246
303
  if (!standalone) {
247
304
  logError('Standalone server.js not found in the installed package. Try running: swarmclaw server --build')
@@ -282,11 +339,22 @@ function startServer(opts, { pkgRoot = PKG_ROOT } = {}) {
282
339
  stdio: ['ignore', logStream, logStream],
283
340
  })
284
341
 
285
- child.unref()
286
342
  fs.writeFileSync(PID_FILE, String(child.pid))
287
- log(`Server started in background (PID: ${child.pid})`)
288
- log(`Logs: ${LOG_FILE}`)
289
- process.exit(0)
343
+ try {
344
+ await waitForPortReady({ host, port, pid: child.pid })
345
+ child.unref()
346
+ log(`Server started in background (PID: ${child.pid})`)
347
+ log(`Logs: ${LOG_FILE}`)
348
+ process.exit(0)
349
+ } catch (err) {
350
+ try {
351
+ if (isProcessRunning(child.pid)) process.kill(child.pid, 'SIGTERM')
352
+ } catch {}
353
+ try { fs.unlinkSync(PID_FILE) } catch {}
354
+ logError(`Detached start failed: ${err.message}`)
355
+ logError(`Check logs: ${LOG_FILE}`)
356
+ process.exit(1)
357
+ }
290
358
  } else {
291
359
  const child = spawn(process.execPath, [serverJs], {
292
360
  cwd: runtimeRoot,
@@ -386,7 +454,7 @@ Options:
386
454
  console.log(help)
387
455
  }
388
456
 
389
- function main(args = process.argv.slice(3)) {
457
+ async function main(args = process.argv.slice(3)) {
390
458
  let command = 'start'
391
459
  let forceBuild = false
392
460
  let detach = false
@@ -446,11 +514,14 @@ function main(args = process.argv.slice(3)) {
446
514
  }
447
515
  }
448
516
 
449
- startServer({ port, wsPort, host, detach })
517
+ await startServer({ port, wsPort, host, detach })
450
518
  }
451
519
 
452
520
  if (require.main === module) {
453
- main()
521
+ void main().catch((err) => {
522
+ logError(err?.message || String(err))
523
+ process.exit(1)
524
+ })
454
525
  }
455
526
 
456
527
  module.exports = {
@@ -469,7 +540,9 @@ module.exports = {
469
540
  prepareBuildWorkspace,
470
541
  resolveInstalledNext,
471
542
  resolvePackageBuildRoot,
543
+ resolveReadyCheckHost,
472
544
  resolveStandaloneCandidateRoots,
473
545
  resolveStandaloneBase,
474
546
  runBuild,
547
+ waitForPortReady,
475
548
  }
package/bin/swarmclaw.js CHANGED
@@ -128,7 +128,7 @@ async function runHelp(argv) {
128
128
  }
129
129
 
130
130
  if (target === 'run' || target === 'start' || target === 'stop' || target === 'status' || target === 'server') {
131
- require('./server-cmd.js').main(['--help'])
131
+ await require('./server-cmd.js').main(['--help'])
132
132
  return
133
133
  }
134
134
  if (target === 'worker') {
@@ -164,7 +164,7 @@ async function main() {
164
164
 
165
165
  // Default to 'server' when invoked with no arguments.
166
166
  if (!top) {
167
- require('./server-cmd.js').main([])
167
+ await require('./server-cmd.js').main([])
168
168
  return
169
169
  }
170
170
 
@@ -185,15 +185,15 @@ async function main() {
185
185
 
186
186
  // Route local lifecycle/maintenance commands to CJS scripts (no TS dependency).
187
187
  if (top === 'server') {
188
- require('./server-cmd.js').main(argv.slice(1))
188
+ await require('./server-cmd.js').main(argv.slice(1))
189
189
  return
190
190
  }
191
191
  if (top === 'run' || top === 'start') {
192
- require('./server-cmd.js').main(argv.slice(1))
192
+ await require('./server-cmd.js').main(argv.slice(1))
193
193
  return
194
194
  }
195
195
  if (top === 'status' || top === 'stop') {
196
- require('./server-cmd.js').main([top, ...argv.slice(1)])
196
+ await require('./server-cmd.js').main([top, ...argv.slice(1)])
197
197
  return
198
198
  }
199
199
  if (top === 'worker') {
@@ -217,7 +217,10 @@ async function main() {
217
217
  }
218
218
 
219
219
  if (require.main === module) {
220
- void main()
220
+ void main().catch((err) => {
221
+ process.stderr.write(`${err?.message || String(err)}\n`)
222
+ process.exit(1)
223
+ })
221
224
  }
222
225
 
223
226
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Self-hosted AI agent orchestration dashboard with OpenClaw integration, multi-provider support, LangGraph workflows, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -135,3 +135,56 @@ test('prepareBuildWorkspace copies the package tree and links node_modules outsi
135
135
  fs.rmSync(pkgRoot, { recursive: true, force: true })
136
136
  fs.rmSync(externalNodeModules, { recursive: true, force: true })
137
137
  })
138
+
139
+ test('resolveReadyCheckHost maps wildcard bind hosts to loopback', () => {
140
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
141
+ const serverCmd = loadServerCmdForHome(homeDir)
142
+
143
+ assert.equal(serverCmd.resolveReadyCheckHost('0.0.0.0'), '127.0.0.1')
144
+ assert.equal(serverCmd.resolveReadyCheckHost('::'), '::1')
145
+ assert.equal(serverCmd.resolveReadyCheckHost('127.0.0.1'), '127.0.0.1')
146
+
147
+ fs.rmSync(homeDir, { recursive: true, force: true })
148
+ })
149
+
150
+ test('waitForPortReady resolves once the readiness probe succeeds', async () => {
151
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
152
+ const serverCmd = loadServerCmdForHome(homeDir)
153
+ const calls = []
154
+ let attempts = 0
155
+
156
+ await serverCmd.waitForPortReady({
157
+ host: '0.0.0.0',
158
+ port: 3456,
159
+ timeoutMs: 1_000,
160
+ intervalMs: 10,
161
+ probeFn: async (host, port) => {
162
+ calls.push({ host, port })
163
+ attempts += 1
164
+ return attempts >= 3
165
+ },
166
+ })
167
+
168
+ assert.deepEqual(calls[0], { host: '127.0.0.1', port: 3456 })
169
+ assert.equal(calls.length, 3)
170
+ fs.rmSync(homeDir, { recursive: true, force: true })
171
+ })
172
+
173
+ test('waitForPortReady fails fast when the detached process exits before readiness', async () => {
174
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
175
+ const serverCmd = loadServerCmdForHome(homeDir)
176
+
177
+ await assert.rejects(
178
+ serverCmd.waitForPortReady({
179
+ host: '127.0.0.1',
180
+ port: 6553,
181
+ pid: 4242,
182
+ timeoutMs: 500,
183
+ intervalMs: 25,
184
+ isProcessRunningFn: () => false,
185
+ }),
186
+ /exited before becoming ready/,
187
+ )
188
+
189
+ fs.rmSync(homeDir, { recursive: true, force: true })
190
+ })