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/docker.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { execSync, spawn, spawnSync } from 'child_process';
|
|
2
|
+
import { getInstanceDir } from './metadata.js';
|
|
3
|
+
|
|
4
|
+
// Check if Docker is running
|
|
5
|
+
export function isDockerRunning() {
|
|
6
|
+
try {
|
|
7
|
+
execSync('docker ps', { stdio: 'ignore' });
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Check if the docker binary is installed (not necessarily running)
|
|
15
|
+
export function isDockerInstalled() {
|
|
16
|
+
try {
|
|
17
|
+
const cmd = process.platform === 'win32' ? 'where docker' : 'which docker';
|
|
18
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check if our base image already exists (skip rebuild if so)
|
|
26
|
+
export function imageExists() {
|
|
27
|
+
try {
|
|
28
|
+
const out = execSync('docker images -q openclaw-spawn-base:latest', { encoding: 'utf8' }).trim();
|
|
29
|
+
return out.length > 0;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if container exists
|
|
36
|
+
export function containerExists(containerName) {
|
|
37
|
+
try {
|
|
38
|
+
execSync(`docker ps -a --filter name=${containerName} --format '{{.Names}}'`, { stdio: 'pipe' });
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Get container status
|
|
46
|
+
export function getContainerStatus(containerName) {
|
|
47
|
+
try {
|
|
48
|
+
const output = execSync(
|
|
49
|
+
`docker ps -a --filter name=${containerName} --format '{{.Status}}'`,
|
|
50
|
+
{ encoding: 'utf8' }
|
|
51
|
+
);
|
|
52
|
+
if (output.includes('Up')) return 'running';
|
|
53
|
+
if (output.includes('Exited')) return 'stopped';
|
|
54
|
+
return 'unknown';
|
|
55
|
+
} catch {
|
|
56
|
+
return 'not-found';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create and start container
|
|
61
|
+
export function createContainer(name, port) {
|
|
62
|
+
const instanceDir = getInstanceDir(name);
|
|
63
|
+
const containerName = `openclaw-${name}`;
|
|
64
|
+
|
|
65
|
+
const cmd = [
|
|
66
|
+
'docker', 'run',
|
|
67
|
+
'-d',
|
|
68
|
+
'--name', containerName,
|
|
69
|
+
'-e', 'HOME=/home/node',
|
|
70
|
+
'-e', 'PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright',
|
|
71
|
+
'-p', `${port}:${port}`, // Gateway port
|
|
72
|
+
'-p', `${port + 2}:${port + 2}`, // Browser control service (gateway.port + 2)
|
|
73
|
+
'-p', `${port + 11}:18800`, // Chrome CDP port
|
|
74
|
+
'-p', `${port + 20}:6080`, // noVNC browser view
|
|
75
|
+
'-v', `${instanceDir}/.openclaw:/home/node/.openclaw`,
|
|
76
|
+
'-v', `${instanceDir}/workspace:/workspace`,
|
|
77
|
+
'-v', `/var/run/docker.sock:/var/run/docker.sock`,
|
|
78
|
+
'--network', 'openclaw-network',
|
|
79
|
+
'--shm-size', '1g', // Chrome needs more than the default 64MB for multiple tabs
|
|
80
|
+
'openclaw-spawn-base:latest'
|
|
81
|
+
// No CMD override โ Dockerfile CMD runs: Xvfb :99 ... & tail -f /dev/null
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
execSync(cmd.join(' '), { stdio: 'inherit' });
|
|
86
|
+
return true;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Failed to create container:', error.message);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Execute command in container (interactive)
|
|
94
|
+
export function execInContainer(containerName, command, detached = false) {
|
|
95
|
+
if (detached) {
|
|
96
|
+
// Run in background
|
|
97
|
+
const process = spawn('docker', ['exec', '-d', containerName, 'sh', '-c', command], {
|
|
98
|
+
stdio: 'inherit',
|
|
99
|
+
shell: false
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
process.on('close', (code) => {
|
|
104
|
+
if (code === 0) {
|
|
105
|
+
resolve(true);
|
|
106
|
+
} else {
|
|
107
|
+
reject(new Error(`Command exited with code ${code}`));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
process.on('error', (error) => {
|
|
112
|
+
reject(error);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
// Run interactively
|
|
117
|
+
const process = spawn('docker', ['exec', '-it', containerName, 'sh', '-c', command], {
|
|
118
|
+
stdio: 'inherit',
|
|
119
|
+
shell: false
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
process.on('close', (code) => {
|
|
124
|
+
if (code === 0) {
|
|
125
|
+
resolve(true);
|
|
126
|
+
} else {
|
|
127
|
+
reject(new Error(`Command exited with code ${code}`));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
process.on('error', (error) => {
|
|
132
|
+
reject(error);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Stop container
|
|
139
|
+
export function stopContainer(containerName) {
|
|
140
|
+
try {
|
|
141
|
+
execSync(`docker stop ${containerName}`, { stdio: 'inherit' });
|
|
142
|
+
return true;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Failed to stop container:', error.message);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Start container
|
|
150
|
+
export function startContainer(containerName) {
|
|
151
|
+
try {
|
|
152
|
+
execSync(`docker start ${containerName}`, { stdio: 'inherit' });
|
|
153
|
+
return true;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('Failed to start container:', error.message);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Remove container
|
|
161
|
+
export function removeContainer(containerName) {
|
|
162
|
+
try {
|
|
163
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'inherit' });
|
|
164
|
+
return true;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('Failed to remove container:', error.message);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Show container logs
|
|
172
|
+
export function showLogs(containerName, follow = false) {
|
|
173
|
+
const cmd = follow ?
|
|
174
|
+
`docker logs -f ${containerName}` :
|
|
175
|
+
`docker logs ${containerName}`;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
179
|
+
return true;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Failed to show logs:', error.message);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Build base image
|
|
187
|
+
export function buildBaseImage() {
|
|
188
|
+
try {
|
|
189
|
+
execSync('docker build -t openclaw-spawn-base:latest .', {
|
|
190
|
+
stdio: 'inherit',
|
|
191
|
+
cwd: process.cwd()
|
|
192
|
+
});
|
|
193
|
+
return true;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('Failed to build image:', error.message);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Start VNC services inside container (x11vnc + websockify/noVNC)
|
|
201
|
+
export function startVnc(containerName) {
|
|
202
|
+
try {
|
|
203
|
+
// Start x11vnc if not already running (serves Xvfb display :99 as VNC on port 5900)
|
|
204
|
+
execSync(
|
|
205
|
+
`docker exec -d ${containerName} sh -c "pgrep -x x11vnc > /dev/null || x11vnc -display :99 -forever -nopw -rfbport 5900 -quiet"`,
|
|
206
|
+
{ stdio: 'ignore' }
|
|
207
|
+
);
|
|
208
|
+
execSync('sleep 1', { stdio: 'ignore' });
|
|
209
|
+
// Kill any existing websockify first so we only ever have one instance on port 6080.
|
|
210
|
+
// Use spawnSync with explicit argv โ avoids shell interpretation and pkill self-kill issues.
|
|
211
|
+
spawnSync('docker', ['exec', containerName, 'pkill', '-f', 'websockify'], { stdio: 'pipe' });
|
|
212
|
+
execSync('sleep 0.5', { stdio: 'ignore' });
|
|
213
|
+
spawnSync('docker', ['exec', '-d', containerName, 'websockify', '--web', '/usr/share/novnc', '6080', 'localhost:5900'], { stdio: 'pipe' });
|
|
214
|
+
execSync('sleep 1', { stdio: 'ignore' });
|
|
215
|
+
return true;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('Failed to start VNC:', error.message);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Start a visible Chrome on the Xvfb display for browser takeover / VNC interaction.
|
|
223
|
+
// Uses --disable-gpu for Docker compatibility and --remote-debugging-port so the gateway can attach via CDP.
|
|
224
|
+
export function startVisibleChrome(containerName, cdpPort) {
|
|
225
|
+
try {
|
|
226
|
+
// Use a dedicated temp profile to avoid restoring old session tabs.
|
|
227
|
+
// Stale session tabs cause playwright connectOverCDP to hang indefinitely.
|
|
228
|
+
// Extra flags reduce background workers/network activity that can destabilize
|
|
229
|
+
// the CDP WebSocket connection when browsing heavy sites like YouTube.
|
|
230
|
+
execSync(
|
|
231
|
+
`docker exec -d ${containerName} sh -c "/home/node/openclaw-chromium` +
|
|
232
|
+
` --no-sandbox` +
|
|
233
|
+
` --disable-gpu` +
|
|
234
|
+
` --disable-dev-shm-usage` +
|
|
235
|
+
` --remote-debugging-port=${cdpPort}` +
|
|
236
|
+
` --user-data-dir=/tmp/openclaw-vnc-profile` +
|
|
237
|
+
` --no-first-run` +
|
|
238
|
+
` --no-default-browser-check` +
|
|
239
|
+
` --disable-background-networking` +
|
|
240
|
+
` --disable-extensions` +
|
|
241
|
+
` --metrics-recording-only` +
|
|
242
|
+
` --safebrowsing-disable-auto-update` +
|
|
243
|
+
` 2>/dev/null"`,
|
|
244
|
+
{ stdio: 'ignore' }
|
|
245
|
+
);
|
|
246
|
+
// Give Chrome 3 seconds to start and expose its CDP port
|
|
247
|
+
execSync('sleep 3', { stdio: 'ignore' });
|
|
248
|
+
return true;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error('Failed to start Chrome:', error.message);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Stop visible Chrome and any orphaned VNC services
|
|
256
|
+
export function stopVisibleChrome(containerName) {
|
|
257
|
+
spawnSync('docker', ['exec', containerName, 'pkill', '-f', 'openclaw-chromium'], { stdio: 'pipe' });
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Create Docker network
|
|
262
|
+
export function ensureNetwork() {
|
|
263
|
+
try {
|
|
264
|
+
execSync('docker network inspect openclaw-network', { stdio: 'ignore' });
|
|
265
|
+
} catch {
|
|
266
|
+
try {
|
|
267
|
+
execSync('docker network create openclaw-network', { stdio: 'inherit' });
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error('Failed to create network:', error.message);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
package/src/metadata.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
const SWARM_DIR = path.join(os.homedir(), '.openclaw-spawn');
|
|
6
|
+
const METADATA_FILE = path.join(SWARM_DIR, 'instances.json');
|
|
7
|
+
const INSTANCES_DIR = path.join(SWARM_DIR, 'instances');
|
|
8
|
+
|
|
9
|
+
// Ensure directories exist
|
|
10
|
+
export function ensureDirectories() {
|
|
11
|
+
if (!existsSync(SWARM_DIR)) {
|
|
12
|
+
mkdirSync(SWARM_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
if (!existsSync(INSTANCES_DIR)) {
|
|
15
|
+
mkdirSync(INSTANCES_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
if (!existsSync(METADATA_FILE)) {
|
|
18
|
+
writeFileSync(METADATA_FILE, JSON.stringify({ instances: {}, nextPort: 18789 }, null, 2));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Read metadata
|
|
23
|
+
export function readMetadata() {
|
|
24
|
+
ensureDirectories();
|
|
25
|
+
return JSON.parse(readFileSync(METADATA_FILE, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Write metadata
|
|
29
|
+
export function writeMetadata(data) {
|
|
30
|
+
ensureDirectories();
|
|
31
|
+
writeFileSync(METADATA_FILE, JSON.stringify(data, null, 2));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get all instances
|
|
35
|
+
export function getInstances() {
|
|
36
|
+
const data = readMetadata();
|
|
37
|
+
return data.instances;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Get instance by name
|
|
41
|
+
export function getInstance(name) {
|
|
42
|
+
const instances = getInstances();
|
|
43
|
+
return instances[name];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Add instance
|
|
47
|
+
export function addInstance(name, port) {
|
|
48
|
+
const data = readMetadata();
|
|
49
|
+
data.instances[name] = {
|
|
50
|
+
container: `openclaw-${name}`,
|
|
51
|
+
port,
|
|
52
|
+
created: new Date().toISOString(),
|
|
53
|
+
status: 'created'
|
|
54
|
+
};
|
|
55
|
+
data.nextPort = port + 220;
|
|
56
|
+
writeMetadata(data);
|
|
57
|
+
|
|
58
|
+
// Create instance directories
|
|
59
|
+
const instanceDir = path.join(INSTANCES_DIR, name);
|
|
60
|
+
mkdirSync(path.join(instanceDir, '.openclaw'), { recursive: true });
|
|
61
|
+
mkdirSync(path.join(instanceDir, 'workspace'), { recursive: true });
|
|
62
|
+
|
|
63
|
+
return data.instances[name];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Remove instance
|
|
67
|
+
export function removeInstance(name) {
|
|
68
|
+
const data = readMetadata();
|
|
69
|
+
if (name === '__all__') {
|
|
70
|
+
// Special case for cleanup: reset everything
|
|
71
|
+
data.instances = {};
|
|
72
|
+
data.nextPort = 18789;
|
|
73
|
+
} else {
|
|
74
|
+
delete data.instances[name];
|
|
75
|
+
}
|
|
76
|
+
writeMetadata(data);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get next available port
|
|
80
|
+
export function getNextPort() {
|
|
81
|
+
const data = readMetadata();
|
|
82
|
+
return data.nextPort;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get instance directory
|
|
86
|
+
export function getInstanceDir(name) {
|
|
87
|
+
return path.join(INSTANCES_DIR, name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Read gateway auth token from instance's openclaw.json (for dashboard URL)
|
|
91
|
+
export function getInstanceGatewayToken(name) {
|
|
92
|
+
const configPath = path.join(INSTANCES_DIR, name, '.openclaw', 'openclaw.json');
|
|
93
|
+
if (!existsSync(configPath)) return null;
|
|
94
|
+
try {
|
|
95
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
96
|
+
return config?.gateway?.auth?.token ?? null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Set gateway.port in instance's openclaw.json so internal port matches host port
|
|
103
|
+
export function setInstanceGatewayPort(name, port) {
|
|
104
|
+
const configPath = path.join(INSTANCES_DIR, name, '.openclaw', 'openclaw.json');
|
|
105
|
+
if (!existsSync(configPath)) return;
|
|
106
|
+
try {
|
|
107
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
108
|
+
if (!config.gateway) config.gateway = {};
|
|
109
|
+
config.gateway.port = port;
|
|
110
|
+
// Don't set bind - let CLI flag --bind lan override for gateway process only
|
|
111
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
112
|
+
} catch {
|
|
113
|
+
// ignore
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Enable browser takeover mode: we launch Chrome ourselves on Xvfb, gateway attaches via CDP.
|
|
118
|
+
// Sets attachOnly:true so the gateway never tries to launch Chrome itself (avoids Playwright launch issues in Docker).
|
|
119
|
+
// headless stays false for bookkeeping but is irrelevant when attachOnly:true.
|
|
120
|
+
export function enableBrowserTakeover(name) {
|
|
121
|
+
const configPath = path.join(INSTANCES_DIR, name, '.openclaw', 'openclaw.json');
|
|
122
|
+
if (!existsSync(configPath)) return;
|
|
123
|
+
try {
|
|
124
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
125
|
+
if (!config.browser) return;
|
|
126
|
+
config.browser.attachOnly = true;
|
|
127
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
128
|
+
} catch {
|
|
129
|
+
// ignore
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Restore normal headless mode โ undo browser takeover
|
|
134
|
+
export function disableBrowserTakeover(name) {
|
|
135
|
+
const configPath = path.join(INSTANCES_DIR, name, '.openclaw', 'openclaw.json');
|
|
136
|
+
if (!existsSync(configPath)) return;
|
|
137
|
+
try {
|
|
138
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
139
|
+
if (!config.browser) return;
|
|
140
|
+
config.browser.attachOnly = false;
|
|
141
|
+
config.browser.headless = true;
|
|
142
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
143
|
+
} catch {
|
|
144
|
+
// ignore
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Enable browser tool with managed Chromium (openclaw profile, CDP port 18800)
|
|
149
|
+
export function enableBrowserTool(name) {
|
|
150
|
+
const configPath = path.join(INSTANCES_DIR, name, '.openclaw', 'openclaw.json');
|
|
151
|
+
if (!existsSync(configPath)) return;
|
|
152
|
+
try {
|
|
153
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
154
|
+
if (!config.browser) config.browser = {};
|
|
155
|
+
config.browser.enabled = true;
|
|
156
|
+
config.browser.defaultProfile = 'openclaw';
|
|
157
|
+
config.browser.headless = true; // Required for Docker/headless environments
|
|
158
|
+
config.browser.noSandbox = true; // Required for Docker (Chrome sandbox needs kernel features)
|
|
159
|
+
config.browser.executablePath = '/home/node/openclaw-chromium';
|
|
160
|
+
if (!config.browser.profiles) config.browser.profiles = {};
|
|
161
|
+
config.browser.profiles.openclaw = { cdpPort: 18800, color: '#FF4500' };
|
|
162
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
163
|
+
} catch {
|
|
164
|
+
// ignore
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/selector.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { getInstances } from './metadata.js';
|
|
3
|
+
import { getContainerStatus } from './docker.js';
|
|
4
|
+
|
|
5
|
+
// Show instance selector
|
|
6
|
+
export async function selectInstance(allowAddNew = true) {
|
|
7
|
+
const instances = getInstances();
|
|
8
|
+
const choices = [];
|
|
9
|
+
|
|
10
|
+
// Add existing instances
|
|
11
|
+
for (const [name, instance] of Object.entries(instances)) {
|
|
12
|
+
const status = getContainerStatus(instance.container);
|
|
13
|
+
const statusIcon = status === 'running' ? '๐ข' : status === 'stopped' ? '๐ด' : 'โช';
|
|
14
|
+
choices.push({
|
|
15
|
+
name: `${statusIcon} ${name} (port ${instance.port}, ${status})`,
|
|
16
|
+
value: name,
|
|
17
|
+
short: name
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Add "new instance" option
|
|
22
|
+
if (allowAddNew) {
|
|
23
|
+
choices.push({
|
|
24
|
+
name: 'โ Add new instance',
|
|
25
|
+
value: '__new__',
|
|
26
|
+
short: 'New instance'
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (choices.length === 0) {
|
|
31
|
+
return '__new__';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const answer = await inquirer.prompt([{
|
|
35
|
+
type: 'list',
|
|
36
|
+
name: 'instance',
|
|
37
|
+
message: 'Select instance:',
|
|
38
|
+
choices
|
|
39
|
+
}]);
|
|
40
|
+
|
|
41
|
+
return answer.instance;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Prompt for new instance name
|
|
45
|
+
export async function promptInstanceName() {
|
|
46
|
+
const answer = await inquirer.prompt([{
|
|
47
|
+
type: 'input',
|
|
48
|
+
name: 'name',
|
|
49
|
+
message: 'Enter instance name:',
|
|
50
|
+
validate: (input) => {
|
|
51
|
+
if (!input) return 'Name cannot be empty';
|
|
52
|
+
if (!/^[a-z0-9-]+$/.test(input)) return 'Use only lowercase letters, numbers, and hyphens';
|
|
53
|
+
const instances = getInstances();
|
|
54
|
+
if (instances[input]) return 'Instance already exists';
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}]);
|
|
58
|
+
|
|
59
|
+
return answer.name;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Confirm action
|
|
63
|
+
export async function confirm(message) {
|
|
64
|
+
const answer = await inquirer.prompt([{
|
|
65
|
+
type: 'confirm',
|
|
66
|
+
name: 'confirmed',
|
|
67
|
+
message,
|
|
68
|
+
default: false
|
|
69
|
+
}]);
|
|
70
|
+
|
|
71
|
+
return answer.confirmed;
|
|
72
|
+
}
|
package/test/verify.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Quick test script to verify the CLI works
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
console.log('๐งช Testing OpenClaw Swarm CLI\n');
|
|
7
|
+
|
|
8
|
+
// Test 1: List instances
|
|
9
|
+
console.log('1๏ธโฃ Testing list command...');
|
|
10
|
+
try {
|
|
11
|
+
execSync('openclaw-spawn list', { stdio: 'inherit' });
|
|
12
|
+
console.log('โ
List command works\n');
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.log('โ List command failed\n');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Test 2: Check instance status
|
|
18
|
+
console.log('2๏ธโฃ Testing instance status...');
|
|
19
|
+
try {
|
|
20
|
+
execSync('docker ps | grep openclaw', { stdio: 'inherit' });
|
|
21
|
+
console.log('โ
Container is running\n');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.log('โ No containers running\n');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Test 3: Execute simple command in container
|
|
27
|
+
console.log('3๏ธโฃ Testing command execution...');
|
|
28
|
+
try {
|
|
29
|
+
execSync('docker exec openclaw-oc-1 openclaw --version', { stdio: 'inherit' });
|
|
30
|
+
console.log('โ
Can execute commands in container\n');
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.log('โ Failed to execute command\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Test 4: Check OpenClaw health
|
|
36
|
+
console.log('4๏ธโฃ Testing OpenClaw installation...');
|
|
37
|
+
try {
|
|
38
|
+
execSync('docker exec openclaw-oc-1 which openclaw', { stdio: 'inherit' });
|
|
39
|
+
console.log('โ
OpenClaw is installed\n');
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.log('โ OpenClaw not found\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Test 5: Check workspace
|
|
45
|
+
console.log('5๏ธโฃ Testing workspace access...');
|
|
46
|
+
try {
|
|
47
|
+
execSync('docker exec openclaw-oc-1 ls -la /workspace', { stdio: 'inherit' });
|
|
48
|
+
console.log('โ
Workspace is accessible\n');
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.log('โ Workspace not accessible\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('\n๐ CLI tests complete!');
|
|
54
|
+
console.log('\nTo test interactive commands manually:');
|
|
55
|
+
console.log(' openclaw-spawn onboard # Run onboarding wizard');
|
|
56
|
+
console.log(' openclaw-spawn gateway # Start gateway');
|
|
57
|
+
console.log(' openclaw-spawn tui # Open TUI');
|