@webmux/agent 0.1.4 → 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 +668 -117
- 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 as execSync2 } 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,19 +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 { execSync } from "child_process";
|
|
202
|
-
import WebSocket from "ws";
|
|
203
|
-
|
|
204
|
-
// src/terminal.ts
|
|
205
|
-
import { spawn } from "node-pty";
|
|
206
|
-
|
|
207
|
-
// ../shared/src/contracts.ts
|
|
208
|
-
var DEFAULT_TERMINAL_SIZE = {
|
|
209
|
-
cols: 120,
|
|
210
|
-
rows: 36
|
|
211
|
-
};
|
|
212
|
-
|
|
213
450
|
// src/terminal.ts
|
|
214
451
|
async function createTerminalBridge(options) {
|
|
215
452
|
const {
|
|
@@ -253,29 +490,265 @@ async function createTerminalBridge(options) {
|
|
|
253
490
|
};
|
|
254
491
|
}
|
|
255
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
|
+
|
|
256
712
|
// src/connection.ts
|
|
257
|
-
var AGENT_VERSION = "0.1.4";
|
|
258
713
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
259
714
|
var SESSION_SYNC_INTERVAL_MS = 15e3;
|
|
260
715
|
var INITIAL_RECONNECT_DELAY_MS = 1e3;
|
|
261
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
|
+
};
|
|
262
732
|
var AgentConnection = class {
|
|
263
733
|
serverUrl;
|
|
264
734
|
agentId;
|
|
265
735
|
agentSecret;
|
|
266
736
|
tmux;
|
|
737
|
+
runtime;
|
|
267
738
|
ws = null;
|
|
268
739
|
heartbeatTimer = null;
|
|
269
740
|
sessionSyncTimer = null;
|
|
270
741
|
reconnectTimer = null;
|
|
271
742
|
reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
272
743
|
bridges = /* @__PURE__ */ new Map();
|
|
744
|
+
runs = /* @__PURE__ */ new Map();
|
|
273
745
|
stopped = false;
|
|
274
|
-
constructor(serverUrl, agentId, agentSecret, tmux) {
|
|
746
|
+
constructor(serverUrl, agentId, agentSecret, tmux, runtime = defaultAgentRuntime) {
|
|
275
747
|
this.serverUrl = serverUrl;
|
|
276
748
|
this.agentId = agentId;
|
|
277
749
|
this.agentSecret = agentSecret;
|
|
278
750
|
this.tmux = tmux;
|
|
751
|
+
this.runtime = runtime;
|
|
279
752
|
}
|
|
280
753
|
start() {
|
|
281
754
|
this.stopped = false;
|
|
@@ -290,6 +763,7 @@ var AgentConnection = class {
|
|
|
290
763
|
this.stopHeartbeat();
|
|
291
764
|
this.stopSessionSync();
|
|
292
765
|
this.disposeAllBridges();
|
|
766
|
+
this.disposeAllRuns();
|
|
293
767
|
if (this.ws) {
|
|
294
768
|
this.ws.close(1e3, "agent shutting down");
|
|
295
769
|
this.ws = null;
|
|
@@ -303,7 +777,12 @@ var AgentConnection = class {
|
|
|
303
777
|
ws.on("open", () => {
|
|
304
778
|
console.log("[agent] WebSocket connected, authenticating...");
|
|
305
779
|
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
306
|
-
this.sendMessage({
|
|
780
|
+
this.sendMessage({
|
|
781
|
+
type: "auth",
|
|
782
|
+
agentId: this.agentId,
|
|
783
|
+
agentSecret: this.agentSecret,
|
|
784
|
+
version: this.runtime.version
|
|
785
|
+
});
|
|
307
786
|
});
|
|
308
787
|
ws.on("message", (raw) => {
|
|
309
788
|
let msg;
|
|
@@ -327,9 +806,7 @@ var AgentConnection = class {
|
|
|
327
806
|
switch (msg.type) {
|
|
328
807
|
case "auth-ok":
|
|
329
808
|
console.log("[agent] Authenticated successfully");
|
|
330
|
-
if (
|
|
331
|
-
console.log(`[agent] Update available: ${AGENT_VERSION} \u2192 ${msg.latestVersion}`);
|
|
332
|
-
this.selfUpdate(msg.latestVersion);
|
|
809
|
+
if (this.applyRecommendedUpgrade(msg.upgradePolicy)) {
|
|
333
810
|
return;
|
|
334
811
|
}
|
|
335
812
|
this.startHeartbeat();
|
|
@@ -343,7 +820,7 @@ var AgentConnection = class {
|
|
|
343
820
|
this.ws.close();
|
|
344
821
|
this.ws = null;
|
|
345
822
|
}
|
|
346
|
-
|
|
823
|
+
this.runtime.exit(1);
|
|
347
824
|
break;
|
|
348
825
|
case "sessions-list":
|
|
349
826
|
this.syncSessions();
|
|
@@ -366,6 +843,21 @@ var AgentConnection = class {
|
|
|
366
843
|
case "session-kill":
|
|
367
844
|
this.handleSessionKill(msg.requestId, msg.name);
|
|
368
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;
|
|
369
861
|
default:
|
|
370
862
|
console.warn("[agent] Unknown message type:", msg.type);
|
|
371
863
|
}
|
|
@@ -457,26 +949,47 @@ var AgentConnection = class {
|
|
|
457
949
|
this.sendMessage({ type: "command-result", requestId, ok: false, error: message });
|
|
458
950
|
}
|
|
459
951
|
}
|
|
460
|
-
|
|
461
|
-
|
|
952
|
+
sendMessage(msg) {
|
|
953
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
954
|
+
this.ws.send(JSON.stringify(msg));
|
|
955
|
+
}
|
|
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
|
+
}
|
|
462
978
|
try {
|
|
463
|
-
|
|
464
|
-
|
|
979
|
+
this.runtime.applyServiceUpgrade({
|
|
980
|
+
packageName: upgradePolicy.packageName || AGENT_PACKAGE_NAME,
|
|
981
|
+
targetVersion
|
|
982
|
+
});
|
|
983
|
+
console.log(`[agent] Managed service switched to ${targetVersion}. Restarting...`);
|
|
465
984
|
} catch (err) {
|
|
466
|
-
|
|
985
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
986
|
+
console.error(`[agent] Failed to apply managed upgrade: ${message}`);
|
|
467
987
|
console.log("[agent] Continuing with current version");
|
|
468
|
-
|
|
469
|
-
this.startSessionSync();
|
|
470
|
-
this.syncSessions();
|
|
471
|
-
return;
|
|
988
|
+
return false;
|
|
472
989
|
}
|
|
473
990
|
this.stop();
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
sendMessage(msg) {
|
|
477
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
478
|
-
this.ws.send(JSON.stringify(msg));
|
|
479
|
-
}
|
|
991
|
+
this.runtime.exit(0);
|
|
992
|
+
return true;
|
|
480
993
|
}
|
|
481
994
|
startHeartbeat() {
|
|
482
995
|
this.stopHeartbeat();
|
|
@@ -508,6 +1021,76 @@ var AgentConnection = class {
|
|
|
508
1021
|
this.bridges.delete(browserId);
|
|
509
1022
|
}
|
|
510
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
|
+
}
|
|
511
1094
|
onDisconnect() {
|
|
512
1095
|
this.stopHeartbeat();
|
|
513
1096
|
this.stopSessionSync();
|
|
@@ -531,12 +1114,11 @@ function buildWsUrl(serverUrl) {
|
|
|
531
1114
|
}
|
|
532
1115
|
|
|
533
1116
|
// src/cli.ts
|
|
534
|
-
var SERVICE_NAME = "webmux-agent";
|
|
535
1117
|
var program = new Command();
|
|
536
|
-
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);
|
|
537
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) => {
|
|
538
1120
|
const serverUrl = opts.server.replace(/\/+$/, "");
|
|
539
|
-
const agentName = opts.name ??
|
|
1121
|
+
const agentName = opts.name ?? os3.hostname();
|
|
540
1122
|
console.log(`[agent] Registering with server ${serverUrl}...`);
|
|
541
1123
|
console.log(`[agent] Agent name: ${agentName}`);
|
|
542
1124
|
const body = {
|
|
@@ -578,8 +1160,8 @@ program.command("register").description("Register this agent with a webmux serve
|
|
|
578
1160
|
console.log(`[agent] Credentials saved to ${credentialsPath()}`);
|
|
579
1161
|
console.log(``);
|
|
580
1162
|
console.log(`Next steps:`);
|
|
581
|
-
console.log(`
|
|
582
|
-
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`);
|
|
583
1165
|
});
|
|
584
1166
|
program.command("start").description("Start the agent and connect to the server").action(() => {
|
|
585
1167
|
const creds = loadCredentials();
|
|
@@ -594,7 +1176,7 @@ program.command("start").description("Start the agent and connect to the server"
|
|
|
594
1176
|
console.log(`[agent] Agent ID: ${creds.agentId}`);
|
|
595
1177
|
const tmux = new TmuxClient({
|
|
596
1178
|
socketName: "webmux",
|
|
597
|
-
workspaceRoot:
|
|
1179
|
+
workspaceRoot: os3.homedir()
|
|
598
1180
|
});
|
|
599
1181
|
const connection = new AgentConnection(
|
|
600
1182
|
creds.serverUrl,
|
|
@@ -618,119 +1200,88 @@ program.command("status").description("Show agent status and credentials info").
|
|
|
618
1200
|
process.exit(0);
|
|
619
1201
|
}
|
|
620
1202
|
console.log(`Agent Name: ${creds.name}`);
|
|
1203
|
+
console.log(`Agent Version: ${AGENT_VERSION}`);
|
|
621
1204
|
console.log(`Server URL: ${creds.serverUrl}`);
|
|
622
1205
|
console.log(`Agent ID: ${creds.agentId}`);
|
|
623
1206
|
console.log(`Credentials File: ${credentialsPath()}`);
|
|
1207
|
+
const installedService = readInstalledServiceConfig();
|
|
624
1208
|
try {
|
|
625
|
-
const result =
|
|
1209
|
+
const result = execFileSync2("systemctl", ["--user", "is-active", SERVICE_NAME], { encoding: "utf-8" }).trim();
|
|
626
1210
|
console.log(`Service: ${result}`);
|
|
627
1211
|
} catch {
|
|
628
1212
|
console.log(`Service: not installed`);
|
|
629
1213
|
}
|
|
1214
|
+
if (installedService?.version) {
|
|
1215
|
+
console.log(`Service Version: ${installedService.version}`);
|
|
1216
|
+
}
|
|
630
1217
|
});
|
|
631
1218
|
var service = program.command("service").description("Manage the systemd service");
|
|
632
|
-
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) => {
|
|
633
1220
|
const creds = loadCredentials();
|
|
634
1221
|
if (!creds) {
|
|
635
1222
|
console.error(`[agent] Not registered. Run "npx @webmux/agent register" first.`);
|
|
636
1223
|
process.exit(1);
|
|
637
1224
|
}
|
|
638
|
-
const npxPath = findBinary("npx");
|
|
639
|
-
if (!npxPath) {
|
|
640
|
-
console.error(`[agent] Cannot find npx. Make sure Node.js is installed.`);
|
|
641
|
-
process.exit(1);
|
|
642
|
-
}
|
|
643
|
-
const serviceDir = path2.join(os2.homedir(), ".config", "systemd", "user");
|
|
644
|
-
const servicePath = path2.join(serviceDir, `${SERVICE_NAME}.service`);
|
|
645
|
-
const npmPath = findBinary("npm") ?? "npm";
|
|
646
|
-
const unit = `[Unit]
|
|
647
|
-
Description=Webmux Agent (${creds.name})
|
|
648
|
-
After=network-online.target
|
|
649
|
-
Wants=network-online.target
|
|
650
|
-
|
|
651
|
-
[Service]
|
|
652
|
-
Type=simple
|
|
653
|
-
ExecStartPre=${npmPath} install -g @webmux/agent@latest
|
|
654
|
-
ExecStart=${findBinary("webmux-agent") ?? `${npxPath} -y @webmux/agent`} start
|
|
655
|
-
Restart=always
|
|
656
|
-
RestartSec=10
|
|
657
|
-
Environment=HOME=${os2.homedir()}
|
|
658
|
-
Environment=PATH=${process.env.PATH}
|
|
659
|
-
WorkingDirectory=${os2.homedir()}
|
|
660
|
-
|
|
661
|
-
[Install]
|
|
662
|
-
WantedBy=default.target
|
|
663
|
-
`;
|
|
664
|
-
fs2.mkdirSync(serviceDir, { recursive: true });
|
|
665
|
-
fs2.writeFileSync(servicePath, unit);
|
|
666
|
-
console.log(`[agent] Service file created: ${servicePath}`);
|
|
667
1225
|
try {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1226
|
+
installService({
|
|
1227
|
+
agentName: creds.name,
|
|
1228
|
+
packageName: AGENT_PACKAGE_NAME,
|
|
1229
|
+
version: AGENT_VERSION,
|
|
1230
|
+
autoUpgrade: opts.autoUpgrade
|
|
1231
|
+
});
|
|
672
1232
|
console.log(``);
|
|
673
1233
|
console.log(`[agent] Service installed and started!`);
|
|
1234
|
+
console.log(`[agent] Managed version: ${AGENT_VERSION}`);
|
|
674
1235
|
console.log(`[agent] It will auto-start on boot.`);
|
|
675
1236
|
console.log(``);
|
|
676
1237
|
console.log(`Useful commands:`);
|
|
677
1238
|
console.log(` systemctl --user status ${SERVICE_NAME}`);
|
|
678
1239
|
console.log(` journalctl --user -u ${SERVICE_NAME} -f`);
|
|
679
|
-
console.log(`
|
|
1240
|
+
console.log(` pnpm dlx @webmux/agent service upgrade --to <version>`);
|
|
1241
|
+
console.log(` pnpm dlx @webmux/agent service uninstall`);
|
|
680
1242
|
} catch (err) {
|
|
681
1243
|
const message = err instanceof Error ? err.message : String(err);
|
|
682
|
-
console.error(`[agent] Failed to
|
|
683
|
-
console.error(`[agent] Service file
|
|
684
|
-
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()}`);
|
|
685
1246
|
process.exit(1);
|
|
686
1247
|
}
|
|
687
1248
|
});
|
|
688
1249
|
service.command("uninstall").description("Stop and remove the systemd user service").action(() => {
|
|
689
|
-
const servicePath = path2.join(os2.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
|
|
690
1250
|
try {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
try {
|
|
700
|
-
execSync2("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
701
|
-
} 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);
|
|
702
1258
|
}
|
|
703
|
-
console.log(`[agent] Service uninstalled.`);
|
|
704
1259
|
});
|
|
705
1260
|
service.command("status").description("Show systemd service status").action(() => {
|
|
706
1261
|
try {
|
|
707
|
-
|
|
1262
|
+
execFileSync2("systemctl", ["--user", "status", SERVICE_NAME], { stdio: "inherit" });
|
|
708
1263
|
} catch {
|
|
709
1264
|
console.log(`[agent] Service is not installed or not running.`);
|
|
710
1265
|
}
|
|
711
1266
|
});
|
|
712
|
-
service.command("upgrade").description("
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
} catch {
|
|
717
|
-
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.`);
|
|
718
1271
|
process.exit(1);
|
|
719
1272
|
}
|
|
720
|
-
console.log(
|
|
1273
|
+
console.log(`[agent] Switching managed service to ${opts.to}...`);
|
|
721
1274
|
try {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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);
|
|
727
1285
|
}
|
|
728
1286
|
});
|
|
729
|
-
function findBinary(name) {
|
|
730
|
-
try {
|
|
731
|
-
return execSync2(`which ${name} 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
732
|
-
} catch {
|
|
733
|
-
return null;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
1287
|
program.parse();
|