@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.
@@ -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,6 @@
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
+ export declare function statusCommand(): Promise<void>;
@@ -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,6 @@
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
+ export declare function stopCommand(): Promise<void>;
@@ -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
+ }