@webmux/agent 0.2.0 → 0.2.1

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/dist/cli.js CHANGED
@@ -1,8 +1,19 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ AGENT_PACKAGE_NAME,
4
+ AGENT_VERSION,
5
+ SERVICE_NAME,
6
+ installService,
7
+ readInstalledServiceConfig,
8
+ servicePath,
9
+ uninstallService,
10
+ upgradeService
11
+ } from "./chunk-INUNCXBM.js";
2
12
 
3
13
  // src/cli.ts
4
- import os3 from "os";
5
- import { execFileSync as execFileSync2 } from "child_process";
14
+ import os2 from "os";
15
+ import { execFileSync } from "child_process";
16
+ import { pathToFileURL } from "url";
6
17
  import { Command } from "commander";
7
18
 
8
19
  // src/credentials.ts
@@ -41,1247 +52,202 @@ function saveCredentials(creds) {
41
52
  });
42
53
  }
43
54
 
44
- // src/connection.ts
45
- import WebSocket from "ws";
46
-
47
- // ../shared/src/contracts.ts
48
- var DEFAULT_TERMINAL_SIZE = {
49
- cols: 120,
50
- rows: 36
51
- };
52
-
53
- // ../shared/src/version.ts
54
- var SEMVER_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
55
- function parseSemanticVersion(version) {
56
- const match = version.trim().match(SEMVER_PATTERN);
57
- if (!match) {
58
- return null;
59
- }
60
- return {
61
- major: Number.parseInt(match[1], 10),
62
- minor: Number.parseInt(match[2], 10),
63
- patch: Number.parseInt(match[3], 10),
64
- prerelease: match[4] ? match[4].split(".") : []
65
- };
66
- }
67
- function compareSemanticVersions(left, right) {
68
- const parsedLeft = parseSemanticVersion(left);
69
- const parsedRight = parseSemanticVersion(right);
70
- if (!parsedLeft || !parsedRight) {
71
- throw new Error(`Invalid semantic version comparison: "${left}" vs "${right}"`);
72
- }
73
- if (parsedLeft.major !== parsedRight.major) {
74
- return parsedLeft.major - parsedRight.major;
75
- }
76
- if (parsedLeft.minor !== parsedRight.minor) {
77
- return parsedLeft.minor - parsedRight.minor;
78
- }
79
- if (parsedLeft.patch !== parsedRight.patch) {
80
- return parsedLeft.patch - parsedRight.patch;
81
- }
82
- return comparePrerelease(parsedLeft.prerelease, parsedRight.prerelease);
83
- }
84
- function comparePrerelease(left, right) {
85
- if (left.length === 0 && right.length === 0) {
86
- return 0;
87
- }
88
- if (left.length === 0) {
89
- return 1;
90
- }
91
- if (right.length === 0) {
92
- return -1;
93
- }
94
- const maxLength = Math.max(left.length, right.length);
95
- for (let index = 0; index < maxLength; index += 1) {
96
- const leftIdentifier = left[index];
97
- const rightIdentifier = right[index];
98
- if (leftIdentifier === void 0) {
99
- return -1;
100
- }
101
- if (rightIdentifier === void 0) {
102
- return 1;
103
- }
104
- const numericLeft = Number.parseInt(leftIdentifier, 10);
105
- const numericRight = Number.parseInt(rightIdentifier, 10);
106
- const leftIsNumber = String(numericLeft) === leftIdentifier;
107
- const rightIsNumber = String(numericRight) === rightIdentifier;
108
- if (leftIsNumber && rightIsNumber && numericLeft !== numericRight) {
109
- return numericLeft - numericRight;
110
- }
111
- if (leftIsNumber !== rightIsNumber) {
112
- return leftIsNumber ? -1 : 1;
113
- }
114
- if (leftIdentifier !== rightIdentifier) {
115
- return leftIdentifier < rightIdentifier ? -1 : 1;
116
- }
117
- }
118
- return 0;
119
- }
120
-
121
- // src/service.ts
122
- import fs2 from "fs";
123
- import os2 from "os";
124
- import path2 from "path";
125
- import { execFileSync } from "child_process";
126
- var SERVICE_NAME = "webmux-agent";
127
- function renderServiceUnit(options) {
128
- return `[Unit]
129
- Description=Webmux Agent (${options.agentName})
130
- After=network-online.target
131
- Wants=network-online.target
132
-
133
- [Service]
134
- Type=simple
135
- ExecStart=${options.nodePath} ${options.cliPath} start
136
- Restart=always
137
- RestartSec=10
138
- Environment=WEBMUX_AGENT_SERVICE=1
139
- Environment=WEBMUX_AGENT_AUTO_UPGRADE=${options.autoUpgrade ? "1" : "0"}
140
- Environment=WEBMUX_AGENT_NAME=${options.agentName}
141
- Environment=HOME=${options.homeDir}
142
- Environment=PATH=${options.pathEnv}
143
- WorkingDirectory=${options.homeDir}
144
-
145
- [Install]
146
- WantedBy=default.target
147
- `;
148
- }
149
- function installService(options) {
150
- const homeDir = options.homeDir ?? os2.homedir();
151
- const autoUpgrade = options.autoUpgrade;
152
- const release = installManagedRelease({
153
- packageName: options.packageName,
154
- version: options.version,
155
- homeDir
156
- });
157
- writeServiceUnit({
158
- agentName: options.agentName,
159
- autoUpgrade,
160
- cliPath: release.cliPath,
161
- homeDir
162
- });
163
- runSystemctl(["--user", "daemon-reload"]);
164
- runSystemctl(["--user", "enable", SERVICE_NAME]);
165
- runSystemctl(["--user", "restart", SERVICE_NAME]);
166
- runCommand("loginctl", ["enable-linger", os2.userInfo().username]);
167
- }
168
- function upgradeService(options) {
169
- const homeDir = options.homeDir ?? os2.homedir();
170
- const installedConfig = readInstalledServiceConfig(homeDir);
171
- const autoUpgrade = options.autoUpgrade ?? installedConfig?.autoUpgrade ?? true;
172
- const release = installManagedRelease({
173
- packageName: options.packageName,
174
- version: options.version,
175
- homeDir
176
- });
177
- writeServiceUnit({
178
- agentName: options.agentName,
179
- autoUpgrade,
180
- cliPath: release.cliPath,
181
- homeDir
182
- });
183
- runSystemctl(["--user", "daemon-reload"]);
184
- runSystemctl(["--user", "restart", SERVICE_NAME]);
185
- }
186
- function uninstallService(homeDir = os2.homedir()) {
187
- const unitPath = servicePath(homeDir);
188
- try {
189
- runSystemctl(["--user", "stop", SERVICE_NAME]);
190
- } catch {
191
- }
192
- try {
193
- runSystemctl(["--user", "disable", SERVICE_NAME]);
194
- } catch {
195
- }
196
- if (fs2.existsSync(unitPath)) {
197
- fs2.unlinkSync(unitPath);
198
- }
199
- try {
200
- runSystemctl(["--user", "daemon-reload"]);
201
- } catch {
202
- }
203
- }
204
- function readInstalledServiceConfig(homeDir = os2.homedir()) {
205
- const unitPath = servicePath(homeDir);
206
- if (!fs2.existsSync(unitPath)) {
207
- return null;
208
- }
209
- const unit = fs2.readFileSync(unitPath, "utf-8");
210
- const autoUpgradeMatch = unit.match(/^Environment=WEBMUX_AGENT_AUTO_UPGRADE=(\d)$/m);
211
- const versionMatch = unit.match(/\/releases\/([^/\s]+)\/node_modules\//);
212
- return {
213
- autoUpgrade: autoUpgradeMatch?.[1] !== "0",
214
- version: versionMatch?.[1] ?? null
215
- };
216
- }
217
- function servicePath(homeDir = os2.homedir()) {
218
- return path2.join(homeDir, ".config", "systemd", "user", `${SERVICE_NAME}.service`);
219
- }
220
- function writeServiceUnit(options) {
221
- const serviceDir = path2.dirname(servicePath(options.homeDir));
222
- fs2.mkdirSync(serviceDir, { recursive: true });
223
- fs2.writeFileSync(
224
- servicePath(options.homeDir),
225
- renderServiceUnit({
226
- agentName: options.agentName,
227
- autoUpgrade: options.autoUpgrade,
228
- cliPath: options.cliPath,
229
- homeDir: options.homeDir,
230
- nodePath: findBinary("node") ?? process.execPath,
231
- pathEnv: process.env.PATH ?? ""
232
- })
233
- );
234
- }
235
- function installManagedRelease(options) {
236
- const releaseDir = path2.join(options.homeDir, ".webmux", "releases", options.version);
237
- const cliPath = path2.join(
238
- releaseDir,
239
- "node_modules",
240
- ...options.packageName.split("/"),
241
- "dist",
242
- "cli.js"
243
- );
244
- if (fs2.existsSync(cliPath)) {
245
- return { cliPath, releaseDir };
246
- }
247
- fs2.mkdirSync(releaseDir, { recursive: true });
248
- ensureRuntimePackageJson(releaseDir);
249
- const packageManager = findBinary("pnpm") ? "pnpm" : "npm";
250
- if (packageManager === "pnpm") {
251
- runCommand("pnpm", ["add", "--dir", releaseDir, `${options.packageName}@${options.version}`]);
252
- } else {
253
- if (!findBinary("npm")) {
254
- throw new Error("Cannot find pnpm or npm. Install one package manager before installing the service.");
255
- }
256
- runCommand("npm", ["install", "--omit=dev", `${options.packageName}@${options.version}`], releaseDir);
257
- }
258
- if (!fs2.existsSync(cliPath)) {
259
- throw new Error(`Managed release did not produce a CLI at ${cliPath}`);
260
- }
261
- return { cliPath, releaseDir };
262
- }
263
- function ensureRuntimePackageJson(releaseDir) {
264
- const packageJsonPath = path2.join(releaseDir, "package.json");
265
- if (fs2.existsSync(packageJsonPath)) {
266
- return;
267
- }
268
- fs2.writeFileSync(
269
- packageJsonPath,
270
- JSON.stringify({
271
- name: "webmux-agent-runtime",
272
- private: true
273
- }, null, 2) + "\n"
274
- );
275
- }
276
- function runSystemctl(args) {
277
- runCommand("systemctl", args);
278
- }
279
- function runCommand(command, args, cwd) {
280
- execFileSync(command, args, {
281
- cwd,
282
- stdio: "inherit"
283
- });
284
- }
285
- function findBinary(name) {
286
- try {
287
- return execFileSync("which", [name], { encoding: "utf-8" }).trim();
288
- } catch {
289
- return null;
290
- }
291
- }
292
-
293
- // src/terminal.ts
294
- import { spawn } from "node-pty";
295
-
296
- // src/tmux.ts
297
- import { execFile } from "child_process";
298
- import { promisify } from "util";
299
- var execFileAsync = promisify(execFile);
300
- var FIELD_SEPARATOR = "";
301
- var SESSION_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,31}$/;
302
- var TMUX_EMPTY_STATE_MARKERS = [
303
- "error connecting to",
304
- "failed to connect to server",
305
- "no server running",
306
- "no sessions"
307
- ];
308
- var TmuxClient = class {
309
- socketName;
310
- workspaceRoot;
311
- constructor(options) {
312
- this.socketName = options.socketName;
313
- this.workspaceRoot = options.workspaceRoot;
314
- }
315
- async listSessions() {
316
- const stdout = await this.run(
317
- [
318
- "list-sessions",
319
- "-F",
320
- [
321
- "#{session_name}",
322
- "#{session_windows}",
323
- "#{session_attached}",
324
- "#{session_created}",
325
- "#{session_activity}",
326
- "#{session_path}",
327
- "#{pane_current_command}"
328
- ].join(FIELD_SEPARATOR)
329
- ],
330
- { allowEmptyState: true }
331
- );
332
- const sessions = parseSessionList(stdout);
333
- const enriched = await Promise.all(
334
- sessions.map(async (session) => ({
335
- ...session,
336
- preview: await this.getPreview(session.name)
337
- }))
338
- );
339
- return enriched.sort((left, right) => {
340
- if (left.lastActivityAt !== right.lastActivityAt) {
341
- return right.lastActivityAt - left.lastActivityAt;
342
- }
343
- return left.name.localeCompare(right.name);
344
- });
345
- }
346
- async createSession(name) {
347
- assertValidSessionName(name);
348
- if (await this.hasSession(name)) {
349
- return;
350
- }
351
- await this.run(["new-session", "-d", "-s", name, "-c", this.workspaceRoot]);
352
- }
353
- async killSession(name) {
354
- assertValidSessionName(name);
355
- await this.run(["kill-session", "-t", name]);
356
- }
357
- async readSession(name) {
358
- const sessions = await this.listSessions();
359
- return sessions.find((session) => session.name === name) ?? null;
360
- }
361
- async hasSession(name) {
362
- try {
363
- await this.run(["has-session", "-t", name]);
364
- return true;
365
- } catch (error) {
366
- const message = String(
367
- error.stderr ?? error.message
368
- );
369
- if (isTmuxEmptyStateMessage(message)) {
370
- return false;
371
- }
372
- return false;
373
- }
374
- }
375
- async getPreview(name) {
376
- try {
377
- const stdout = await this.run(
378
- ["capture-pane", "-p", "-J", "-S", "-18", "-E", "-", "-t", `${name}:`],
379
- { allowEmptyState: true }
380
- );
381
- return formatPreview(stdout);
382
- } catch {
383
- return ["Session available. Tap to attach."];
384
- }
385
- }
386
- async run(args, options = {}) {
55
+ // src/cli.ts
56
+ async function loadAgentRuntime() {
57
+ const [{ AgentConnection }, { TmuxClient }] = await Promise.all([
58
+ import("./connection-RJY775NL.js"),
59
+ import("./tmux-QIB4H3UA.js")
60
+ ]);
61
+ return { AgentConnection, TmuxClient };
62
+ }
63
+ function createProgram() {
64
+ const program = new Command();
65
+ program.name("webmux-agent").description("Webmux agent \u2014 connects your machine to the webmux server").version(AGENT_VERSION);
66
+ program.command("register").description("Register this agent with a webmux server").requiredOption("--server <url>", "Server URL (e.g. https://webmux.example.com)").requiredOption("--token <token>", "One-time registration token from the server").option("--name <name>", "Display name for this agent (defaults to hostname)").action(async (opts) => {
67
+ const serverUrl = opts.server.replace(/\/+$/, "");
68
+ const agentName = opts.name ?? os2.hostname();
69
+ console.log(`[agent] Registering with server ${serverUrl}...`);
70
+ console.log(`[agent] Agent name: ${agentName}`);
71
+ const body = {
72
+ token: opts.token,
73
+ name: agentName
74
+ };
75
+ let response;
387
76
  try {
388
- const { stdout } = await execFileAsync(
389
- "tmux",
390
- ["-L", this.socketName, ...args],
391
- {
392
- cwd: this.workspaceRoot,
393
- env: {
394
- ...process.env,
395
- TERM: "xterm-256color"
396
- }
397
- }
398
- );
399
- return stdout;
400
- } catch (error) {
401
- const message = String(
402
- error.stderr ?? error.message
403
- );
404
- if (options.allowEmptyState && TMUX_EMPTY_STATE_MARKERS.some((marker) => message.includes(marker))) {
405
- return "";
406
- }
407
- throw error;
408
- }
409
- }
410
- };
411
- function assertValidSessionName(name) {
412
- if (!SESSION_NAME_PATTERN.test(name)) {
413
- throw new Error(
414
- "Invalid session name. Use up to 32 letters, numbers, dot, dash, or underscore."
415
- );
416
- }
417
- }
418
- function parseSessionList(stdout) {
419
- return stdout.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
420
- const parts = line.split(FIELD_SEPARATOR);
421
- const [name, windows, attachedClients, createdAt, lastActivityAt, path3] = parts;
422
- const currentCommand = parts[6] ?? "";
423
- if (!name || !windows || !attachedClients || !createdAt || !lastActivityAt || !path3) {
424
- return [];
425
- }
426
- return [
427
- {
428
- name,
429
- windows: Number(windows),
430
- attachedClients: Number(attachedClients),
431
- createdAt: Number(createdAt),
432
- lastActivityAt: Number(lastActivityAt),
433
- path: path3,
434
- currentCommand
435
- }
436
- ];
437
- });
438
- }
439
- function formatPreview(stdout) {
440
- const lines = stdout.replaceAll("\r", "").split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).slice(-3);
441
- if (lines.length > 0) {
442
- return lines;
443
- }
444
- return ["Fresh session. Nothing has run yet."];
445
- }
446
- function isTmuxEmptyStateMessage(message) {
447
- return TMUX_EMPTY_STATE_MARKERS.some((marker) => message.includes(marker));
448
- }
449
-
450
- // src/terminal.ts
451
- async function createTerminalBridge(options) {
452
- const {
453
- tmux,
454
- sessionName,
455
- cols = DEFAULT_TERMINAL_SIZE.cols,
456
- rows = DEFAULT_TERMINAL_SIZE.rows,
457
- onData,
458
- onExit
459
- } = options;
460
- assertValidSessionName(sessionName);
461
- await tmux.createSession(sessionName);
462
- const ptyProcess = spawn(
463
- "tmux",
464
- ["-L", tmux.socketName, "attach-session", "-t", sessionName],
465
- {
466
- cols,
467
- rows,
468
- cwd: tmux.workspaceRoot,
469
- env: {
470
- ...process.env,
471
- TERM: "xterm-256color"
472
- },
473
- name: "xterm-256color"
474
- }
475
- );
476
- ptyProcess.onData(onData);
477
- ptyProcess.onExit(({ exitCode }) => {
478
- onExit(exitCode);
479
- });
480
- return {
481
- write(data) {
482
- ptyProcess.write(data);
483
- },
484
- resize(nextCols, nextRows) {
485
- ptyProcess.resize(nextCols, nextRows);
486
- },
487
- dispose() {
488
- ptyProcess.kill();
489
- }
490
- };
491
- }
492
-
493
- // src/run-wrapper.ts
494
- import { spawn as spawn2 } from "node-pty";
495
- var STATUS_DEBOUNCE_MS = 300;
496
- var OUTPUT_BUFFER_MAX_LINES = 20;
497
- var CLAUDE_APPROVAL_PATTERNS = [
498
- /do you want to/i,
499
- /\ballow\b/i,
500
- /\bdeny\b/i,
501
- /\bpermission\b/i,
502
- /proceed\?/i
503
- ];
504
- var CLAUDE_INPUT_PATTERNS = [
505
- /^>\s*$/m,
506
- /❯/,
507
- /\$ $/m
508
- ];
509
- var CODEX_APPROVAL_PATTERNS = [
510
- /apply changes/i,
511
- /\[y\/n\]/i,
512
- /\bapprove\b/i
513
- ];
514
- var CODEX_INPUT_PATTERNS = [
515
- /what would you like/i,
516
- /❯/,
517
- /^>\s*$/m
518
- ];
519
- function matchesAny(text, patterns) {
520
- return patterns.some((pattern) => pattern.test(text));
521
- }
522
- var RunWrapper = class {
523
- runId;
524
- tool;
525
- repoPath;
526
- prompt;
527
- tmux;
528
- onEvent;
529
- onOutput;
530
- ptyProcess = null;
531
- currentStatus = "starting";
532
- outputBuffer = [];
533
- debounceTimer = null;
534
- disposed = false;
535
- sessionName;
536
- constructor(options) {
537
- this.runId = options.runId;
538
- this.tool = options.tool;
539
- this.repoPath = options.repoPath;
540
- this.prompt = options.prompt;
541
- this.tmux = options.tmux;
542
- this.onEvent = options.onEvent;
543
- this.onOutput = options.onOutput;
544
- const shortId = this.runId.slice(0, 8);
545
- this.sessionName = `run-${shortId}`;
546
- }
547
- async start() {
548
- if (this.disposed) {
549
- return;
550
- }
551
- this.emitStatus("starting");
552
- await this.tmux.createSession(this.sessionName);
553
- const command = this.buildCommand();
554
- const ptyProcess = spawn2(
555
- "tmux",
556
- ["-L", this.tmux.socketName, "attach-session", "-t", this.sessionName],
557
- {
558
- cols: 120,
559
- rows: 36,
560
- cwd: this.repoPath,
561
- env: {
562
- ...process.env,
563
- TERM: "xterm-256color"
564
- },
565
- name: "xterm-256color"
566
- }
567
- );
568
- this.ptyProcess = ptyProcess;
569
- ptyProcess.onData((data) => {
570
- if (this.disposed) {
571
- return;
572
- }
573
- this.onOutput(data);
574
- this.appendToBuffer(data);
575
- this.scheduleStatusDetection();
576
- });
577
- ptyProcess.onExit(({ exitCode }) => {
578
- if (this.disposed) {
579
- return;
580
- }
581
- if (this.debounceTimer) {
582
- clearTimeout(this.debounceTimer);
583
- this.debounceTimer = null;
584
- }
585
- if (exitCode === 0) {
586
- this.emitStatus("success");
587
- } else {
588
- this.emitStatus("failed");
589
- }
590
- this.ptyProcess = null;
591
- });
592
- setTimeout(() => {
593
- if (this.ptyProcess && !this.disposed) {
594
- this.ptyProcess.write(command + "\n");
595
- this.emitStatus("running");
596
- }
597
- }, 500);
598
- }
599
- sendInput(input) {
600
- if (this.ptyProcess && !this.disposed) {
601
- this.ptyProcess.write(input);
602
- }
603
- }
604
- interrupt() {
605
- if (this.ptyProcess && !this.disposed) {
606
- this.ptyProcess.write("");
607
- this.emitStatus("interrupted");
608
- }
609
- }
610
- approve() {
611
- if (this.ptyProcess && !this.disposed) {
612
- this.ptyProcess.write("y\n");
613
- }
614
- }
615
- reject() {
616
- if (this.ptyProcess && !this.disposed) {
617
- this.ptyProcess.write("n\n");
618
- }
619
- }
620
- dispose() {
621
- if (this.disposed) {
622
- return;
623
- }
624
- this.disposed = true;
625
- if (this.debounceTimer) {
626
- clearTimeout(this.debounceTimer);
627
- this.debounceTimer = null;
628
- }
629
- if (this.ptyProcess) {
630
- this.ptyProcess.kill();
631
- this.ptyProcess = null;
632
- }
633
- this.tmux.killSession(this.sessionName).catch(() => {
634
- });
635
- }
636
- buildCommand() {
637
- const escapedPrompt = this.prompt.replace(/'/g, "'\\''");
638
- switch (this.tool) {
639
- case "claude":
640
- return `cd '${this.repoPath.replace(/'/g, "'\\''")}' && claude '${escapedPrompt}'`;
641
- case "codex":
642
- return `cd '${this.repoPath.replace(/'/g, "'\\''")}' && codex '${escapedPrompt}'`;
643
- }
644
- }
645
- appendToBuffer(data) {
646
- const newLines = data.split("\n");
647
- this.outputBuffer.push(...newLines);
648
- if (this.outputBuffer.length > OUTPUT_BUFFER_MAX_LINES) {
649
- this.outputBuffer = this.outputBuffer.slice(-OUTPUT_BUFFER_MAX_LINES);
650
- }
651
- }
652
- scheduleStatusDetection() {
653
- if (this.debounceTimer) {
654
- clearTimeout(this.debounceTimer);
655
- }
656
- this.debounceTimer = setTimeout(() => {
657
- this.debounceTimer = null;
658
- this.detectStatus();
659
- }, STATUS_DEBOUNCE_MS);
660
- }
661
- detectStatus() {
662
- if (this.disposed) {
663
- return;
664
- }
665
- if (this.currentStatus === "success" || this.currentStatus === "failed" || this.currentStatus === "interrupted") {
666
- return;
667
- }
668
- const recentText = this.outputBuffer.join("\n");
669
- const detectedStatus = this.detectStatusFromText(recentText);
670
- if (detectedStatus && detectedStatus !== this.currentStatus) {
671
- this.emitStatus(detectedStatus);
672
- }
673
- }
674
- detectStatusFromText(text) {
675
- const approvalPatterns = this.tool === "claude" ? CLAUDE_APPROVAL_PATTERNS : CODEX_APPROVAL_PATTERNS;
676
- if (matchesAny(text, approvalPatterns)) {
677
- return "waiting_approval";
678
- }
679
- const inputPatterns = this.tool === "claude" ? CLAUDE_INPUT_PATTERNS : CODEX_INPUT_PATTERNS;
680
- if (matchesAny(text, inputPatterns)) {
681
- return "waiting_input";
682
- }
683
- if (text.trim().length > 0) {
684
- return "running";
685
- }
686
- return null;
687
- }
688
- emitStatus(status, summary, hasDiff) {
689
- this.currentStatus = status;
690
- this.onEvent(status, summary, hasDiff);
691
- }
692
- };
693
-
694
- // src/version.ts
695
- import fs3 from "fs";
696
- var packageMetadata = readAgentPackageMetadata();
697
- var AGENT_PACKAGE_NAME = packageMetadata.name;
698
- var AGENT_VERSION = packageMetadata.version;
699
- function readAgentPackageMetadata() {
700
- const packageJsonPath = new URL("../package.json", import.meta.url);
701
- const raw = fs3.readFileSync(packageJsonPath, "utf-8");
702
- const parsed = JSON.parse(raw);
703
- if (!parsed.name || !parsed.version) {
704
- throw new Error("Agent package metadata is missing name or version");
705
- }
706
- return {
707
- name: parsed.name,
708
- version: parsed.version
709
- };
710
- }
711
-
712
- // src/connection.ts
713
- var HEARTBEAT_INTERVAL_MS = 3e4;
714
- var SESSION_SYNC_INTERVAL_MS = 15e3;
715
- var INITIAL_RECONNECT_DELAY_MS = 1e3;
716
- var MAX_RECONNECT_DELAY_MS = 3e4;
717
- var defaultAgentRuntime = {
718
- version: AGENT_VERSION,
719
- serviceMode: process.env.WEBMUX_AGENT_SERVICE === "1",
720
- autoUpgrade: process.env.WEBMUX_AGENT_AUTO_UPGRADE !== "0",
721
- applyServiceUpgrade: ({ packageName, targetVersion }) => {
722
- upgradeService({
723
- agentName: process.env.WEBMUX_AGENT_NAME ?? "webmux-agent",
724
- packageName,
725
- version: targetVersion
726
- });
727
- },
728
- exit: (code) => {
729
- process.exit(code);
730
- }
731
- };
732
- var AgentConnection = class {
733
- serverUrl;
734
- agentId;
735
- agentSecret;
736
- tmux;
737
- runtime;
738
- ws = null;
739
- heartbeatTimer = null;
740
- sessionSyncTimer = null;
741
- reconnectTimer = null;
742
- reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
743
- bridges = /* @__PURE__ */ new Map();
744
- runs = /* @__PURE__ */ new Map();
745
- stopped = false;
746
- constructor(serverUrl, agentId, agentSecret, tmux, runtime = defaultAgentRuntime) {
747
- this.serverUrl = serverUrl;
748
- this.agentId = agentId;
749
- this.agentSecret = agentSecret;
750
- this.tmux = tmux;
751
- this.runtime = runtime;
752
- }
753
- start() {
754
- this.stopped = false;
755
- this.connect();
756
- }
757
- stop() {
758
- this.stopped = true;
759
- if (this.reconnectTimer) {
760
- clearTimeout(this.reconnectTimer);
761
- this.reconnectTimer = null;
762
- }
763
- this.stopHeartbeat();
764
- this.stopSessionSync();
765
- this.disposeAllBridges();
766
- this.disposeAllRuns();
767
- if (this.ws) {
768
- this.ws.close(1e3, "agent shutting down");
769
- this.ws = null;
770
- }
771
- }
772
- connect() {
773
- const wsUrl = buildWsUrl(this.serverUrl);
774
- console.log(`[agent] Connecting to ${wsUrl}`);
775
- const ws = new WebSocket(wsUrl);
776
- this.ws = ws;
777
- ws.on("open", () => {
778
- console.log("[agent] WebSocket connected, authenticating...");
779
- this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
780
- this.sendMessage({
781
- type: "auth",
782
- agentId: this.agentId,
783
- agentSecret: this.agentSecret,
784
- version: this.runtime.version
77
+ response = await fetch(`${serverUrl}/api/agents/register`, {
78
+ method: "POST",
79
+ headers: { "Content-Type": "application/json" },
80
+ body: JSON.stringify(body)
785
81
  });
786
- });
787
- ws.on("message", (raw) => {
788
- let msg;
82
+ } catch (err) {
83
+ const message = err instanceof Error ? err.message : String(err);
84
+ console.error(`[agent] Failed to connect to server: ${message}`);
85
+ process.exit(1);
86
+ }
87
+ if (!response.ok) {
88
+ let errorMessage;
789
89
  try {
790
- msg = JSON.parse(raw.toString());
90
+ const errorBody = await response.json();
91
+ errorMessage = errorBody.error ?? response.statusText;
791
92
  } catch {
792
- console.error("[agent] Failed to parse server message:", raw.toString());
793
- return;
93
+ errorMessage = response.statusText;
794
94
  }
795
- this.handleMessage(msg);
95
+ console.error(`[agent] Registration failed: ${errorMessage}`);
96
+ process.exit(1);
97
+ }
98
+ const result = await response.json();
99
+ saveCredentials({
100
+ serverUrl,
101
+ agentId: result.agentId,
102
+ agentSecret: result.agentSecret,
103
+ name: agentName
796
104
  });
797
- ws.on("close", (code, reason) => {
798
- console.log(`[agent] WebSocket closed: code=${code} reason=${reason.toString()}`);
799
- this.onDisconnect();
800
- });
801
- ws.on("error", (err) => {
802
- console.error("[agent] WebSocket error:", err.message);
105
+ console.log(`[agent] Registration successful!`);
106
+ console.log(`[agent] Agent ID: ${result.agentId}`);
107
+ console.log(`[agent] Credentials saved to ${credentialsPath()}`);
108
+ console.log(``);
109
+ console.log(`Next steps:`);
110
+ console.log(` pnpm dlx @webmux/agent start # run once`);
111
+ console.log(` pnpm dlx @webmux/agent service install # install as managed systemd service`);
112
+ });
113
+ program.command("start").description("Start the agent and connect to the server").action(async () => {
114
+ const creds = loadCredentials();
115
+ if (!creds) {
116
+ console.error(
117
+ `[agent] No credentials found at ${credentialsPath()}. Run "npx @webmux/agent register" first.`
118
+ );
119
+ process.exit(1);
120
+ }
121
+ const { AgentConnection, TmuxClient } = await loadAgentRuntime();
122
+ console.log(`[agent] Starting agent "${creds.name}"...`);
123
+ console.log(`[agent] Server: ${creds.serverUrl}`);
124
+ console.log(`[agent] Agent ID: ${creds.agentId}`);
125
+ const tmux = new TmuxClient({
126
+ socketName: "webmux",
127
+ workspaceRoot: os2.homedir()
803
128
  });
804
- }
805
- handleMessage(msg) {
806
- switch (msg.type) {
807
- case "auth-ok":
808
- console.log("[agent] Authenticated successfully");
809
- if (this.applyRecommendedUpgrade(msg.upgradePolicy)) {
810
- return;
811
- }
812
- this.startHeartbeat();
813
- this.startSessionSync();
814
- this.syncSessions();
815
- break;
816
- case "auth-fail":
817
- console.error(`[agent] Authentication failed: ${msg.message}`);
818
- this.stopped = true;
819
- if (this.ws) {
820
- this.ws.close();
821
- this.ws = null;
822
- }
823
- this.runtime.exit(1);
824
- break;
825
- case "sessions-list":
826
- this.syncSessions();
827
- break;
828
- case "terminal-attach":
829
- this.handleTerminalAttach(msg.browserId, msg.sessionName, msg.cols, msg.rows);
830
- break;
831
- case "terminal-detach":
832
- this.handleTerminalDetach(msg.browserId);
833
- break;
834
- case "terminal-input":
835
- this.handleTerminalInput(msg.browserId, msg.data);
836
- break;
837
- case "terminal-resize":
838
- this.handleTerminalResize(msg.browserId, msg.cols, msg.rows);
839
- break;
840
- case "session-create":
841
- this.handleSessionCreate(msg.requestId, msg.name);
842
- break;
843
- case "session-kill":
844
- this.handleSessionKill(msg.requestId, msg.name);
845
- break;
846
- case "run-start":
847
- this.handleRunStart(msg.runId, msg.tool, msg.repoPath, msg.prompt);
848
- break;
849
- case "run-input":
850
- this.handleRunInput(msg.runId, msg.input);
851
- break;
852
- case "run-interrupt":
853
- this.handleRunInterrupt(msg.runId);
854
- break;
855
- case "run-approve":
856
- this.handleRunApprove(msg.runId);
857
- break;
858
- case "run-reject":
859
- this.handleRunReject(msg.runId);
860
- break;
861
- default:
862
- console.warn("[agent] Unknown message type:", msg.type);
863
- }
864
- }
865
- async syncSessions() {
866
- try {
867
- const sessions = await this.tmux.listSessions();
868
- this.sendMessage({ type: "sessions-sync", sessions });
869
- return sessions;
870
- } catch (err) {
871
- console.error("[agent] Failed to list sessions:", err);
872
- this.sendMessage({ type: "error", message: "Failed to list sessions" });
873
- return [];
874
- }
875
- }
876
- async handleTerminalAttach(browserId, sessionName, cols, rows) {
877
- const existing = this.bridges.get(browserId);
878
- if (existing) {
879
- existing.dispose();
880
- this.bridges.delete(browserId);
881
- }
129
+ const connection = new AgentConnection(
130
+ creds.serverUrl,
131
+ creds.agentId,
132
+ creds.agentSecret,
133
+ tmux
134
+ );
135
+ const shutdown = () => {
136
+ console.log("\n[agent] Shutting down...");
137
+ connection.stop();
138
+ process.exit(0);
139
+ };
140
+ process.on("SIGINT", shutdown);
141
+ process.on("SIGTERM", shutdown);
142
+ connection.start();
143
+ });
144
+ program.command("status").description("Show agent status and credentials info").action(() => {
145
+ const creds = loadCredentials();
146
+ if (!creds) {
147
+ console.log(`[agent] Not registered. No credentials found at ${credentialsPath()}.`);
148
+ process.exit(0);
149
+ }
150
+ console.log(`Agent Name: ${creds.name}`);
151
+ console.log(`Agent Version: ${AGENT_VERSION}`);
152
+ console.log(`Server URL: ${creds.serverUrl}`);
153
+ console.log(`Agent ID: ${creds.agentId}`);
154
+ console.log(`Credentials File: ${credentialsPath()}`);
155
+ const installedService = readInstalledServiceConfig();
882
156
  try {
883
- const bridge = await createTerminalBridge({
884
- tmux: this.tmux,
885
- sessionName,
886
- cols,
887
- rows,
888
- onData: (data) => {
889
- this.sendMessage({ type: "terminal-output", browserId, data });
890
- },
891
- onExit: (exitCode) => {
892
- this.bridges.delete(browserId);
893
- this.sendMessage({ type: "terminal-exit", browserId, exitCode });
894
- void this.syncSessions();
895
- }
896
- });
897
- this.bridges.set(browserId, bridge);
898
- this.sendMessage({ type: "terminal-ready", browserId, sessionName });
899
- await this.syncSessions();
900
- } catch (err) {
901
- const message = err instanceof Error ? err.message : String(err);
902
- console.error(`[agent] Failed to attach terminal for browser ${browserId}:`, message);
903
- this.sendMessage({ type: "error", browserId, message: `Failed to attach: ${message}` });
157
+ const result = execFileSync("systemctl", ["--user", "is-active", SERVICE_NAME], { encoding: "utf-8" }).trim();
158
+ console.log(`Service: ${result}`);
159
+ } catch {
160
+ console.log(`Service: not installed`);
904
161
  }
905
- }
906
- handleTerminalDetach(browserId) {
907
- const bridge = this.bridges.get(browserId);
908
- if (bridge) {
909
- bridge.dispose();
910
- this.bridges.delete(browserId);
911
- void this.syncSessions();
162
+ if (installedService?.version) {
163
+ console.log(`Service Version: ${installedService.version}`);
912
164
  }
913
- }
914
- handleTerminalInput(browserId, data) {
915
- const bridge = this.bridges.get(browserId);
916
- if (bridge) {
917
- bridge.write(data);
918
- }
919
- }
920
- handleTerminalResize(browserId, cols, rows) {
921
- const bridge = this.bridges.get(browserId);
922
- if (bridge) {
923
- bridge.resize(cols, rows);
165
+ });
166
+ const service = program.command("service").description("Manage the systemd service");
167
+ service.command("install").description("Install and start the agent as a managed systemd user service").option("--no-auto-upgrade", "Disable automatic upgrades for the managed service").action((opts) => {
168
+ const creds = loadCredentials();
169
+ if (!creds) {
170
+ console.error(`[agent] Not registered. Run "npx @webmux/agent register" first.`);
171
+ process.exit(1);
924
172
  }
925
- }
926
- async handleSessionCreate(requestId, name) {
927
173
  try {
928
- await this.tmux.createSession(name);
929
- const sessions = await this.syncSessions();
930
- const session = sessions.find((item) => item.name === name);
931
- if (!session) {
932
- throw new Error("Created session was not returned by tmux");
933
- }
934
- this.sendMessage({ type: "command-result", requestId, ok: true, session });
174
+ installService({
175
+ agentName: creds.name,
176
+ packageName: AGENT_PACKAGE_NAME,
177
+ version: AGENT_VERSION,
178
+ autoUpgrade: opts.autoUpgrade
179
+ });
180
+ console.log(``);
181
+ console.log(`[agent] Service installed and started!`);
182
+ console.log(`[agent] Managed version: ${AGENT_VERSION}`);
183
+ console.log(`[agent] It will auto-start on boot.`);
184
+ console.log(``);
185
+ console.log(`Useful commands:`);
186
+ console.log(` systemctl --user status ${SERVICE_NAME}`);
187
+ console.log(` journalctl --user -u ${SERVICE_NAME} -f`);
188
+ console.log(` pnpm dlx @webmux/agent service upgrade --to <version>`);
189
+ console.log(` pnpm dlx @webmux/agent service uninstall`);
935
190
  } catch (err) {
936
191
  const message = err instanceof Error ? err.message : String(err);
937
- console.error(`[agent] Failed to create session "${name}":`, message);
938
- this.sendMessage({ type: "command-result", requestId, ok: false, error: message });
192
+ console.error(`[agent] Failed to install managed service: ${message}`);
193
+ console.error(`[agent] Service file path: ${servicePath()}`);
194
+ process.exit(1);
939
195
  }
940
- }
941
- async handleSessionKill(requestId, name) {
196
+ });
197
+ service.command("uninstall").description("Stop and remove the systemd user service").action(() => {
942
198
  try {
943
- await this.tmux.killSession(name);
944
- await this.syncSessions();
945
- this.sendMessage({ type: "command-result", requestId, ok: true });
199
+ uninstallService();
200
+ console.log(`[agent] Service file removed: ${servicePath()}`);
201
+ console.log(`[agent] Service uninstalled.`);
946
202
  } catch (err) {
947
203
  const message = err instanceof Error ? err.message : String(err);
948
- console.error(`[agent] Failed to kill session "${name}":`, message);
949
- this.sendMessage({ type: "command-result", requestId, ok: false, error: message });
950
- }
951
- }
952
- sendMessage(msg) {
953
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
954
- this.ws.send(JSON.stringify(msg));
204
+ console.error(`[agent] Failed to uninstall service: ${message}`);
205
+ process.exit(1);
955
206
  }
956
- }
957
- applyRecommendedUpgrade(upgradePolicy) {
958
- const targetVersion = upgradePolicy?.targetVersion;
959
- if (!targetVersion) {
960
- return false;
961
- }
962
- let comparison;
207
+ });
208
+ service.command("status").description("Show systemd service status").action(() => {
963
209
  try {
964
- comparison = compareSemanticVersions(this.runtime.version, targetVersion);
210
+ execFileSync("systemctl", ["--user", "status", SERVICE_NAME], { stdio: "inherit" });
965
211
  } catch {
966
- console.warn("[agent] Skipping automatic upgrade because version parsing failed");
967
- return false;
968
- }
969
- if (comparison >= 0) {
970
- return false;
212
+ console.log(`[agent] Service is not installed or not running.`);
971
213
  }
972
- console.log(`[agent] Update available: ${this.runtime.version} \u2192 ${targetVersion}`);
973
- if (!this.runtime.serviceMode || !this.runtime.autoUpgrade) {
974
- console.log("[agent] Automatic upgrades are only applied for the managed systemd service");
975
- console.log(`[agent] To upgrade manually, run: pnpm dlx @webmux/agent service upgrade --to ${targetVersion}`);
976
- return false;
214
+ });
215
+ service.command("upgrade").description("Switch the managed service to a specific agent version and restart it").requiredOption("--to <version>", "Target agent version (for example 0.1.5)").action((opts) => {
216
+ const creds = loadCredentials();
217
+ if (!creds) {
218
+ console.error(`[agent] Not registered. Run "npx @webmux/agent register" first.`);
219
+ process.exit(1);
977
220
  }
221
+ console.log(`[agent] Switching managed service to ${opts.to}...`);
978
222
  try {
979
- this.runtime.applyServiceUpgrade({
980
- packageName: upgradePolicy.packageName || AGENT_PACKAGE_NAME,
981
- targetVersion
223
+ upgradeService({
224
+ agentName: creds.name,
225
+ packageName: AGENT_PACKAGE_NAME,
226
+ version: opts.to
982
227
  });
983
- console.log(`[agent] Managed service switched to ${targetVersion}. Restarting...`);
228
+ console.log("[agent] Managed service updated successfully.");
984
229
  } catch (err) {
985
230
  const message = err instanceof Error ? err.message : String(err);
986
- console.error(`[agent] Failed to apply managed upgrade: ${message}`);
987
- console.log("[agent] Continuing with current version");
988
- return false;
989
- }
990
- this.stop();
991
- this.runtime.exit(0);
992
- return true;
993
- }
994
- startHeartbeat() {
995
- this.stopHeartbeat();
996
- this.heartbeatTimer = setInterval(() => {
997
- this.sendMessage({ type: "heartbeat" });
998
- }, HEARTBEAT_INTERVAL_MS);
999
- }
1000
- startSessionSync() {
1001
- this.stopSessionSync();
1002
- this.sessionSyncTimer = setInterval(() => {
1003
- void this.syncSessions();
1004
- }, SESSION_SYNC_INTERVAL_MS);
1005
- }
1006
- stopHeartbeat() {
1007
- if (this.heartbeatTimer) {
1008
- clearInterval(this.heartbeatTimer);
1009
- this.heartbeatTimer = null;
1010
- }
1011
- }
1012
- stopSessionSync() {
1013
- if (this.sessionSyncTimer) {
1014
- clearInterval(this.sessionSyncTimer);
1015
- this.sessionSyncTimer = null;
1016
- }
1017
- }
1018
- disposeAllBridges() {
1019
- for (const [browserId, bridge] of this.bridges) {
1020
- bridge.dispose();
1021
- this.bridges.delete(browserId);
1022
- }
1023
- }
1024
- disposeAllRuns() {
1025
- for (const [runId, run] of this.runs) {
1026
- run.dispose();
1027
- this.runs.delete(runId);
1028
- }
1029
- }
1030
- handleRunStart(runId, tool, repoPath, prompt) {
1031
- const existing = this.runs.get(runId);
1032
- if (existing) {
1033
- existing.dispose();
1034
- this.runs.delete(runId);
1035
- }
1036
- const run = new RunWrapper({
1037
- runId,
1038
- tool,
1039
- repoPath,
1040
- prompt,
1041
- tmux: this.tmux,
1042
- onEvent: (status, summary, hasDiff) => {
1043
- this.sendMessage({ type: "run-event", runId, status, summary, hasDiff });
1044
- },
1045
- onOutput: (data) => {
1046
- this.sendMessage({ type: "run-output", runId, data });
1047
- }
1048
- });
1049
- this.runs.set(runId, run);
1050
- run.start().catch((err) => {
1051
- const message = err instanceof Error ? err.message : String(err);
1052
- console.error(`[agent] Failed to start run ${runId}:`, message);
1053
- this.sendMessage({
1054
- type: "run-event",
1055
- runId,
1056
- status: "failed",
1057
- summary: `Failed to start: ${message}`
1058
- });
1059
- this.runs.delete(runId);
1060
- });
1061
- }
1062
- handleRunInput(runId, input) {
1063
- const run = this.runs.get(runId);
1064
- if (run) {
1065
- run.sendInput(input);
1066
- } else {
1067
- console.warn(`[agent] run-input: no run found for ${runId}`);
1068
- }
1069
- }
1070
- handleRunInterrupt(runId) {
1071
- const run = this.runs.get(runId);
1072
- if (run) {
1073
- run.interrupt();
1074
- } else {
1075
- console.warn(`[agent] run-interrupt: no run found for ${runId}`);
1076
- }
1077
- }
1078
- handleRunApprove(runId) {
1079
- const run = this.runs.get(runId);
1080
- if (run) {
1081
- run.approve();
1082
- } else {
1083
- console.warn(`[agent] run-approve: no run found for ${runId}`);
1084
- }
1085
- }
1086
- handleRunReject(runId) {
1087
- const run = this.runs.get(runId);
1088
- if (run) {
1089
- run.reject();
1090
- } else {
1091
- console.warn(`[agent] run-reject: no run found for ${runId}`);
231
+ console.error(`[agent] Failed to upgrade managed service: ${message}`);
232
+ process.exit(1);
1092
233
  }
1093
- }
1094
- onDisconnect() {
1095
- this.stopHeartbeat();
1096
- this.stopSessionSync();
1097
- this.disposeAllBridges();
1098
- this.ws = null;
1099
- if (this.stopped) {
1100
- return;
1101
- }
1102
- console.log(`[agent] Reconnecting in ${this.reconnectDelay}ms...`);
1103
- this.reconnectTimer = setTimeout(() => {
1104
- this.reconnectTimer = null;
1105
- this.connect();
1106
- }, this.reconnectDelay);
1107
- this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
1108
- }
1109
- };
1110
- function buildWsUrl(serverUrl) {
1111
- const url = new URL("/ws/agent", serverUrl);
1112
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1113
- return url.toString();
1114
- }
1115
-
1116
- // src/cli.ts
1117
- var program = new Command();
1118
- program.name("webmux-agent").description("Webmux agent \u2014 connects your machine to the webmux server").version(AGENT_VERSION);
1119
- program.command("register").description("Register this agent with a webmux server").requiredOption("--server <url>", "Server URL (e.g. https://webmux.example.com)").requiredOption("--token <token>", "One-time registration token from the server").option("--name <name>", "Display name for this agent (defaults to hostname)").action(async (opts) => {
1120
- const serverUrl = opts.server.replace(/\/+$/, "");
1121
- const agentName = opts.name ?? os3.hostname();
1122
- console.log(`[agent] Registering with server ${serverUrl}...`);
1123
- console.log(`[agent] Agent name: ${agentName}`);
1124
- const body = {
1125
- token: opts.token,
1126
- name: agentName
1127
- };
1128
- let response;
1129
- try {
1130
- response = await fetch(`${serverUrl}/api/agents/register`, {
1131
- method: "POST",
1132
- headers: { "Content-Type": "application/json" },
1133
- body: JSON.stringify(body)
1134
- });
1135
- } catch (err) {
1136
- const message = err instanceof Error ? err.message : String(err);
1137
- console.error(`[agent] Failed to connect to server: ${message}`);
1138
- process.exit(1);
1139
- }
1140
- if (!response.ok) {
1141
- let errorMessage;
1142
- try {
1143
- const errorBody = await response.json();
1144
- errorMessage = errorBody.error ?? response.statusText;
1145
- } catch {
1146
- errorMessage = response.statusText;
1147
- }
1148
- console.error(`[agent] Registration failed: ${errorMessage}`);
1149
- process.exit(1);
1150
- }
1151
- const result = await response.json();
1152
- saveCredentials({
1153
- serverUrl,
1154
- agentId: result.agentId,
1155
- agentSecret: result.agentSecret,
1156
- name: agentName
1157
- });
1158
- console.log(`[agent] Registration successful!`);
1159
- console.log(`[agent] Agent ID: ${result.agentId}`);
1160
- console.log(`[agent] Credentials saved to ${credentialsPath()}`);
1161
- console.log(``);
1162
- console.log(`Next steps:`);
1163
- console.log(` pnpm dlx @webmux/agent start # run once`);
1164
- console.log(` pnpm dlx @webmux/agent service install # install as managed systemd service`);
1165
- });
1166
- program.command("start").description("Start the agent and connect to the server").action(() => {
1167
- const creds = loadCredentials();
1168
- if (!creds) {
1169
- console.error(
1170
- `[agent] No credentials found at ${credentialsPath()}. Run "npx @webmux/agent register" first.`
1171
- );
1172
- process.exit(1);
1173
- }
1174
- console.log(`[agent] Starting agent "${creds.name}"...`);
1175
- console.log(`[agent] Server: ${creds.serverUrl}`);
1176
- console.log(`[agent] Agent ID: ${creds.agentId}`);
1177
- const tmux = new TmuxClient({
1178
- socketName: "webmux",
1179
- workspaceRoot: os3.homedir()
1180
234
  });
1181
- const connection = new AgentConnection(
1182
- creds.serverUrl,
1183
- creds.agentId,
1184
- creds.agentSecret,
1185
- tmux
1186
- );
1187
- const shutdown = () => {
1188
- console.log("\n[agent] Shutting down...");
1189
- connection.stop();
1190
- process.exit(0);
1191
- };
1192
- process.on("SIGINT", shutdown);
1193
- process.on("SIGTERM", shutdown);
1194
- connection.start();
1195
- });
1196
- program.command("status").description("Show agent status and credentials info").action(() => {
1197
- const creds = loadCredentials();
1198
- if (!creds) {
1199
- console.log(`[agent] Not registered. No credentials found at ${credentialsPath()}.`);
1200
- process.exit(0);
1201
- }
1202
- console.log(`Agent Name: ${creds.name}`);
1203
- console.log(`Agent Version: ${AGENT_VERSION}`);
1204
- console.log(`Server URL: ${creds.serverUrl}`);
1205
- console.log(`Agent ID: ${creds.agentId}`);
1206
- console.log(`Credentials File: ${credentialsPath()}`);
1207
- const installedService = readInstalledServiceConfig();
1208
- try {
1209
- const result = execFileSync2("systemctl", ["--user", "is-active", SERVICE_NAME], { encoding: "utf-8" }).trim();
1210
- console.log(`Service: ${result}`);
1211
- } catch {
1212
- console.log(`Service: not installed`);
1213
- }
1214
- if (installedService?.version) {
1215
- console.log(`Service Version: ${installedService.version}`);
1216
- }
1217
- });
1218
- var service = program.command("service").description("Manage the systemd service");
1219
- service.command("install").description("Install and start the agent as a managed systemd user service").option("--no-auto-upgrade", "Disable automatic upgrades for the managed service").action((opts) => {
1220
- const creds = loadCredentials();
1221
- if (!creds) {
1222
- console.error(`[agent] Not registered. Run "npx @webmux/agent register" first.`);
1223
- process.exit(1);
1224
- }
1225
- try {
1226
- installService({
1227
- agentName: creds.name,
1228
- packageName: AGENT_PACKAGE_NAME,
1229
- version: AGENT_VERSION,
1230
- autoUpgrade: opts.autoUpgrade
1231
- });
1232
- console.log(``);
1233
- console.log(`[agent] Service installed and started!`);
1234
- console.log(`[agent] Managed version: ${AGENT_VERSION}`);
1235
- console.log(`[agent] It will auto-start on boot.`);
1236
- console.log(``);
1237
- console.log(`Useful commands:`);
1238
- console.log(` systemctl --user status ${SERVICE_NAME}`);
1239
- console.log(` journalctl --user -u ${SERVICE_NAME} -f`);
1240
- console.log(` pnpm dlx @webmux/agent service upgrade --to <version>`);
1241
- console.log(` pnpm dlx @webmux/agent service uninstall`);
1242
- } catch (err) {
1243
- const message = err instanceof Error ? err.message : String(err);
1244
- console.error(`[agent] Failed to install managed service: ${message}`);
1245
- console.error(`[agent] Service file path: ${servicePath()}`);
1246
- process.exit(1);
1247
- }
1248
- });
1249
- service.command("uninstall").description("Stop and remove the systemd user service").action(() => {
1250
- try {
1251
- uninstallService();
1252
- console.log(`[agent] Service file removed: ${servicePath()}`);
1253
- console.log(`[agent] Service uninstalled.`);
1254
- } catch (err) {
1255
- const message = err instanceof Error ? err.message : String(err);
1256
- console.error(`[agent] Failed to uninstall service: ${message}`);
1257
- process.exit(1);
1258
- }
1259
- });
1260
- service.command("status").description("Show systemd service status").action(() => {
1261
- try {
1262
- execFileSync2("systemctl", ["--user", "status", SERVICE_NAME], { stdio: "inherit" });
1263
- } catch {
1264
- console.log(`[agent] Service is not installed or not running.`);
1265
- }
1266
- });
1267
- service.command("upgrade").description("Switch the managed service to a specific agent version and restart it").requiredOption("--to <version>", "Target agent version (for example 0.1.5)").action((opts) => {
1268
- const creds = loadCredentials();
1269
- if (!creds) {
1270
- console.error(`[agent] Not registered. Run "npx @webmux/agent register" first.`);
1271
- process.exit(1);
1272
- }
1273
- console.log(`[agent] Switching managed service to ${opts.to}...`);
1274
- try {
1275
- upgradeService({
1276
- agentName: creds.name,
1277
- packageName: AGENT_PACKAGE_NAME,
1278
- version: opts.to
1279
- });
1280
- console.log("[agent] Managed service updated successfully.");
1281
- } catch (err) {
1282
- const message = err instanceof Error ? err.message : String(err);
1283
- console.error(`[agent] Failed to upgrade managed service: ${message}`);
1284
- process.exit(1);
235
+ return program;
236
+ }
237
+ async function run(argv = process.argv) {
238
+ await createProgram().parseAsync(argv);
239
+ }
240
+ function isDirectExecution() {
241
+ const entryPath = process.argv[1];
242
+ if (!entryPath) {
243
+ return false;
1285
244
  }
1286
- });
1287
- program.parse();
245
+ return import.meta.url === pathToFileURL(entryPath).href;
246
+ }
247
+ if (isDirectExecution()) {
248
+ void run();
249
+ }
250
+ export {
251
+ createProgram,
252
+ run
253
+ };