openclaw-spawn 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.
- package/Dockerfile +45 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +46 -0
- package/README.md +237 -0
- package/bin/openclaw-spawn.js +5 -0
- package/cleanup.sh +30 -0
- package/package.json +20 -0
- package/src/cli.js +629 -0
- package/src/docker.js +272 -0
- package/src/metadata.js +166 -0
- package/src/selector.js +72 -0
- package/test/verify.js +57 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { execSync, spawnSync } from 'child_process';
|
|
5
|
+
import {
|
|
6
|
+
getInstances,
|
|
7
|
+
getInstance,
|
|
8
|
+
addInstance,
|
|
9
|
+
removeInstance as removeInstanceMetadata,
|
|
10
|
+
getNextPort,
|
|
11
|
+
getInstanceGatewayToken,
|
|
12
|
+
setInstanceGatewayPort,
|
|
13
|
+
enableBrowserTool,
|
|
14
|
+
enableBrowserTakeover
|
|
15
|
+
} from './metadata.js';
|
|
16
|
+
import {
|
|
17
|
+
isDockerRunning,
|
|
18
|
+
isDockerInstalled,
|
|
19
|
+
imageExists,
|
|
20
|
+
getContainerStatus,
|
|
21
|
+
createContainer,
|
|
22
|
+
execInContainer,
|
|
23
|
+
stopContainer,
|
|
24
|
+
startContainer,
|
|
25
|
+
removeContainer,
|
|
26
|
+
showLogs,
|
|
27
|
+
buildBaseImage,
|
|
28
|
+
ensureNetwork,
|
|
29
|
+
startVnc,
|
|
30
|
+
startVisibleChrome,
|
|
31
|
+
stopVisibleChrome
|
|
32
|
+
} from './docker.js';
|
|
33
|
+
import { selectInstance, promptInstanceName, confirm } from './selector.js';
|
|
34
|
+
|
|
35
|
+
// Management commands handled by openclaw-spawn itself
|
|
36
|
+
const MANAGEMENT_COMMANDS = ['list', 'remove', 'stop', 'start', 'logs', 'build', 'cleanup'];
|
|
37
|
+
|
|
38
|
+
// CLI entry point
|
|
39
|
+
export async function cli(args) {
|
|
40
|
+
const command = args[0];
|
|
41
|
+
|
|
42
|
+
// init runs its own Docker checks — must come before the global Docker running check
|
|
43
|
+
if (command === 'init') {
|
|
44
|
+
return await initCommand();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check Docker
|
|
48
|
+
if (!isDockerRunning()) {
|
|
49
|
+
console.error(chalk.red('✗ Docker is not running. Please start Docker Desktop.'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Management commands
|
|
54
|
+
if (command === 'list') {
|
|
55
|
+
return await listInstances();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (command === 'remove') {
|
|
59
|
+
const name = args[1];
|
|
60
|
+
if (!name) {
|
|
61
|
+
console.error(chalk.red('✗ Please specify instance name'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
return await removeInstanceCommand(name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (command === 'stop') {
|
|
68
|
+
const name = args[1];
|
|
69
|
+
if (!name) {
|
|
70
|
+
console.error(chalk.red('✗ Please specify instance name'));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
return await stopInstanceCommand(name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (command === 'start') {
|
|
77
|
+
const name = args[1];
|
|
78
|
+
if (!name) {
|
|
79
|
+
console.error(chalk.red('✗ Please specify instance name'));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
return await startInstanceCommand(name);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (command === 'logs') {
|
|
86
|
+
const name = args[1];
|
|
87
|
+
const follow = args.includes('-f') || args.includes('--follow');
|
|
88
|
+
if (!name) {
|
|
89
|
+
console.error(chalk.red('✗ Please specify instance name'));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
return await showLogsCommand(name, follow);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (command === 'build') {
|
|
96
|
+
return await buildCommand();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (command === 'cleanup') {
|
|
100
|
+
return await cleanupCommand();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (command === 'browser') {
|
|
104
|
+
if (args[1] === 'stop') {
|
|
105
|
+
return await browserStopCommand(args[2]);
|
|
106
|
+
}
|
|
107
|
+
return await browserCommand(args[1]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// All other commands: route to OpenClaw in container
|
|
111
|
+
const detach = args.includes('-d') || args.includes('--detach');
|
|
112
|
+
const filteredArgs = args.filter(a => a !== '-d' && a !== '--detach');
|
|
113
|
+
return await proxyOpenClawCommand(filteredArgs, detach);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Smart first-time setup wizard
|
|
117
|
+
async function initCommand() {
|
|
118
|
+
console.log(chalk.blue.bold('\n🦞 OpenClaw Swarm — Setup Wizard\n'));
|
|
119
|
+
|
|
120
|
+
// ── Step 1: Docker installed? ─────────────────────────────────────────────
|
|
121
|
+
console.log(chalk.bold('Step 1/5 Check Docker is installed'));
|
|
122
|
+
if (!isDockerInstalled()) {
|
|
123
|
+
console.log(chalk.yellow(' Docker not found. Installing...\n'));
|
|
124
|
+
try {
|
|
125
|
+
if (process.platform === 'darwin') {
|
|
126
|
+
// Check for Homebrew
|
|
127
|
+
try {
|
|
128
|
+
execSync('which brew', { stdio: 'ignore' });
|
|
129
|
+
} catch {
|
|
130
|
+
console.log(chalk.red(' Homebrew is required to auto-install Docker on macOS.'));
|
|
131
|
+
console.log(chalk.yellow(' Install Homebrew first: https://brew.sh'));
|
|
132
|
+
console.log(chalk.yellow(' Then re-run: openclaw-spawn init\n'));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
execSync('brew install --cask docker', { stdio: 'inherit' });
|
|
136
|
+
} else if (process.platform === 'linux') {
|
|
137
|
+
// Try apt, fall back to yum
|
|
138
|
+
try {
|
|
139
|
+
execSync('which apt-get', { stdio: 'ignore' });
|
|
140
|
+
execSync('sudo apt-get update && sudo apt-get install -y docker.io', { stdio: 'inherit' });
|
|
141
|
+
execSync('sudo systemctl start docker && sudo systemctl enable docker', { stdio: 'inherit' });
|
|
142
|
+
} catch {
|
|
143
|
+
execSync('sudo yum install -y docker', { stdio: 'inherit' });
|
|
144
|
+
execSync('sudo systemctl start docker && sudo systemctl enable docker', { stdio: 'inherit' });
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
console.log(chalk.yellow(' Auto-install is not supported on Windows.'));
|
|
148
|
+
console.log(chalk.yellow(' Download Docker Desktop: https://docs.docker.com/desktop/windows/'));
|
|
149
|
+
console.log(chalk.yellow(' Then re-run: openclaw-spawn init\n'));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
console.log(chalk.green(' ✓ Docker installed\n'));
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(chalk.red(` ✗ Failed to install Docker: ${err.message}`));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
console.log(chalk.green(' ✓ Docker is installed\n'));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Step 2: Docker running? ───────────────────────────────────────────────
|
|
162
|
+
console.log(chalk.bold('Step 2/5 Check Docker is running'));
|
|
163
|
+
if (!isDockerRunning()) {
|
|
164
|
+
console.log(chalk.yellow(' Docker is not running. Starting it...'));
|
|
165
|
+
if (process.platform === 'darwin') {
|
|
166
|
+
try { execSync('open -a Docker', { stdio: 'ignore' }); } catch {}
|
|
167
|
+
} else if (process.platform === 'linux') {
|
|
168
|
+
try { execSync('sudo systemctl start docker', { stdio: 'ignore' }); } catch {}
|
|
169
|
+
} else if (process.platform === 'win32') {
|
|
170
|
+
try { execSync('start "" "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"', { stdio: 'ignore' }); } catch {}
|
|
171
|
+
}
|
|
172
|
+
// Poll until Docker is ready (up to 60s)
|
|
173
|
+
let ready = false;
|
|
174
|
+
for (let i = 0; i < 20; i++) {
|
|
175
|
+
if (isDockerRunning()) { ready = true; break; }
|
|
176
|
+
process.stdout.write(chalk.dim(` Waiting for Docker to start... ${(i + 1) * 3}s\r`));
|
|
177
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
178
|
+
}
|
|
179
|
+
if (!ready) {
|
|
180
|
+
console.error(chalk.red('\n ✗ Docker did not start within 60s. Please start it manually and re-run init.'));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
process.stdout.write('\n');
|
|
184
|
+
}
|
|
185
|
+
console.log(chalk.green(' ✓ Docker is running\n'));
|
|
186
|
+
|
|
187
|
+
// ── Step 3: Base image built? ─────────────────────────────────────────────
|
|
188
|
+
console.log(chalk.bold('Step 3/5 Build base Docker image'));
|
|
189
|
+
if (imageExists()) {
|
|
190
|
+
console.log(chalk.green(' ✓ Image already built, skipping\n'));
|
|
191
|
+
} else {
|
|
192
|
+
console.log(chalk.dim(' Building openclaw-spawn-base:latest (this takes a few minutes the first time)...\n'));
|
|
193
|
+
const ok = buildBaseImage();
|
|
194
|
+
if (!ok) {
|
|
195
|
+
console.error(chalk.red(' ✗ Image build failed'));
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
console.log(chalk.green(' ✓ Image built\n'));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Step 4: Onboard new instance ─────────────────────────────────────────
|
|
202
|
+
console.log(chalk.bold('Step 4/5 Set up OpenClaw instance'));
|
|
203
|
+
ensureNetwork();
|
|
204
|
+
console.log(chalk.dim(' Running OpenClaw onboarding wizard...\n'));
|
|
205
|
+
const instanceName = await promptInstanceName();
|
|
206
|
+
const port = getNextPort();
|
|
207
|
+
console.log(chalk.blue(`\n Creating instance ${instanceName} on port ${port}...`));
|
|
208
|
+
addInstance(instanceName, port);
|
|
209
|
+
const created = createContainer(instanceName, port);
|
|
210
|
+
if (!created) {
|
|
211
|
+
console.error(chalk.red(' ✗ Failed to create container'));
|
|
212
|
+
removeInstanceMetadata(instanceName);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
const instance = getInstance(instanceName);
|
|
216
|
+
await execInContainer(instance.container, 'openclaw onboard', false);
|
|
217
|
+
setInstanceGatewayPort(instanceName, instance.port);
|
|
218
|
+
enableBrowserTool(instanceName);
|
|
219
|
+
console.log(chalk.green(`\n ✓ Instance ${instanceName} ready\n`));
|
|
220
|
+
|
|
221
|
+
// ── Step 5: Start gateway ─────────────────────────────────────────────────
|
|
222
|
+
console.log(chalk.bold('Step 5/5 Start gateway'));
|
|
223
|
+
const inst = getInstance(instanceName);
|
|
224
|
+
|
|
225
|
+
// Bootstrap Chrome then start gateway
|
|
226
|
+
stopVisibleChrome(inst.container);
|
|
227
|
+
startVisibleChrome(inst.container, 18800);
|
|
228
|
+
enableBrowserTakeover(instanceName);
|
|
229
|
+
setInstanceGatewayPort(instanceName, inst.port);
|
|
230
|
+
await execInContainer(inst.container, 'openclaw gateway --bind lan', true);
|
|
231
|
+
execSync('sleep 2', { stdio: 'ignore' });
|
|
232
|
+
console.log(chalk.green(' ✓ Gateway started\n'));
|
|
233
|
+
|
|
234
|
+
// ── Done ──────────────────────────────────────────────────────────────────
|
|
235
|
+
console.log(chalk.green.bold('✓ OpenClaw Swarm is ready!\n'));
|
|
236
|
+
console.log(chalk.blue('Next steps:'));
|
|
237
|
+
console.log(chalk.cyan(` openclaw-spawn tui`) + chalk.dim(' # chat with your agent'));
|
|
238
|
+
console.log(chalk.cyan(` openclaw-spawn browser`) + chalk.dim(' # open VNC to see/control the browser'));
|
|
239
|
+
console.log(chalk.cyan(` openclaw-spawn dashboard`) + chalk.dim(' # open the web dashboard\n'));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Show help
|
|
243
|
+
function showHelp() {
|
|
244
|
+
console.log(`
|
|
245
|
+
${chalk.blue.bold('OpenClaw Swarm')} - Docker orchestrator for multiple OpenClaw instances
|
|
246
|
+
|
|
247
|
+
${chalk.yellow('Usage:')}
|
|
248
|
+
openclaw-spawn <command> [options]
|
|
249
|
+
|
|
250
|
+
${chalk.yellow('OpenClaw Commands:')} (auto-selects instance)
|
|
251
|
+
onboard Run OpenClaw onboarding wizard
|
|
252
|
+
gateway [-d] Start OpenClaw gateway (-d for background)
|
|
253
|
+
tui Open OpenClaw TUI
|
|
254
|
+
channels Manage channels
|
|
255
|
+
devices Manage devices
|
|
256
|
+
dashboard Open dashboard
|
|
257
|
+
... Any other OpenClaw command
|
|
258
|
+
|
|
259
|
+
${chalk.yellow('Setup:')}
|
|
260
|
+
init First-time setup wizard (installs Docker, builds image, onboards)
|
|
261
|
+
|
|
262
|
+
${chalk.yellow('Management Commands:')}
|
|
263
|
+
list List all instances
|
|
264
|
+
remove <name> Stop and remove instance
|
|
265
|
+
stop <name> Stop instance
|
|
266
|
+
start <name> Start instance
|
|
267
|
+
logs <name> [-f] Show instance logs
|
|
268
|
+
build Build Docker base image
|
|
269
|
+
cleanup Remove all containers and reset metadata
|
|
270
|
+
browser [name] Open VNC tab to see/control the agent's browser
|
|
271
|
+
browser stop [name] Close VNC view (agent browser keeps running)
|
|
272
|
+
|
|
273
|
+
${chalk.yellow('Examples:')}
|
|
274
|
+
openclaw-spawn onboard # Select instance and run onboard
|
|
275
|
+
openclaw-spawn gateway -d # Select instance and start gateway
|
|
276
|
+
openclaw-spawn list # List all instances
|
|
277
|
+
openclaw-spawn logs worker1 -f # Follow logs for worker1
|
|
278
|
+
`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Proxy OpenClaw command
|
|
282
|
+
async function proxyOpenClawCommand(args, detach = false) {
|
|
283
|
+
let command = args.length > 0 ? args.join(' ') : 'onboard';
|
|
284
|
+
|
|
285
|
+
// Select or create instance
|
|
286
|
+
const selected = await selectInstance(true);
|
|
287
|
+
|
|
288
|
+
let instanceName;
|
|
289
|
+
if (selected === '__new__') {
|
|
290
|
+
instanceName = await promptInstanceName();
|
|
291
|
+
const port = getNextPort();
|
|
292
|
+
|
|
293
|
+
console.log(chalk.blue(`\n📦 Creating instance ${instanceName} on port ${port}...`));
|
|
294
|
+
|
|
295
|
+
// Ensure network exists
|
|
296
|
+
ensureNetwork();
|
|
297
|
+
|
|
298
|
+
// Add to metadata
|
|
299
|
+
addInstance(instanceName, port);
|
|
300
|
+
|
|
301
|
+
// Create container
|
|
302
|
+
const created = createContainer(instanceName, port);
|
|
303
|
+
if (!created) {
|
|
304
|
+
console.error(chalk.red('✗ Failed to create container'));
|
|
305
|
+
removeInstanceMetadata(instanceName);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log(chalk.green(`✓ Created instance ${instanceName}`));
|
|
310
|
+
} else {
|
|
311
|
+
instanceName = selected;
|
|
312
|
+
const instance = getInstance(instanceName);
|
|
313
|
+
|
|
314
|
+
// Check if container is running
|
|
315
|
+
const status = getContainerStatus(instance.container);
|
|
316
|
+
if (status === 'stopped') {
|
|
317
|
+
console.log(chalk.yellow(`⚠ Instance ${instanceName} is stopped. Starting...`));
|
|
318
|
+
startContainer(instance.container);
|
|
319
|
+
} else if (status === 'not-found') {
|
|
320
|
+
console.log(chalk.yellow(`⚠ Container not found. Recreating...`));
|
|
321
|
+
createContainer(instanceName, instance.port);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Execute OpenClaw command
|
|
326
|
+
const instance = getInstance(instanceName);
|
|
327
|
+
|
|
328
|
+
// Ensure gateway port in config matches host port
|
|
329
|
+
// Add --bind lan for gateway commands (required for Docker containers)
|
|
330
|
+
if (command.startsWith('gateway')) {
|
|
331
|
+
setInstanceGatewayPort(instanceName, instance.port);
|
|
332
|
+
// Add --bind lan if not already present (gateway must bind to 0.0.0.0 in container)
|
|
333
|
+
if (!command.includes('--bind')) {
|
|
334
|
+
command = command.replace(/^gateway/, 'gateway --bind lan');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// When starting gateway in background: bootstrap Chrome first so the browser tool
|
|
339
|
+
// is ready the moment the gateway comes up — no separate `browser` command needed.
|
|
340
|
+
// CDP port is always 18800 inside the container (mapped to host port+11 externally).
|
|
341
|
+
if (command.startsWith('gateway') && detach) {
|
|
342
|
+
stopVisibleChrome(instance.container);
|
|
343
|
+
startVisibleChrome(instance.container, 18800);
|
|
344
|
+
enableBrowserTakeover(instanceName);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (detach) {
|
|
348
|
+
console.log(chalk.blue(`\n🦞 Starting in background: openclaw ${command}`));
|
|
349
|
+
console.log(chalk.dim(`Container: ${instance.container}\n`));
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
await execInContainer(instance.container, `openclaw ${command}`, true);
|
|
353
|
+
console.log(chalk.green(`✓ Command started in background`));
|
|
354
|
+
console.log(chalk.dim(`View logs: openclaw-spawn logs ${instanceName} -f`));
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.error(chalk.red(`\n✗ Command failed: ${error.message}`));
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
console.log(chalk.blue(`\n🦞 Running: openclaw ${command}`));
|
|
361
|
+
console.log(chalk.dim(`Container: ${instance.container}\n`));
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
await execInContainer(instance.container, `openclaw ${command}`, false);
|
|
365
|
+
if (command === 'onboard') {
|
|
366
|
+
setInstanceGatewayPort(instanceName, instance.port);
|
|
367
|
+
enableBrowserTool(instanceName);
|
|
368
|
+
}
|
|
369
|
+
if (command.startsWith('dashboard')) {
|
|
370
|
+
const token = getInstanceGatewayToken(instanceName);
|
|
371
|
+
const url = token
|
|
372
|
+
? `http://localhost:${instance.port}/#token=${token}`
|
|
373
|
+
: `http://localhost:${instance.port}/`;
|
|
374
|
+
console.log(chalk.blue(`\n📌 Instance ${instanceName} → open on your machine:`));
|
|
375
|
+
console.log(chalk.cyan(` ${url}`));
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error(chalk.red(`\n✗ Command failed: ${error.message}`));
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Open live browser view via noVNC.
|
|
385
|
+
// Instead of asking the gateway to launch Chrome (which has Playwright/GPU issues in Docker non-headless),
|
|
386
|
+
// we launch Chrome ourselves on the Xvfb display and set attachOnly:true so the gateway just attaches
|
|
387
|
+
// to our already-running Chrome via CDP. Both the agent and the user share the same browser session.
|
|
388
|
+
async function browserCommand(name) {
|
|
389
|
+
let instanceName = name;
|
|
390
|
+
|
|
391
|
+
if (!instanceName) {
|
|
392
|
+
const selected = await selectInstance(false);
|
|
393
|
+
if (!selected || selected === '__new__') {
|
|
394
|
+
console.error(chalk.red('✗ No instance selected'));
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
instanceName = selected;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const instance = getInstance(instanceName);
|
|
401
|
+
if (!instance) {
|
|
402
|
+
console.error(chalk.red(`✗ Instance ${instanceName} not found`));
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Ensure container is running
|
|
407
|
+
const status = getContainerStatus(instance.container);
|
|
408
|
+
if (status === 'stopped') {
|
|
409
|
+
console.log(chalk.yellow(`⚠ Instance ${instanceName} is stopped. Starting...`));
|
|
410
|
+
startContainer(instance.container);
|
|
411
|
+
} else if (status === 'not-found') {
|
|
412
|
+
console.error(chalk.red(`✗ Container not found. Run: openclaw-spawn start ${instanceName}`));
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Check Xvfb is available — only present in containers built with the new image
|
|
417
|
+
let xvfbAvailable = false;
|
|
418
|
+
try {
|
|
419
|
+
const check = execSync(
|
|
420
|
+
`docker exec ${instance.container} sh -c "command -v Xvfb > /dev/null 2>&1 && echo yes || echo no"`,
|
|
421
|
+
{ encoding: 'utf8' }
|
|
422
|
+
).trim();
|
|
423
|
+
xvfbAvailable = check === 'yes';
|
|
424
|
+
} catch {}
|
|
425
|
+
|
|
426
|
+
if (!xvfbAvailable) {
|
|
427
|
+
console.error(chalk.red('\n✗ This container was built without VNC support.'));
|
|
428
|
+
console.error(chalk.yellow(' Rebuild the image and recreate the container:'));
|
|
429
|
+
console.error(chalk.dim(`\n openclaw-spawn build`));
|
|
430
|
+
console.error(chalk.dim(` openclaw-spawn cleanup`));
|
|
431
|
+
console.error(chalk.dim(` openclaw-spawn onboard\n`));
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log(chalk.blue(`\n🖥 Starting browser view for ${instanceName}...`));
|
|
436
|
+
|
|
437
|
+
// Chrome is already running (started by `gateway -d`). Just attach VNC services to the display.
|
|
438
|
+
console.log(chalk.dim(' Starting VNC services...'));
|
|
439
|
+
startVnc(instance.container);
|
|
440
|
+
|
|
441
|
+
const vncUrl = `http://localhost:${instance.port + 20}/vnc.html?autoconnect=true&reconnect=true`;
|
|
442
|
+
|
|
443
|
+
console.log(chalk.green(`\n✓ Browser view ready!`));
|
|
444
|
+
console.log(chalk.blue(`\n📌 Instance ${instanceName} → open on your machine:`));
|
|
445
|
+
console.log(chalk.cyan(` ${vncUrl}`));
|
|
446
|
+
console.log(chalk.dim(`\n You and the agent share the same Chrome session.`));
|
|
447
|
+
console.log(chalk.dim(` Log in, solve captchas, or fill credentials — the agent picks it up automatically.`));
|
|
448
|
+
console.log(chalk.dim(`\n When done: openclaw-spawn browser stop ${instanceName}\n`));
|
|
449
|
+
|
|
450
|
+
// Auto-open VNC tab
|
|
451
|
+
try {
|
|
452
|
+
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
453
|
+
execSync(`${openCmd} "${vncUrl}"`, { stdio: 'ignore' });
|
|
454
|
+
} catch {}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Restore normal headless mode — stop visible Chrome, re-enable headless, restart gateway
|
|
458
|
+
async function browserStopCommand(name) {
|
|
459
|
+
let instanceName = name;
|
|
460
|
+
|
|
461
|
+
if (!instanceName) {
|
|
462
|
+
const selected = await selectInstance(false);
|
|
463
|
+
if (!selected || selected === '__new__') {
|
|
464
|
+
console.error(chalk.red('✗ No instance selected'));
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
instanceName = selected;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const instance = getInstance(instanceName);
|
|
471
|
+
if (!instance) {
|
|
472
|
+
console.error(chalk.red(`✗ Instance ${instanceName} not found`));
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
console.log(chalk.blue(`\n🔄 Stopping VNC view for ${instanceName}...`));
|
|
477
|
+
|
|
478
|
+
// Stop VNC services only — Chrome and gateway keep running so the agent can still use the browser
|
|
479
|
+
spawnSync('docker', ['exec', instance.container, 'pkill', '-f', 'websockify'], { stdio: 'pipe' });
|
|
480
|
+
spawnSync('docker', ['exec', instance.container, 'pkill', '-x', 'x11vnc'], { stdio: 'pipe' });
|
|
481
|
+
|
|
482
|
+
console.log(chalk.green(`\n✓ VNC stopped. Agent browser tool is still active.`));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// List instances
|
|
486
|
+
async function listInstances() {
|
|
487
|
+
const instances = getInstances();
|
|
488
|
+
|
|
489
|
+
if (Object.keys(instances).length === 0) {
|
|
490
|
+
console.log(chalk.yellow('No instances found. Run a command to create one!'));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
console.log(chalk.blue.bold('\n📋 OpenClaw Instances:\n'));
|
|
495
|
+
|
|
496
|
+
for (const [name, instance] of Object.entries(instances)) {
|
|
497
|
+
const status = getContainerStatus(instance.container);
|
|
498
|
+
const statusIcon = status === 'running' ? '🟢' : status === 'stopped' ? '🔴' : '⚪';
|
|
499
|
+
const statusColor = status === 'running' ? chalk.green : status === 'stopped' ? chalk.red : chalk.gray;
|
|
500
|
+
|
|
501
|
+
console.log(`${statusIcon} ${chalk.bold(name)}`);
|
|
502
|
+
console.log(` Port: ${instance.port}`);
|
|
503
|
+
console.log(` Status: ${statusColor(status)}`);
|
|
504
|
+
console.log(` Container: ${instance.container}`);
|
|
505
|
+
console.log(` Created: ${new Date(instance.created).toLocaleString()}`);
|
|
506
|
+
if (status === 'running') {
|
|
507
|
+
console.log(` Browser: ${chalk.cyan(`http://localhost:${instance.port + 20}/vnc.html?autoconnect=true`)}`);
|
|
508
|
+
}
|
|
509
|
+
console.log();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Remove instance
|
|
514
|
+
async function removeInstanceCommand(name) {
|
|
515
|
+
const instance = getInstance(name);
|
|
516
|
+
if (!instance) {
|
|
517
|
+
console.error(chalk.red(`✗ Instance ${name} not found`));
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const confirmed = await confirm(`Remove instance ${name}? This will delete all data.`);
|
|
522
|
+
if (!confirmed) {
|
|
523
|
+
console.log(chalk.yellow('Cancelled'));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
console.log(chalk.blue(`Removing instance ${name}...`));
|
|
528
|
+
removeContainer(instance.container);
|
|
529
|
+
removeInstanceMetadata(name);
|
|
530
|
+
console.log(chalk.green(`✓ Removed instance ${name}`));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Stop instance
|
|
534
|
+
async function stopInstanceCommand(name) {
|
|
535
|
+
const instance = getInstance(name);
|
|
536
|
+
if (!instance) {
|
|
537
|
+
console.error(chalk.red(`✗ Instance ${name} not found`));
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
console.log(chalk.blue(`Stopping instance ${name}...`));
|
|
542
|
+
stopContainer(instance.container);
|
|
543
|
+
console.log(chalk.green(`✓ Stopped instance ${name}`));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Start instance
|
|
547
|
+
async function startInstanceCommand(name) {
|
|
548
|
+
const instance = getInstance(name);
|
|
549
|
+
if (!instance) {
|
|
550
|
+
console.error(chalk.red(`✗ Instance ${name} not found`));
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
console.log(chalk.blue(`Starting instance ${name}...`));
|
|
555
|
+
startContainer(instance.container);
|
|
556
|
+
console.log(chalk.green(`✓ Started instance ${name}`));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Show logs
|
|
560
|
+
async function showLogsCommand(name, follow) {
|
|
561
|
+
const instance = getInstance(name);
|
|
562
|
+
if (!instance) {
|
|
563
|
+
console.error(chalk.red(`✗ Instance ${name} not found`));
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
showLogs(instance.container, follow);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Build base image
|
|
571
|
+
async function buildCommand() {
|
|
572
|
+
console.log(chalk.blue('Building Docker base image...'));
|
|
573
|
+
console.log(chalk.dim('This may take a few minutes on first build.\n'));
|
|
574
|
+
|
|
575
|
+
const success = buildBaseImage();
|
|
576
|
+
if (success) {
|
|
577
|
+
console.log(chalk.green('\n✓ Build complete!'));
|
|
578
|
+
} else {
|
|
579
|
+
console.error(chalk.red('\n✗ Build failed'));
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Cleanup - remove all containers and reset metadata
|
|
585
|
+
async function cleanupCommand() {
|
|
586
|
+
const confirmed = await confirm('Remove all OpenClaw instances and reset metadata?');
|
|
587
|
+
if (!confirmed) {
|
|
588
|
+
console.log(chalk.yellow('Cancelled'));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
console.log(chalk.blue('🧹 Cleaning up OpenClaw Swarm...\n'));
|
|
593
|
+
|
|
594
|
+
// Get all instances
|
|
595
|
+
const instances = getInstances();
|
|
596
|
+
const instanceNames = Object.keys(instances);
|
|
597
|
+
|
|
598
|
+
if (instanceNames.length > 0) {
|
|
599
|
+
console.log(chalk.dim('Stopping and removing containers...'));
|
|
600
|
+
for (const name of instanceNames) {
|
|
601
|
+
const instance = instances[name];
|
|
602
|
+
const status = getContainerStatus(instance.container);
|
|
603
|
+
if (status === 'running') {
|
|
604
|
+
stopContainer(instance.container);
|
|
605
|
+
}
|
|
606
|
+
if (status !== 'not-found') {
|
|
607
|
+
removeContainer(instance.container);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Clear instance configs (but keep directory structure)
|
|
613
|
+
console.log(chalk.dim('Clearing instance configs...'));
|
|
614
|
+
const instancesDir = path.join(os.homedir(), '.openclaw-spawn', 'instances');
|
|
615
|
+
try {
|
|
616
|
+
// Remove all files in .openclaw subdirectories
|
|
617
|
+
execSync(`find "${instancesDir}" -mindepth 2 -type f -delete 2>/dev/null || true`, { stdio: 'ignore' });
|
|
618
|
+
// Remove empty subdirectories
|
|
619
|
+
execSync(`find "${instancesDir}" -mindepth 2 -type d -empty -delete 2>/dev/null || true`, { stdio: 'ignore' });
|
|
620
|
+
} catch (error) {
|
|
621
|
+
// Ignore errors (directory might not exist)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Reset metadata
|
|
625
|
+
console.log(chalk.dim('Clearing metadata...'));
|
|
626
|
+
removeInstanceMetadata('__all__'); // Will clear in metadata module
|
|
627
|
+
|
|
628
|
+
console.log(chalk.green('\n✅ Cleanup complete! Re-add instances with: openclaw-spawn onboard'));
|
|
629
|
+
}
|