@workermill/agent 0.1.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/README.md +73 -0
- package/dist/api.d.ts +13 -0
- package/dist/api.js +29 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +59 -0
- package/dist/commands/logs.d.ts +9 -0
- package/dist/commands/logs.js +52 -0
- package/dist/commands/pull.d.ts +4 -0
- package/dist/commands/pull.js +35 -0
- package/dist/commands/setup.d.ts +11 -0
- package/dist/commands/setup.js +396 -0
- package/dist/commands/start.d.ts +11 -0
- package/dist/commands/start.js +117 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +86 -0
- package/dist/commands/stop.d.ts +6 -0
- package/dist/commands/stop.js +61 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.js +284 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +104 -0
- package/dist/planner.d.ts +19 -0
- package/dist/planner.js +268 -0
- package/dist/poller.d.ts +15 -0
- package/dist/poller.js +188 -0
- package/dist/spawner.d.ts +42 -0
- package/dist/spawner.js +290 -0
- package/package.json +34 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup Wizard — Interactive configuration for the WorkerMill Remote Agent.
|
|
3
|
+
*
|
|
4
|
+
* Prerequisites checked/installed:
|
|
5
|
+
* - Docker Desktop (must be installed manually)
|
|
6
|
+
* - Git for Windows (must be installed manually — required by Claude Code on Windows)
|
|
7
|
+
* - Claude CLI → winget on Windows, curl | bash on Linux/macOS/WSL
|
|
8
|
+
* - Claude auth → launches `claude` which prompts for sign-in on first run
|
|
9
|
+
* - Worker image → `docker pull public.ecr.aws/a7k5r0v0/workermill-worker:latest`
|
|
10
|
+
*/
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import ora from "ora";
|
|
13
|
+
import inquirer from "inquirer";
|
|
14
|
+
import { execSync, spawnSync } from "child_process";
|
|
15
|
+
import { existsSync } from "fs";
|
|
16
|
+
import { hostname, homedir, totalmem, cpus } from "os";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import axios from "axios";
|
|
19
|
+
import { saveConfigToFile, getConfigFile, findClaudePath, } from "../config.js";
|
|
20
|
+
const isWindows = process.platform === "win32";
|
|
21
|
+
/**
|
|
22
|
+
* Check if a command exists in PATH.
|
|
23
|
+
*/
|
|
24
|
+
function commandExists(cmd) {
|
|
25
|
+
try {
|
|
26
|
+
const which = isWindows ? "where" : "which";
|
|
27
|
+
execSync(`${which} ${cmd}`, { stdio: "ignore", timeout: 10000 });
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get version string from a command, or null.
|
|
36
|
+
*/
|
|
37
|
+
function getVersion(cmd) {
|
|
38
|
+
try {
|
|
39
|
+
return execSync(`"${cmd}" --version`, { encoding: "utf-8", timeout: 10000 }).trim();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Find Git Bash on Windows. Returns path to bash.exe or null.
|
|
47
|
+
*/
|
|
48
|
+
function findGitBash() {
|
|
49
|
+
if (!isWindows)
|
|
50
|
+
return null; // Not needed on non-Windows
|
|
51
|
+
// Check if git is in PATH (Git for Windows adds it)
|
|
52
|
+
if (commandExists("git")) {
|
|
53
|
+
try {
|
|
54
|
+
const gitPath = execSync("where git", { encoding: "utf-8", timeout: 10000 }).trim().split("\n")[0];
|
|
55
|
+
// git.exe is at Git/cmd/git.exe, bash.exe is at Git/bin/bash.exe
|
|
56
|
+
const gitDir = join(gitPath, "..", "..", "bin", "bash.exe");
|
|
57
|
+
if (existsSync(gitDir))
|
|
58
|
+
return gitDir;
|
|
59
|
+
}
|
|
60
|
+
catch { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
// Check common install locations
|
|
63
|
+
const candidates = [
|
|
64
|
+
join(process.env.ProgramFiles || "C:\\Program Files", "Git", "bin", "bash.exe"),
|
|
65
|
+
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
66
|
+
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
67
|
+
join(homedir(), "scoop", "apps", "git", "current", "bin", "bash.exe"),
|
|
68
|
+
];
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
if (existsSync(candidate))
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Install Claude CLI using the official installer.
|
|
77
|
+
* Returns true on success.
|
|
78
|
+
*/
|
|
79
|
+
function installClaudeCli() {
|
|
80
|
+
if (isWindows) {
|
|
81
|
+
// Windows: use winget (built into Windows 10/11)
|
|
82
|
+
try {
|
|
83
|
+
execSync("winget install Anthropic.ClaudeCode --accept-package-agreements --accept-source-agreements", { stdio: "inherit", timeout: 180_000 });
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// macOS: try Homebrew first
|
|
92
|
+
if (process.platform === "darwin") {
|
|
93
|
+
try {
|
|
94
|
+
execSync("brew install --cask claude-code", { stdio: "inherit", timeout: 180_000 });
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch { /* fall through */ }
|
|
98
|
+
}
|
|
99
|
+
// Linux, WSL, macOS fallback: native installer
|
|
100
|
+
try {
|
|
101
|
+
execSync("curl -fsSL https://claude.ai/install.sh | bash", { stdio: "inherit", timeout: 120_000 });
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function setupCommand() {
|
|
110
|
+
// Welcome banner
|
|
111
|
+
console.log();
|
|
112
|
+
console.log(chalk.bold.cyan(" WorkerMill Remote Agent Setup"));
|
|
113
|
+
console.log(chalk.dim(" ─────────────────────────────────────"));
|
|
114
|
+
console.log();
|
|
115
|
+
console.log(" Run AI workers locally with your Claude Max subscription.");
|
|
116
|
+
console.log(" Workers execute on your machine, logs stream to the cloud dashboard.");
|
|
117
|
+
console.log();
|
|
118
|
+
// ── Step 0: System Requirements ───────────────────────────────────────────
|
|
119
|
+
const totalRamGB = Math.round(totalmem() / (1024 * 1024 * 1024));
|
|
120
|
+
const cpuCount = cpus().length;
|
|
121
|
+
console.log(chalk.dim(" System"));
|
|
122
|
+
console.log(` ${chalk.dim("RAM:")} ${totalRamGB} GB ${chalk.dim("CPUs:")} ${cpuCount}`);
|
|
123
|
+
if (totalRamGB < 8) {
|
|
124
|
+
console.log();
|
|
125
|
+
console.log(chalk.red(" ✗ Insufficient RAM"));
|
|
126
|
+
console.log(chalk.yellow(" WorkerMill requires at least 8 GB of RAM (16 GB recommended)."));
|
|
127
|
+
console.log(chalk.yellow(` Your system has ${totalRamGB} GB.`));
|
|
128
|
+
console.log();
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
else if (totalRamGB < 16) {
|
|
132
|
+
console.log(chalk.yellow(` ⚠ ${totalRamGB} GB RAM is below the recommended 16 GB.`));
|
|
133
|
+
console.log(chalk.yellow(" Workers may run slowly or be killed by the OS under memory pressure."));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.log(chalk.green(" ✓ System meets requirements"));
|
|
137
|
+
}
|
|
138
|
+
console.log();
|
|
139
|
+
// ── Step 1: Docker ────────────────────────────────────────────────────────
|
|
140
|
+
const dockerSpinner = ora("Checking Docker...").start();
|
|
141
|
+
if (commandExists("docker")) {
|
|
142
|
+
const version = getVersion("docker");
|
|
143
|
+
// Verify Docker daemon is actually running (not just the CLI installed)
|
|
144
|
+
try {
|
|
145
|
+
const osType = execSync("docker info --format {{.OSType}}", {
|
|
146
|
+
encoding: "utf-8",
|
|
147
|
+
timeout: 15000,
|
|
148
|
+
}).trim();
|
|
149
|
+
// On Windows, verify Linux containers mode
|
|
150
|
+
if (isWindows && osType === "windows") {
|
|
151
|
+
dockerSpinner.fail("Docker is running in Windows containers mode");
|
|
152
|
+
console.log();
|
|
153
|
+
console.log(chalk.yellow(" WorkerMill workers require Linux containers."));
|
|
154
|
+
console.log(chalk.yellow(" Right-click the Docker Desktop icon in the system tray →"));
|
|
155
|
+
console.log(chalk.yellow(" 'Switch to Linux containers...'"));
|
|
156
|
+
console.log();
|
|
157
|
+
console.log(" Then re-run: workermill-agent");
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
dockerSpinner.fail("Docker is installed but the daemon is not running");
|
|
163
|
+
console.log();
|
|
164
|
+
console.log(chalk.yellow(" Start Docker Desktop, wait for it to fully initialize,"));
|
|
165
|
+
console.log(chalk.yellow(" then re-run: workermill-agent"));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
dockerSpinner.succeed(`Docker ${chalk.dim(version ? `(${version})` : "")}`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
dockerSpinner.fail("Docker is not installed");
|
|
172
|
+
console.log();
|
|
173
|
+
console.log(chalk.yellow(" Docker Desktop is required but must be installed manually:"));
|
|
174
|
+
console.log(` ${chalk.cyan("https://docs.docker.com/get-docker/")}`);
|
|
175
|
+
console.log();
|
|
176
|
+
console.log(" Install Docker, start it, then re-run: workermill-agent");
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
// ── Step 2: Git for Windows (required by Claude Code on Windows) ────────
|
|
180
|
+
if (isWindows) {
|
|
181
|
+
const gitSpinner = ora("Checking Git for Windows...").start();
|
|
182
|
+
const gitBashPath = findGitBash();
|
|
183
|
+
if (gitBashPath) {
|
|
184
|
+
gitSpinner.succeed(`Git for Windows ${chalk.dim(`(${gitBashPath})`)}`);
|
|
185
|
+
// Set CLAUDE_CODE_GIT_BASH_PATH so Claude can find it
|
|
186
|
+
process.env.CLAUDE_CODE_GIT_BASH_PATH = gitBashPath;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
gitSpinner.fail("Git for Windows is not installed");
|
|
190
|
+
console.log();
|
|
191
|
+
console.log(chalk.yellow(" Claude Code on Windows requires Git for Windows (Git Bash):"));
|
|
192
|
+
console.log(` ${chalk.cyan("https://git-scm.com/downloads/win")}`);
|
|
193
|
+
console.log();
|
|
194
|
+
console.log(" Install Git for Windows, then re-run: workermill-agent");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// ── Step 3: Claude CLI ────────────────────────────────────────────────────
|
|
199
|
+
const claudeSpinner = ora("Checking Claude CLI...").start();
|
|
200
|
+
let claudePath = findClaudePath();
|
|
201
|
+
if (claudePath) {
|
|
202
|
+
const version = getVersion(claudePath);
|
|
203
|
+
claudeSpinner.succeed(`Claude CLI ${chalk.dim(version ? `(${version})` : "")}`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
claudeSpinner.warn("Claude CLI not found — installing...");
|
|
207
|
+
console.log();
|
|
208
|
+
const installed = installClaudeCli();
|
|
209
|
+
if (installed) {
|
|
210
|
+
claudePath = findClaudePath();
|
|
211
|
+
}
|
|
212
|
+
if (claudePath) {
|
|
213
|
+
const version = getVersion(claudePath);
|
|
214
|
+
console.log();
|
|
215
|
+
console.log(chalk.green(` ✓ Claude CLI installed ${chalk.dim(version ? `(${version})` : "")}`));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
console.log();
|
|
219
|
+
console.log(chalk.red(" Claude CLI was installed but could not be found."));
|
|
220
|
+
console.log();
|
|
221
|
+
if (isWindows) {
|
|
222
|
+
console.log(chalk.yellow(" Winget updated your PATH but this shell doesn't have it yet."));
|
|
223
|
+
console.log(chalk.yellow(" Close this terminal, open a new one, and re-run: workermill-agent"));
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
console.log(" Try manually:");
|
|
227
|
+
console.log(chalk.cyan(" curl -fsSL https://claude.ai/install.sh | bash"));
|
|
228
|
+
console.log(" Then re-run: workermill-agent");
|
|
229
|
+
}
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// ── Step 4: Claude auth ───────────────────────────────────────────────────
|
|
234
|
+
const authSpinner = ora("Checking Claude authentication...").start();
|
|
235
|
+
const credsPath = join(homedir(), ".claude", ".credentials.json");
|
|
236
|
+
if (existsSync(credsPath)) {
|
|
237
|
+
authSpinner.succeed("Claude authenticated");
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
authSpinner.warn("Not authenticated — launching Claude...");
|
|
241
|
+
console.log();
|
|
242
|
+
console.log(chalk.dim(" Claude will open and prompt you to authenticate."));
|
|
243
|
+
console.log(chalk.dim(" Sign in with your Claude Max account, then exit Claude (Ctrl+C)."));
|
|
244
|
+
console.log();
|
|
245
|
+
// Pass Git Bash path on Windows so Claude can find it
|
|
246
|
+
const env = { ...process.env };
|
|
247
|
+
if (isWindows) {
|
|
248
|
+
const gitBash = findGitBash();
|
|
249
|
+
if (gitBash)
|
|
250
|
+
env.CLAUDE_CODE_GIT_BASH_PATH = gitBash;
|
|
251
|
+
}
|
|
252
|
+
spawnSync(claudePath, [], {
|
|
253
|
+
stdio: "inherit",
|
|
254
|
+
timeout: 300_000,
|
|
255
|
+
env,
|
|
256
|
+
});
|
|
257
|
+
if (existsSync(credsPath)) {
|
|
258
|
+
console.log();
|
|
259
|
+
console.log(chalk.green(" ✓ Claude authenticated"));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
console.log();
|
|
263
|
+
console.log(chalk.red(" Authentication failed or was cancelled."));
|
|
264
|
+
console.log(` Try manually: open a new terminal and run 'claude'`);
|
|
265
|
+
console.log(" Then re-run: workermill-agent");
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// ── Step 5: Node.js ──────────────────────────────────────────────────────
|
|
270
|
+
console.log(chalk.green(" ✓") + ` Node.js ${chalk.dim(`(${process.version})`)}`);
|
|
271
|
+
console.log();
|
|
272
|
+
// ── Step 6: Configuration prompts ─────────────────────────────────────────
|
|
273
|
+
console.log(chalk.bold("Configuration"));
|
|
274
|
+
console.log();
|
|
275
|
+
const { apiUrl } = await inquirer.prompt([
|
|
276
|
+
{
|
|
277
|
+
type: "input",
|
|
278
|
+
name: "apiUrl",
|
|
279
|
+
message: "WorkerMill API URL:",
|
|
280
|
+
default: "https://workermill.com",
|
|
281
|
+
validate: (v) => v.startsWith("http://") || v.startsWith("https://") ? true : "Must be a valid URL",
|
|
282
|
+
},
|
|
283
|
+
]);
|
|
284
|
+
const { apiKey } = await inquirer.prompt([
|
|
285
|
+
{
|
|
286
|
+
type: "password",
|
|
287
|
+
name: "apiKey",
|
|
288
|
+
message: "API Key (from Settings > Integrations):",
|
|
289
|
+
mask: "*",
|
|
290
|
+
validate: (v) => (v.length > 0 ? true : "API key is required"),
|
|
291
|
+
},
|
|
292
|
+
]);
|
|
293
|
+
// Validate API key
|
|
294
|
+
const validateSpinner = ora("Validating API key...").start();
|
|
295
|
+
let scmProvider = "github";
|
|
296
|
+
try {
|
|
297
|
+
const resp = await axios.get(`${apiUrl.replace(/\/$/, "")}/api/agent/config`, {
|
|
298
|
+
headers: { "x-api-key": apiKey },
|
|
299
|
+
timeout: 15000,
|
|
300
|
+
});
|
|
301
|
+
scmProvider = resp.data.scmProvider || "github";
|
|
302
|
+
validateSpinner.succeed(`Connected! SCM provider: ${scmProvider}`);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
const err = error;
|
|
306
|
+
if (err.response?.status === 401) {
|
|
307
|
+
validateSpinner.fail("Invalid API key. Check Settings > Integrations on the dashboard.");
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
validateSpinner.fail("Failed to connect to WorkerMill API. Check the URL and try again.");
|
|
311
|
+
}
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
// SCM token
|
|
315
|
+
const tokenPrompts = {
|
|
316
|
+
github: "",
|
|
317
|
+
bitbucket: "",
|
|
318
|
+
gitlab: "",
|
|
319
|
+
};
|
|
320
|
+
const scmLabel = scmProvider === "bitbucket"
|
|
321
|
+
? "Bitbucket"
|
|
322
|
+
: scmProvider === "gitlab"
|
|
323
|
+
? "GitLab"
|
|
324
|
+
: "GitHub";
|
|
325
|
+
const { scmToken } = await inquirer.prompt([
|
|
326
|
+
{
|
|
327
|
+
type: "password",
|
|
328
|
+
name: "scmToken",
|
|
329
|
+
message: `${scmLabel} personal access token (for cloning/pushing to your repos):`,
|
|
330
|
+
mask: "*",
|
|
331
|
+
validate: (v) => (v.length > 0 ? true : "A token is required for workers to clone and push to your repositories"),
|
|
332
|
+
},
|
|
333
|
+
]);
|
|
334
|
+
if (scmToken) {
|
|
335
|
+
tokenPrompts[scmProvider] = scmToken;
|
|
336
|
+
}
|
|
337
|
+
const { agentId } = await inquirer.prompt([
|
|
338
|
+
{
|
|
339
|
+
type: "input",
|
|
340
|
+
name: "agentId",
|
|
341
|
+
message: "Agent name:",
|
|
342
|
+
default: `agent-${hostname()}`,
|
|
343
|
+
},
|
|
344
|
+
]);
|
|
345
|
+
// ── Step 7: Pull worker image ─────────────────────────────────────────────
|
|
346
|
+
console.log();
|
|
347
|
+
const workerImage = "public.ecr.aws/a7k5r0v0/workermill-worker:latest";
|
|
348
|
+
console.log(chalk.dim(` Pulling worker image: ${workerImage}`));
|
|
349
|
+
console.log(chalk.dim(" This may take a few minutes on first run (~1.1 GB)..."));
|
|
350
|
+
console.log();
|
|
351
|
+
// Use spawnSync to show progress AND capture errors
|
|
352
|
+
const pullResult = spawnSync("docker", ["pull", workerImage], {
|
|
353
|
+
stdio: "inherit",
|
|
354
|
+
timeout: 600_000, // 10 minutes
|
|
355
|
+
});
|
|
356
|
+
if (pullResult.status === 0) {
|
|
357
|
+
console.log();
|
|
358
|
+
console.log(chalk.green(` ✓ Worker image pulled`));
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
console.log();
|
|
362
|
+
console.log(chalk.red(" ✗ Failed to pull worker image."));
|
|
363
|
+
console.log();
|
|
364
|
+
if (pullResult.error) {
|
|
365
|
+
console.log(chalk.yellow(` Error: ${pullResult.error.message}`));
|
|
366
|
+
}
|
|
367
|
+
console.log(chalk.yellow(" Troubleshooting:"));
|
|
368
|
+
console.log(chalk.yellow(" 1. Is Docker Desktop running?"));
|
|
369
|
+
if (isWindows) {
|
|
370
|
+
console.log(chalk.yellow(" 2. Is Docker in Linux containers mode? (Right-click Docker tray icon)"));
|
|
371
|
+
}
|
|
372
|
+
console.log(chalk.yellow(` ${isWindows ? "3" : "2"}. Try manually: ${chalk.cyan(`docker pull ${workerImage}`)}`));
|
|
373
|
+
console.log();
|
|
374
|
+
console.log(chalk.dim(" Setup will continue — you can pull the image later before starting."));
|
|
375
|
+
}
|
|
376
|
+
// ── Step 8: Save config ───────────────────────────────────────────────────
|
|
377
|
+
const fileConfig = {
|
|
378
|
+
apiUrl: apiUrl.replace(/\/$/, ""),
|
|
379
|
+
apiKey,
|
|
380
|
+
agentId,
|
|
381
|
+
maxWorkers: 1,
|
|
382
|
+
pollIntervalMs: 5000,
|
|
383
|
+
heartbeatIntervalMs: 30000,
|
|
384
|
+
tokens: tokenPrompts,
|
|
385
|
+
workerImage,
|
|
386
|
+
setupCompletedAt: new Date().toISOString(),
|
|
387
|
+
};
|
|
388
|
+
saveConfigToFile(fileConfig);
|
|
389
|
+
console.log();
|
|
390
|
+
console.log(chalk.green.bold(" Setup complete!"));
|
|
391
|
+
console.log();
|
|
392
|
+
console.log(` Config saved to: ${chalk.dim(getConfigFile())}`);
|
|
393
|
+
console.log();
|
|
394
|
+
console.log(` Start the agent with: ${chalk.cyan("workermill-agent start")}`);
|
|
395
|
+
console.log();
|
|
396
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start Command — Start the WorkerMill Remote Agent.
|
|
3
|
+
*
|
|
4
|
+
* Loads config from ~/.workermill/config.json, validates prerequisites,
|
|
5
|
+
* registers with the cloud API, and starts polling.
|
|
6
|
+
*
|
|
7
|
+
* Supports --detach mode for running as a daemon.
|
|
8
|
+
*/
|
|
9
|
+
export declare function startCommand(options: {
|
|
10
|
+
detach?: boolean;
|
|
11
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start Command — Start the WorkerMill Remote Agent.
|
|
3
|
+
*
|
|
4
|
+
* Loads config from ~/.workermill/config.json, validates prerequisites,
|
|
5
|
+
* registers with the cloud API, and starts polling.
|
|
6
|
+
*
|
|
7
|
+
* Supports --detach mode for running as a daemon.
|
|
8
|
+
*/
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { totalmem } from "os";
|
|
11
|
+
import { spawn } from "child_process";
|
|
12
|
+
import { writeFileSync, existsSync, unlinkSync, openSync } from "fs";
|
|
13
|
+
import { loadConfigFromFile, checkPrerequisites, getSystemInfo, getPidFile, getLogFile, getConfigFile, } from "../config.js";
|
|
14
|
+
import { startAgent } from "../index.js";
|
|
15
|
+
export async function startCommand(options) {
|
|
16
|
+
// Check config exists
|
|
17
|
+
if (!existsSync(getConfigFile())) {
|
|
18
|
+
console.log(chalk.red("No configuration found."));
|
|
19
|
+
console.log(`Run ${chalk.cyan("workermill-agent setup")} first.`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const config = loadConfigFromFile();
|
|
23
|
+
// Validate prerequisites
|
|
24
|
+
const prereqs = checkPrerequisites(config.workerImage);
|
|
25
|
+
const failing = prereqs.filter((p) => !p.ok);
|
|
26
|
+
// Auto-pull worker image if it's the only missing prereq
|
|
27
|
+
const imageMissing = failing.find((p) => p.name === "Worker image");
|
|
28
|
+
const otherFailing = failing.filter((p) => p.name !== "Worker image");
|
|
29
|
+
if (otherFailing.length > 0) {
|
|
30
|
+
console.log(chalk.red("Prerequisites check failed:"));
|
|
31
|
+
for (const p of otherFailing) {
|
|
32
|
+
console.log(chalk.red(` ✗ ${p.name}: ${p.detail}`));
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
if (imageMissing) {
|
|
37
|
+
console.log(chalk.yellow(` Worker image not found locally. Pulling ${config.workerImage}...`));
|
|
38
|
+
const { spawnSync } = await import("child_process");
|
|
39
|
+
const pull = spawnSync("docker", ["pull", config.workerImage], {
|
|
40
|
+
stdio: "inherit",
|
|
41
|
+
timeout: 600_000,
|
|
42
|
+
});
|
|
43
|
+
if (pull.status !== 0) {
|
|
44
|
+
console.log(chalk.red(` Failed to pull worker image.`));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
console.log(chalk.green(` ✓ Worker image pulled`));
|
|
48
|
+
}
|
|
49
|
+
if (options.detach) {
|
|
50
|
+
const logFile = getLogFile();
|
|
51
|
+
const pidFile = getPidFile();
|
|
52
|
+
console.log(chalk.dim(`Starting agent in background...`));
|
|
53
|
+
console.log(chalk.dim(` Logs: ${logFile}`));
|
|
54
|
+
console.log(chalk.dim(` PID: ${pidFile}`));
|
|
55
|
+
// Spawn the CLI with "start" (no --detach) as a detached child, redirecting output to log file
|
|
56
|
+
const logFd = openSync(logFile, "a");
|
|
57
|
+
const child = spawn("workermill-agent", ["start"], {
|
|
58
|
+
detached: true,
|
|
59
|
+
stdio: ["ignore", logFd, logFd],
|
|
60
|
+
shell: true, // Required on Windows to find .cmd wrappers
|
|
61
|
+
});
|
|
62
|
+
if (child.pid) {
|
|
63
|
+
writeFileSync(pidFile, String(child.pid), "utf-8");
|
|
64
|
+
child.unref();
|
|
65
|
+
console.log(chalk.green(`Agent started (PID: ${child.pid})`));
|
|
66
|
+
console.log(`Check status with: ${chalk.cyan("workermill-agent status")}`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log(chalk.red("Failed to start agent in background."));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Foreground mode
|
|
75
|
+
console.log();
|
|
76
|
+
console.log(chalk.bold.cyan(" WorkerMill Remote Agent"));
|
|
77
|
+
console.log(chalk.dim(" ─────────────────────────────────────"));
|
|
78
|
+
console.log();
|
|
79
|
+
// RAM check
|
|
80
|
+
const totalRamGB = Math.round(totalmem() / (1024 * 1024 * 1024));
|
|
81
|
+
if (totalRamGB < 8) {
|
|
82
|
+
console.log(chalk.red(` ✗ Insufficient RAM: ${totalRamGB} GB (minimum 8 GB, recommended 16 GB)`));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
else if (totalRamGB < 16) {
|
|
86
|
+
console.log(chalk.yellow(` ⚠ RAM: ${totalRamGB} GB (below recommended 16 GB — workers may be slow)`));
|
|
87
|
+
}
|
|
88
|
+
// Register with system info
|
|
89
|
+
const sysInfo = getSystemInfo();
|
|
90
|
+
console.log(chalk.dim(` Agent: ${config.agentId}`));
|
|
91
|
+
console.log(chalk.dim(` Host: ${sysInfo.hostname}`));
|
|
92
|
+
console.log(chalk.dim(` Platform: ${sysInfo.platform}`));
|
|
93
|
+
console.log(chalk.dim(` Image: ${config.workerImage}`));
|
|
94
|
+
console.log();
|
|
95
|
+
try {
|
|
96
|
+
const cleanup = await startAgent(config);
|
|
97
|
+
// Write PID file for status command
|
|
98
|
+
writeFileSync(getPidFile(), String(process.pid), "utf-8");
|
|
99
|
+
// Graceful shutdown
|
|
100
|
+
const shutdown = async () => {
|
|
101
|
+
await cleanup();
|
|
102
|
+
try {
|
|
103
|
+
unlinkSync(getPidFile());
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
process.exit(0);
|
|
109
|
+
};
|
|
110
|
+
process.on("SIGINT", shutdown);
|
|
111
|
+
process.on("SIGTERM", shutdown);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.log(chalk.red(`Failed to start: ${error instanceof Error ? error.message : String(error)}`));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Command — Show the current state of the WorkerMill Remote Agent.
|
|
3
|
+
*
|
|
4
|
+
* Displays: running/stopped, agent ID, active containers, API connectivity.
|
|
5
|
+
*/
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import axios from "axios";
|
|
10
|
+
import { getPidFile, getConfigFile, loadConfigFromFile } from "../config.js";
|
|
11
|
+
export async function statusCommand() {
|
|
12
|
+
console.log();
|
|
13
|
+
console.log(chalk.bold("WorkerMill Remote Agent Status"));
|
|
14
|
+
console.log(chalk.dim("─────────────────────────────────────"));
|
|
15
|
+
console.log();
|
|
16
|
+
// Check config
|
|
17
|
+
if (!existsSync(getConfigFile())) {
|
|
18
|
+
console.log(chalk.yellow(" Not configured. Run 'workermill-agent setup' first."));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const config = loadConfigFromFile();
|
|
22
|
+
// Agent process status
|
|
23
|
+
const pidFile = getPidFile();
|
|
24
|
+
let isRunning = false;
|
|
25
|
+
let pid = null;
|
|
26
|
+
if (existsSync(pidFile)) {
|
|
27
|
+
const pidStr = readFileSync(pidFile, "utf-8").trim();
|
|
28
|
+
pid = parseInt(pidStr, 10);
|
|
29
|
+
if (!isNaN(pid)) {
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, 0);
|
|
32
|
+
isRunning = true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
isRunning = false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const statusIcon = isRunning ? chalk.green("● online") : chalk.red("● offline");
|
|
40
|
+
console.log(` Status: ${statusIcon}${pid ? chalk.dim(` (PID ${pid})`) : ""}`);
|
|
41
|
+
console.log(` Agent ID: ${config.agentId}`);
|
|
42
|
+
console.log(` API URL: ${config.apiUrl}`);
|
|
43
|
+
console.log(` Max workers: ${config.maxWorkers}`);
|
|
44
|
+
console.log(` Image: ${config.workerImage}`);
|
|
45
|
+
// Active containers
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(chalk.bold(" Active Containers"));
|
|
48
|
+
try {
|
|
49
|
+
const output = execSync('docker ps --filter "name=workermill-" --format "{{.Names}}\t{{.Status}}\t{{.RunningFor}}"', { encoding: "utf-8", timeout: 10000 }).trim();
|
|
50
|
+
if (output) {
|
|
51
|
+
const lines = output.split("\n");
|
|
52
|
+
console.log(chalk.dim(` Found ${lines.length} container(s):`));
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const [name, status, running] = line.split("\t");
|
|
55
|
+
console.log(` ${chalk.cyan(name)} ${status} ${chalk.dim(running || "")}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(chalk.dim(" No active containers"));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
console.log(chalk.dim(" Could not query Docker"));
|
|
64
|
+
}
|
|
65
|
+
// API connectivity
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(chalk.bold(" API Connectivity"));
|
|
68
|
+
try {
|
|
69
|
+
const resp = await axios.get(`${config.apiUrl}/api/agent/config`, {
|
|
70
|
+
headers: { "x-api-key": config.apiKey },
|
|
71
|
+
timeout: 10000,
|
|
72
|
+
});
|
|
73
|
+
console.log(` ${chalk.green("✓")} Connected to ${config.apiUrl}`);
|
|
74
|
+
console.log(chalk.dim(` SCM: ${resp.data.scmProvider}, Model: ${resp.data.defaultWorkerModel}`));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const err = error;
|
|
78
|
+
if (err.response?.status === 401) {
|
|
79
|
+
console.log(` ${chalk.red("✗")} Authentication failed (invalid API key)`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(` ${chalk.red("✗")} Cannot reach ${config.apiUrl}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.log();
|
|
86
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop Command — Stop a running WorkerMill Remote Agent daemon.
|
|
3
|
+
*
|
|
4
|
+
* Reads PID from ~/.workermill/agent.pid, sends SIGTERM, waits for exit.
|
|
5
|
+
*/
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { existsSync, readFileSync, unlinkSync } from "fs";
|
|
8
|
+
import { getPidFile } from "../config.js";
|
|
9
|
+
export async function stopCommand() {
|
|
10
|
+
const pidFile = getPidFile();
|
|
11
|
+
if (!existsSync(pidFile)) {
|
|
12
|
+
console.log(chalk.yellow("No running agent found (no PID file)."));
|
|
13
|
+
console.log(chalk.dim(`Expected PID file at: ${pidFile}`));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const pidStr = readFileSync(pidFile, "utf-8").trim();
|
|
17
|
+
const pid = parseInt(pidStr, 10);
|
|
18
|
+
if (isNaN(pid)) {
|
|
19
|
+
console.log(chalk.red(`Invalid PID in ${pidFile}: ${pidStr}`));
|
|
20
|
+
unlinkSync(pidFile);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Check if process is running
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0); // Signal 0 = check existence
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
console.log(chalk.yellow(`Agent (PID ${pid}) is not running. Cleaning up PID file.`));
|
|
29
|
+
unlinkSync(pidFile);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Send SIGTERM
|
|
33
|
+
console.log(chalk.dim(`Stopping agent (PID ${pid})...`));
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, "SIGTERM");
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.log(chalk.red(`Failed to stop agent: ${error instanceof Error ? error.message : String(error)}`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Wait for process to exit (up to 15 seconds)
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
while (Date.now() - start < 15000) {
|
|
44
|
+
try {
|
|
45
|
+
process.kill(pid, 0);
|
|
46
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Process has exited
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Clean up PID file
|
|
54
|
+
try {
|
|
55
|
+
unlinkSync(pidFile);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* ignore */
|
|
59
|
+
}
|
|
60
|
+
console.log(chalk.green("Agent stopped."));
|
|
61
|
+
}
|