@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 +81 -8
- package/bin/swarmclaw.js +9 -6
- package/package.json +1 -1
- package/src/cli/server-cmd.test.js +53 -0
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
"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
|
+
})
|