@webmux/agent 0.1.3 → 0.2.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/dist/cli.js +671 -97
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import path2 from "path";
|
|
7
|
-
import { execSync } from "child_process";
|
|
4
|
+
import os3 from "os";
|
|
5
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
8
6
|
import { Command } from "commander";
|
|
9
7
|
|
|
10
8
|
// src/credentials.ts
|
|
@@ -43,6 +41,258 @@ function saveCredentials(creds) {
|
|
|
43
41
|
});
|
|
44
42
|
}
|
|
45
43
|
|
|
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
|
+
|
|
46
296
|
// src/tmux.ts
|
|
47
297
|
import { execFile } from "child_process";
|
|
48
298
|
import { promisify } from "util";
|
|
@@ -197,18 +447,6 @@ function isTmuxEmptyStateMessage(message) {
|
|
|
197
447
|
return TMUX_EMPTY_STATE_MARKERS.some((marker) => message.includes(marker));
|
|
198
448
|
}
|
|
199
449
|
|
|
200
|
-
// src/connection.ts
|
|
201
|
-
import WebSocket from "ws";
|
|
202
|
-
|
|
203
|
-
// src/terminal.ts
|
|
204
|
-
import { spawn } from "node-pty";
|
|
205
|
-
|
|
206
|
-
// ../shared/src/contracts.ts
|
|
207
|
-
var DEFAULT_TERMINAL_SIZE = {
|
|
208
|
-
cols: 120,
|
|
209
|
-
rows: 36
|
|
210
|
-
};
|
|
211
|
-
|
|
212
450
|
// src/terminal.ts
|
|
213
451
|
async function createTerminalBridge(options) {
|
|
214
452
|
const {
|
|
@@ -252,28 +490,265 @@ async function createTerminalBridge(options) {
|
|
|
252
490
|
};
|
|
253
491
|
}
|
|
254
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
|
+
|
|
255
712
|
// src/connection.ts
|
|
256
713
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
257
714
|
var SESSION_SYNC_INTERVAL_MS = 15e3;
|
|
258
715
|
var INITIAL_RECONNECT_DELAY_MS = 1e3;
|
|
259
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
|
+
};
|
|
260
732
|
var AgentConnection = class {
|
|
261
733
|
serverUrl;
|
|
262
734
|
agentId;
|
|
263
735
|
agentSecret;
|
|
264
736
|
tmux;
|
|
737
|
+
runtime;
|
|
265
738
|
ws = null;
|
|
266
739
|
heartbeatTimer = null;
|
|
267
740
|
sessionSyncTimer = null;
|
|
268
741
|
reconnectTimer = null;
|
|
269
742
|
reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
270
743
|
bridges = /* @__PURE__ */ new Map();
|
|
744
|
+
runs = /* @__PURE__ */ new Map();
|
|
271
745
|
stopped = false;
|
|
272
|
-
constructor(serverUrl, agentId, agentSecret, tmux) {
|
|
746
|
+
constructor(serverUrl, agentId, agentSecret, tmux, runtime = defaultAgentRuntime) {
|
|
273
747
|
this.serverUrl = serverUrl;
|
|
274
748
|
this.agentId = agentId;
|
|
275
749
|
this.agentSecret = agentSecret;
|
|
276
750
|
this.tmux = tmux;
|
|
751
|
+
this.runtime = runtime;
|
|
277
752
|
}
|
|
278
753
|
start() {
|
|
279
754
|
this.stopped = false;
|
|
@@ -288,6 +763,7 @@ var AgentConnection = class {
|
|
|
288
763
|
this.stopHeartbeat();
|
|
289
764
|
this.stopSessionSync();
|
|
290
765
|
this.disposeAllBridges();
|
|
766
|
+
this.disposeAllRuns();
|
|
291
767
|
if (this.ws) {
|
|
292
768
|
this.ws.close(1e3, "agent shutting down");
|
|
293
769
|
this.ws = null;
|
|
@@ -301,7 +777,12 @@ var AgentConnection = class {
|
|
|
301
777
|
ws.on("open", () => {
|
|
302
778
|
console.log("[agent] WebSocket connected, authenticating...");
|
|
303
779
|
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
304
|
-
this.sendMessage({
|
|
780
|
+
this.sendMessage({
|
|
781
|
+
type: "auth",
|
|
782
|
+
agentId: this.agentId,
|
|
783
|
+
agentSecret: this.agentSecret,
|
|
784
|
+
version: this.runtime.version
|
|
785
|
+
});
|
|
305
786
|
});
|
|
306
787
|
ws.on("message", (raw) => {
|
|
307
788
|
let msg;
|
|
@@ -325,6 +806,9 @@ var AgentConnection = class {
|
|
|
325
806
|
switch (msg.type) {
|
|
326
807
|
case "auth-ok":
|
|
327
808
|
console.log("[agent] Authenticated successfully");
|
|
809
|
+
if (this.applyRecommendedUpgrade(msg.upgradePolicy)) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
328
812
|
this.startHeartbeat();
|
|
329
813
|
this.startSessionSync();
|
|
330
814
|
this.syncSessions();
|
|
@@ -336,7 +820,7 @@ var AgentConnection = class {
|
|
|
336
820
|
this.ws.close();
|
|
337
821
|
this.ws = null;
|
|
338
822
|
}
|
|
339
|
-
|
|
823
|
+
this.runtime.exit(1);
|
|
340
824
|
break;
|
|
341
825
|
case "sessions-list":
|
|
342
826
|
this.syncSessions();
|
|
@@ -359,6 +843,21 @@ var AgentConnection = class {
|
|
|
359
843
|
case "session-kill":
|
|
360
844
|
this.handleSessionKill(msg.requestId, msg.name);
|
|
361
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;
|
|
362
861
|
default:
|
|
363
862
|
console.warn("[agent] Unknown message type:", msg.type);
|
|
364
863
|
}
|
|
@@ -455,6 +954,43 @@ var AgentConnection = class {
|
|
|
455
954
|
this.ws.send(JSON.stringify(msg));
|
|
456
955
|
}
|
|
457
956
|
}
|
|
957
|
+
applyRecommendedUpgrade(upgradePolicy) {
|
|
958
|
+
const targetVersion = upgradePolicy?.targetVersion;
|
|
959
|
+
if (!targetVersion) {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
let comparison;
|
|
963
|
+
try {
|
|
964
|
+
comparison = compareSemanticVersions(this.runtime.version, targetVersion);
|
|
965
|
+
} catch {
|
|
966
|
+
console.warn("[agent] Skipping automatic upgrade because version parsing failed");
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
if (comparison >= 0) {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
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;
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
this.runtime.applyServiceUpgrade({
|
|
980
|
+
packageName: upgradePolicy.packageName || AGENT_PACKAGE_NAME,
|
|
981
|
+
targetVersion
|
|
982
|
+
});
|
|
983
|
+
console.log(`[agent] Managed service switched to ${targetVersion}. Restarting...`);
|
|
984
|
+
} catch (err) {
|
|
985
|
+
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
|
+
}
|
|
458
994
|
startHeartbeat() {
|
|
459
995
|
this.stopHeartbeat();
|
|
460
996
|
this.heartbeatTimer = setInterval(() => {
|
|
@@ -485,6 +1021,76 @@ var AgentConnection = class {
|
|
|
485
1021
|
this.bridges.delete(browserId);
|
|
486
1022
|
}
|
|
487
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}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
488
1094
|
onDisconnect() {
|
|
489
1095
|
this.stopHeartbeat();
|
|
490
1096
|
this.stopSessionSync();
|
|
@@ -508,12 +1114,11 @@ function buildWsUrl(serverUrl) {
|
|
|
508
1114
|
}
|
|
509
1115
|
|
|
510
1116
|
// src/cli.ts
|
|
511
|
-
var SERVICE_NAME = "webmux-agent";
|
|
512
1117
|
var program = new Command();
|
|
513
|
-
program.name("webmux-agent").description("Webmux agent \u2014 connects your machine to the webmux server").version(
|
|
1118
|
+
program.name("webmux-agent").description("Webmux agent \u2014 connects your machine to the webmux server").version(AGENT_VERSION);
|
|
514
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) => {
|
|
515
1120
|
const serverUrl = opts.server.replace(/\/+$/, "");
|
|
516
|
-
const agentName = opts.name ??
|
|
1121
|
+
const agentName = opts.name ?? os3.hostname();
|
|
517
1122
|
console.log(`[agent] Registering with server ${serverUrl}...`);
|
|
518
1123
|
console.log(`[agent] Agent name: ${agentName}`);
|
|
519
1124
|
const body = {
|
|
@@ -555,8 +1160,8 @@ program.command("register").description("Register this agent with a webmux serve
|
|
|
555
1160
|
console.log(`[agent] Credentials saved to ${credentialsPath()}`);
|
|
556
1161
|
console.log(``);
|
|
557
1162
|
console.log(`Next steps:`);
|
|
558
|
-
console.log(`
|
|
559
|
-
console.log(`
|
|
1163
|
+
console.log(` pnpm dlx @webmux/agent start # run once`);
|
|
1164
|
+
console.log(` pnpm dlx @webmux/agent service install # install as managed systemd service`);
|
|
560
1165
|
});
|
|
561
1166
|
program.command("start").description("Start the agent and connect to the server").action(() => {
|
|
562
1167
|
const creds = loadCredentials();
|
|
@@ -571,7 +1176,7 @@ program.command("start").description("Start the agent and connect to the server"
|
|
|
571
1176
|
console.log(`[agent] Agent ID: ${creds.agentId}`);
|
|
572
1177
|
const tmux = new TmuxClient({
|
|
573
1178
|
socketName: "webmux",
|
|
574
|
-
workspaceRoot:
|
|
1179
|
+
workspaceRoot: os3.homedir()
|
|
575
1180
|
});
|
|
576
1181
|
const connection = new AgentConnection(
|
|
577
1182
|
creds.serverUrl,
|
|
@@ -595,119 +1200,88 @@ program.command("status").description("Show agent status and credentials info").
|
|
|
595
1200
|
process.exit(0);
|
|
596
1201
|
}
|
|
597
1202
|
console.log(`Agent Name: ${creds.name}`);
|
|
1203
|
+
console.log(`Agent Version: ${AGENT_VERSION}`);
|
|
598
1204
|
console.log(`Server URL: ${creds.serverUrl}`);
|
|
599
1205
|
console.log(`Agent ID: ${creds.agentId}`);
|
|
600
1206
|
console.log(`Credentials File: ${credentialsPath()}`);
|
|
1207
|
+
const installedService = readInstalledServiceConfig();
|
|
601
1208
|
try {
|
|
602
|
-
const result =
|
|
1209
|
+
const result = execFileSync2("systemctl", ["--user", "is-active", SERVICE_NAME], { encoding: "utf-8" }).trim();
|
|
603
1210
|
console.log(`Service: ${result}`);
|
|
604
1211
|
} catch {
|
|
605
1212
|
console.log(`Service: not installed`);
|
|
606
1213
|
}
|
|
1214
|
+
if (installedService?.version) {
|
|
1215
|
+
console.log(`Service Version: ${installedService.version}`);
|
|
1216
|
+
}
|
|
607
1217
|
});
|
|
608
1218
|
var service = program.command("service").description("Manage the systemd service");
|
|
609
|
-
service.command("install").description("Install and start the agent as a systemd user service").action(() => {
|
|
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) => {
|
|
610
1220
|
const creds = loadCredentials();
|
|
611
1221
|
if (!creds) {
|
|
612
1222
|
console.error(`[agent] Not registered. Run "npx @webmux/agent register" first.`);
|
|
613
1223
|
process.exit(1);
|
|
614
1224
|
}
|
|
615
|
-
const npxPath = findBinary("npx");
|
|
616
|
-
if (!npxPath) {
|
|
617
|
-
console.error(`[agent] Cannot find npx. Make sure Node.js is installed.`);
|
|
618
|
-
process.exit(1);
|
|
619
|
-
}
|
|
620
|
-
const serviceDir = path2.join(os2.homedir(), ".config", "systemd", "user");
|
|
621
|
-
const servicePath = path2.join(serviceDir, `${SERVICE_NAME}.service`);
|
|
622
|
-
const npmPath = findBinary("npm") ?? "npm";
|
|
623
|
-
const unit = `[Unit]
|
|
624
|
-
Description=Webmux Agent (${creds.name})
|
|
625
|
-
After=network-online.target
|
|
626
|
-
Wants=network-online.target
|
|
627
|
-
|
|
628
|
-
[Service]
|
|
629
|
-
Type=simple
|
|
630
|
-
ExecStartPre=${npmPath} install -g @webmux/agent@latest
|
|
631
|
-
ExecStart=${findBinary("webmux-agent") ?? `${npxPath} -y @webmux/agent`} start
|
|
632
|
-
Restart=always
|
|
633
|
-
RestartSec=10
|
|
634
|
-
Environment=HOME=${os2.homedir()}
|
|
635
|
-
Environment=PATH=${process.env.PATH}
|
|
636
|
-
WorkingDirectory=${os2.homedir()}
|
|
637
|
-
|
|
638
|
-
[Install]
|
|
639
|
-
WantedBy=default.target
|
|
640
|
-
`;
|
|
641
|
-
fs2.mkdirSync(serviceDir, { recursive: true });
|
|
642
|
-
fs2.writeFileSync(servicePath, unit);
|
|
643
|
-
console.log(`[agent] Service file created: ${servicePath}`);
|
|
644
1225
|
try {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1226
|
+
installService({
|
|
1227
|
+
agentName: creds.name,
|
|
1228
|
+
packageName: AGENT_PACKAGE_NAME,
|
|
1229
|
+
version: AGENT_VERSION,
|
|
1230
|
+
autoUpgrade: opts.autoUpgrade
|
|
1231
|
+
});
|
|
649
1232
|
console.log(``);
|
|
650
1233
|
console.log(`[agent] Service installed and started!`);
|
|
1234
|
+
console.log(`[agent] Managed version: ${AGENT_VERSION}`);
|
|
651
1235
|
console.log(`[agent] It will auto-start on boot.`);
|
|
652
1236
|
console.log(``);
|
|
653
1237
|
console.log(`Useful commands:`);
|
|
654
1238
|
console.log(` systemctl --user status ${SERVICE_NAME}`);
|
|
655
1239
|
console.log(` journalctl --user -u ${SERVICE_NAME} -f`);
|
|
656
|
-
console.log(`
|
|
1240
|
+
console.log(` pnpm dlx @webmux/agent service upgrade --to <version>`);
|
|
1241
|
+
console.log(` pnpm dlx @webmux/agent service uninstall`);
|
|
657
1242
|
} catch (err) {
|
|
658
1243
|
const message = err instanceof Error ? err.message : String(err);
|
|
659
|
-
console.error(`[agent] Failed to
|
|
660
|
-
console.error(`[agent] Service file
|
|
661
|
-
console.error(`[agent] You can try manually: systemctl --user enable --now ${SERVICE_NAME}`);
|
|
1244
|
+
console.error(`[agent] Failed to install managed service: ${message}`);
|
|
1245
|
+
console.error(`[agent] Service file path: ${servicePath()}`);
|
|
662
1246
|
process.exit(1);
|
|
663
1247
|
}
|
|
664
1248
|
});
|
|
665
1249
|
service.command("uninstall").description("Stop and remove the systemd user service").action(() => {
|
|
666
|
-
const servicePath = path2.join(os2.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
|
|
667
1250
|
try {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
try {
|
|
677
|
-
execSync("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
678
|
-
} catch {
|
|
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);
|
|
679
1258
|
}
|
|
680
|
-
console.log(`[agent] Service uninstalled.`);
|
|
681
1259
|
});
|
|
682
1260
|
service.command("status").description("Show systemd service status").action(() => {
|
|
683
1261
|
try {
|
|
684
|
-
|
|
1262
|
+
execFileSync2("systemctl", ["--user", "status", SERVICE_NAME], { stdio: "inherit" });
|
|
685
1263
|
} catch {
|
|
686
1264
|
console.log(`[agent] Service is not installed or not running.`);
|
|
687
1265
|
}
|
|
688
1266
|
});
|
|
689
|
-
service.command("upgrade").description("
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
} catch {
|
|
694
|
-
console.error("[agent] Failed to upgrade. Try manually: npm install -g @webmux/agent@latest");
|
|
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.`);
|
|
695
1271
|
process.exit(1);
|
|
696
1272
|
}
|
|
697
|
-
console.log(
|
|
1273
|
+
console.log(`[agent] Switching managed service to ${opts.to}...`);
|
|
698
1274
|
try {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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);
|
|
704
1285
|
}
|
|
705
1286
|
});
|
|
706
|
-
function findBinary(name) {
|
|
707
|
-
try {
|
|
708
|
-
return execSync(`which ${name} 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
709
|
-
} catch {
|
|
710
|
-
return null;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
1287
|
program.parse();
|