@webmux/agent 0.1.4 → 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/chunk-EWE7ZUYJ.js +163 -0
- package/dist/chunk-INUNCXBM.js +202 -0
- package/dist/cli.js +184 -667
- package/dist/connection-RJY775NL.js +1046 -0
- package/dist/tmux-QIB4H3UA.js +15 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +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 fs2 from "fs";
|
|
5
14
|
import os2 from "os";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
15
|
+
import { execFileSync } from "child_process";
|
|
16
|
+
import { pathToFileURL } from "url";
|
|
8
17
|
import { Command } from "commander";
|
|
9
18
|
|
|
10
19
|
// src/credentials.ts
|
|
@@ -43,694 +52,202 @@ function saveCredentials(creds) {
|
|
|
43
52
|
});
|
|
44
53
|
}
|
|
45
54
|
|
|
46
|
-
// src/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"error connecting to",
|
|
54
|
-
"failed to connect to server",
|
|
55
|
-
"no server running",
|
|
56
|
-
"no sessions"
|
|
57
|
-
];
|
|
58
|
-
var TmuxClient = class {
|
|
59
|
-
socketName;
|
|
60
|
-
workspaceRoot;
|
|
61
|
-
constructor(options) {
|
|
62
|
-
this.socketName = options.socketName;
|
|
63
|
-
this.workspaceRoot = options.workspaceRoot;
|
|
64
|
-
}
|
|
65
|
-
async listSessions() {
|
|
66
|
-
const stdout = await this.run(
|
|
67
|
-
[
|
|
68
|
-
"list-sessions",
|
|
69
|
-
"-F",
|
|
70
|
-
[
|
|
71
|
-
"#{session_name}",
|
|
72
|
-
"#{session_windows}",
|
|
73
|
-
"#{session_attached}",
|
|
74
|
-
"#{session_created}",
|
|
75
|
-
"#{session_activity}",
|
|
76
|
-
"#{session_path}",
|
|
77
|
-
"#{pane_current_command}"
|
|
78
|
-
].join(FIELD_SEPARATOR)
|
|
79
|
-
],
|
|
80
|
-
{ allowEmptyState: true }
|
|
81
|
-
);
|
|
82
|
-
const sessions = parseSessionList(stdout);
|
|
83
|
-
const enriched = await Promise.all(
|
|
84
|
-
sessions.map(async (session) => ({
|
|
85
|
-
...session,
|
|
86
|
-
preview: await this.getPreview(session.name)
|
|
87
|
-
}))
|
|
88
|
-
);
|
|
89
|
-
return enriched.sort((left, right) => {
|
|
90
|
-
if (left.lastActivityAt !== right.lastActivityAt) {
|
|
91
|
-
return right.lastActivityAt - left.lastActivityAt;
|
|
92
|
-
}
|
|
93
|
-
return left.name.localeCompare(right.name);
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
async createSession(name) {
|
|
97
|
-
assertValidSessionName(name);
|
|
98
|
-
if (await this.hasSession(name)) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
await this.run(["new-session", "-d", "-s", name, "-c", this.workspaceRoot]);
|
|
102
|
-
}
|
|
103
|
-
async killSession(name) {
|
|
104
|
-
assertValidSessionName(name);
|
|
105
|
-
await this.run(["kill-session", "-t", name]);
|
|
106
|
-
}
|
|
107
|
-
async readSession(name) {
|
|
108
|
-
const sessions = await this.listSessions();
|
|
109
|
-
return sessions.find((session) => session.name === name) ?? null;
|
|
110
|
-
}
|
|
111
|
-
async hasSession(name) {
|
|
112
|
-
try {
|
|
113
|
-
await this.run(["has-session", "-t", name]);
|
|
114
|
-
return true;
|
|
115
|
-
} catch (error) {
|
|
116
|
-
const message = String(
|
|
117
|
-
error.stderr ?? error.message
|
|
118
|
-
);
|
|
119
|
-
if (isTmuxEmptyStateMessage(message)) {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
async getPreview(name) {
|
|
126
|
-
try {
|
|
127
|
-
const stdout = await this.run(
|
|
128
|
-
["capture-pane", "-p", "-J", "-S", "-18", "-E", "-", "-t", `${name}:`],
|
|
129
|
-
{ allowEmptyState: true }
|
|
130
|
-
);
|
|
131
|
-
return formatPreview(stdout);
|
|
132
|
-
} catch {
|
|
133
|
-
return ["Session available. Tap to attach."];
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
async run(args, options = {}) {
|
|
137
|
-
try {
|
|
138
|
-
const { stdout } = await execFileAsync(
|
|
139
|
-
"tmux",
|
|
140
|
-
["-L", this.socketName, ...args],
|
|
141
|
-
{
|
|
142
|
-
cwd: this.workspaceRoot,
|
|
143
|
-
env: {
|
|
144
|
-
...process.env,
|
|
145
|
-
TERM: "xterm-256color"
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
);
|
|
149
|
-
return stdout;
|
|
150
|
-
} catch (error) {
|
|
151
|
-
const message = String(
|
|
152
|
-
error.stderr ?? error.message
|
|
153
|
-
);
|
|
154
|
-
if (options.allowEmptyState && TMUX_EMPTY_STATE_MARKERS.some((marker) => message.includes(marker))) {
|
|
155
|
-
return "";
|
|
156
|
-
}
|
|
157
|
-
throw error;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
function assertValidSessionName(name) {
|
|
162
|
-
if (!SESSION_NAME_PATTERN.test(name)) {
|
|
163
|
-
throw new Error(
|
|
164
|
-
"Invalid session name. Use up to 32 letters, numbers, dot, dash, or underscore."
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
function parseSessionList(stdout) {
|
|
169
|
-
return stdout.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
170
|
-
const parts = line.split(FIELD_SEPARATOR);
|
|
171
|
-
const [name, windows, attachedClients, createdAt, lastActivityAt, path3] = parts;
|
|
172
|
-
const currentCommand = parts[6] ?? "";
|
|
173
|
-
if (!name || !windows || !attachedClients || !createdAt || !lastActivityAt || !path3) {
|
|
174
|
-
return [];
|
|
175
|
-
}
|
|
176
|
-
return [
|
|
177
|
-
{
|
|
178
|
-
name,
|
|
179
|
-
windows: Number(windows),
|
|
180
|
-
attachedClients: Number(attachedClients),
|
|
181
|
-
createdAt: Number(createdAt),
|
|
182
|
-
lastActivityAt: Number(lastActivityAt),
|
|
183
|
-
path: path3,
|
|
184
|
-
currentCommand
|
|
185
|
-
}
|
|
186
|
-
];
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
function formatPreview(stdout) {
|
|
190
|
-
const lines = stdout.replaceAll("\r", "").split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).slice(-3);
|
|
191
|
-
if (lines.length > 0) {
|
|
192
|
-
return lines;
|
|
193
|
-
}
|
|
194
|
-
return ["Fresh session. Nothing has run yet."];
|
|
195
|
-
}
|
|
196
|
-
function isTmuxEmptyStateMessage(message) {
|
|
197
|
-
return TMUX_EMPTY_STATE_MARKERS.some((marker) => message.includes(marker));
|
|
198
|
-
}
|
|
199
|
-
|
|
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
|
-
// src/terminal.ts
|
|
214
|
-
async function createTerminalBridge(options) {
|
|
215
|
-
const {
|
|
216
|
-
tmux,
|
|
217
|
-
sessionName,
|
|
218
|
-
cols = DEFAULT_TERMINAL_SIZE.cols,
|
|
219
|
-
rows = DEFAULT_TERMINAL_SIZE.rows,
|
|
220
|
-
onData,
|
|
221
|
-
onExit
|
|
222
|
-
} = options;
|
|
223
|
-
assertValidSessionName(sessionName);
|
|
224
|
-
await tmux.createSession(sessionName);
|
|
225
|
-
const ptyProcess = spawn(
|
|
226
|
-
"tmux",
|
|
227
|
-
["-L", tmux.socketName, "attach-session", "-t", sessionName],
|
|
228
|
-
{
|
|
229
|
-
cols,
|
|
230
|
-
rows,
|
|
231
|
-
cwd: tmux.workspaceRoot,
|
|
232
|
-
env: {
|
|
233
|
-
...process.env,
|
|
234
|
-
TERM: "xterm-256color"
|
|
235
|
-
},
|
|
236
|
-
name: "xterm-256color"
|
|
237
|
-
}
|
|
238
|
-
);
|
|
239
|
-
ptyProcess.onData(onData);
|
|
240
|
-
ptyProcess.onExit(({ exitCode }) => {
|
|
241
|
-
onExit(exitCode);
|
|
242
|
-
});
|
|
243
|
-
return {
|
|
244
|
-
write(data) {
|
|
245
|
-
ptyProcess.write(data);
|
|
246
|
-
},
|
|
247
|
-
resize(nextCols, nextRows) {
|
|
248
|
-
ptyProcess.resize(nextCols, nextRows);
|
|
249
|
-
},
|
|
250
|
-
dispose() {
|
|
251
|
-
ptyProcess.kill();
|
|
252
|
-
}
|
|
253
|
-
};
|
|
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 };
|
|
254
62
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
this.tmux = tmux;
|
|
279
|
-
}
|
|
280
|
-
start() {
|
|
281
|
-
this.stopped = false;
|
|
282
|
-
this.connect();
|
|
283
|
-
}
|
|
284
|
-
stop() {
|
|
285
|
-
this.stopped = true;
|
|
286
|
-
if (this.reconnectTimer) {
|
|
287
|
-
clearTimeout(this.reconnectTimer);
|
|
288
|
-
this.reconnectTimer = null;
|
|
289
|
-
}
|
|
290
|
-
this.stopHeartbeat();
|
|
291
|
-
this.stopSessionSync();
|
|
292
|
-
this.disposeAllBridges();
|
|
293
|
-
if (this.ws) {
|
|
294
|
-
this.ws.close(1e3, "agent shutting down");
|
|
295
|
-
this.ws = null;
|
|
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;
|
|
76
|
+
try {
|
|
77
|
+
response = await fetch(`${serverUrl}/api/agents/register`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/json" },
|
|
80
|
+
body: JSON.stringify(body)
|
|
81
|
+
});
|
|
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);
|
|
296
86
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const wsUrl = buildWsUrl(this.serverUrl);
|
|
300
|
-
console.log(`[agent] Connecting to ${wsUrl}`);
|
|
301
|
-
const ws = new WebSocket(wsUrl);
|
|
302
|
-
this.ws = ws;
|
|
303
|
-
ws.on("open", () => {
|
|
304
|
-
console.log("[agent] WebSocket connected, authenticating...");
|
|
305
|
-
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
306
|
-
this.sendMessage({ type: "auth", agentId: this.agentId, agentSecret: this.agentSecret, version: AGENT_VERSION });
|
|
307
|
-
});
|
|
308
|
-
ws.on("message", (raw) => {
|
|
309
|
-
let msg;
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
let errorMessage;
|
|
310
89
|
try {
|
|
311
|
-
|
|
90
|
+
const errorBody = await response.json();
|
|
91
|
+
errorMessage = errorBody.error ?? response.statusText;
|
|
312
92
|
} catch {
|
|
313
|
-
|
|
314
|
-
return;
|
|
93
|
+
errorMessage = response.statusText;
|
|
315
94
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
321
104
|
});
|
|
322
|
-
|
|
323
|
-
|
|
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()
|
|
324
128
|
});
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
this.handleTerminalAttach(msg.browserId, msg.sessionName, msg.cols, msg.rows);
|
|
353
|
-
break;
|
|
354
|
-
case "terminal-detach":
|
|
355
|
-
this.handleTerminalDetach(msg.browserId);
|
|
356
|
-
break;
|
|
357
|
-
case "terminal-input":
|
|
358
|
-
this.handleTerminalInput(msg.browserId, msg.data);
|
|
359
|
-
break;
|
|
360
|
-
case "terminal-resize":
|
|
361
|
-
this.handleTerminalResize(msg.browserId, msg.cols, msg.rows);
|
|
362
|
-
break;
|
|
363
|
-
case "session-create":
|
|
364
|
-
this.handleSessionCreate(msg.requestId, msg.name);
|
|
365
|
-
break;
|
|
366
|
-
case "session-kill":
|
|
367
|
-
this.handleSessionKill(msg.requestId, msg.name);
|
|
368
|
-
break;
|
|
369
|
-
default:
|
|
370
|
-
console.warn("[agent] Unknown message type:", msg.type);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
async syncSessions() {
|
|
374
|
-
try {
|
|
375
|
-
const sessions = await this.tmux.listSessions();
|
|
376
|
-
this.sendMessage({ type: "sessions-sync", sessions });
|
|
377
|
-
return sessions;
|
|
378
|
-
} catch (err) {
|
|
379
|
-
console.error("[agent] Failed to list sessions:", err);
|
|
380
|
-
this.sendMessage({ type: "error", message: "Failed to list sessions" });
|
|
381
|
-
return [];
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
async handleTerminalAttach(browserId, sessionName, cols, rows) {
|
|
385
|
-
const existing = this.bridges.get(browserId);
|
|
386
|
-
if (existing) {
|
|
387
|
-
existing.dispose();
|
|
388
|
-
this.bridges.delete(browserId);
|
|
389
|
-
}
|
|
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();
|
|
390
156
|
try {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
rows,
|
|
396
|
-
onData: (data) => {
|
|
397
|
-
this.sendMessage({ type: "terminal-output", browserId, data });
|
|
398
|
-
},
|
|
399
|
-
onExit: (exitCode) => {
|
|
400
|
-
this.bridges.delete(browserId);
|
|
401
|
-
this.sendMessage({ type: "terminal-exit", browserId, exitCode });
|
|
402
|
-
void this.syncSessions();
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
this.bridges.set(browserId, bridge);
|
|
406
|
-
this.sendMessage({ type: "terminal-ready", browserId, sessionName });
|
|
407
|
-
await this.syncSessions();
|
|
408
|
-
} catch (err) {
|
|
409
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
410
|
-
console.error(`[agent] Failed to attach terminal for browser ${browserId}:`, message);
|
|
411
|
-
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`);
|
|
412
161
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const bridge = this.bridges.get(browserId);
|
|
416
|
-
if (bridge) {
|
|
417
|
-
bridge.dispose();
|
|
418
|
-
this.bridges.delete(browserId);
|
|
419
|
-
void this.syncSessions();
|
|
162
|
+
if (installedService?.version) {
|
|
163
|
+
console.log(`Service Version: ${installedService.version}`);
|
|
420
164
|
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
handleTerminalResize(browserId, cols, rows) {
|
|
429
|
-
const bridge = this.bridges.get(browserId);
|
|
430
|
-
if (bridge) {
|
|
431
|
-
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);
|
|
432
172
|
}
|
|
433
|
-
}
|
|
434
|
-
async handleSessionCreate(requestId, name) {
|
|
435
173
|
try {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
|
|
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`);
|
|
443
190
|
} catch (err) {
|
|
444
191
|
const message = err instanceof Error ? err.message : String(err);
|
|
445
|
-
console.error(`[agent] Failed to
|
|
446
|
-
|
|
192
|
+
console.error(`[agent] Failed to install managed service: ${message}`);
|
|
193
|
+
console.error(`[agent] Service file path: ${servicePath()}`);
|
|
194
|
+
process.exit(1);
|
|
447
195
|
}
|
|
448
|
-
}
|
|
449
|
-
|
|
196
|
+
});
|
|
197
|
+
service.command("uninstall").description("Stop and remove the systemd user service").action(() => {
|
|
450
198
|
try {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
199
|
+
uninstallService();
|
|
200
|
+
console.log(`[agent] Service file removed: ${servicePath()}`);
|
|
201
|
+
console.log(`[agent] Service uninstalled.`);
|
|
454
202
|
} catch (err) {
|
|
455
203
|
const message = err instanceof Error ? err.message : String(err);
|
|
456
|
-
console.error(`[agent] Failed to
|
|
457
|
-
|
|
204
|
+
console.error(`[agent] Failed to uninstall service: ${message}`);
|
|
205
|
+
process.exit(1);
|
|
458
206
|
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
console.log(`[agent] Installing @webmux/agent@${targetVersion}...`);
|
|
207
|
+
});
|
|
208
|
+
service.command("status").description("Show systemd service status").action(() => {
|
|
462
209
|
try {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
console.error("[agent] Update failed:", err instanceof Error ? err.message : err);
|
|
467
|
-
console.log("[agent] Continuing with current version");
|
|
468
|
-
this.startHeartbeat();
|
|
469
|
-
this.startSessionSync();
|
|
470
|
-
this.syncSessions();
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
this.stop();
|
|
474
|
-
process.exit(0);
|
|
475
|
-
}
|
|
476
|
-
sendMessage(msg) {
|
|
477
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
478
|
-
this.ws.send(JSON.stringify(msg));
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
startHeartbeat() {
|
|
482
|
-
this.stopHeartbeat();
|
|
483
|
-
this.heartbeatTimer = setInterval(() => {
|
|
484
|
-
this.sendMessage({ type: "heartbeat" });
|
|
485
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
486
|
-
}
|
|
487
|
-
startSessionSync() {
|
|
488
|
-
this.stopSessionSync();
|
|
489
|
-
this.sessionSyncTimer = setInterval(() => {
|
|
490
|
-
void this.syncSessions();
|
|
491
|
-
}, SESSION_SYNC_INTERVAL_MS);
|
|
492
|
-
}
|
|
493
|
-
stopHeartbeat() {
|
|
494
|
-
if (this.heartbeatTimer) {
|
|
495
|
-
clearInterval(this.heartbeatTimer);
|
|
496
|
-
this.heartbeatTimer = null;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
stopSessionSync() {
|
|
500
|
-
if (this.sessionSyncTimer) {
|
|
501
|
-
clearInterval(this.sessionSyncTimer);
|
|
502
|
-
this.sessionSyncTimer = null;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
disposeAllBridges() {
|
|
506
|
-
for (const [browserId, bridge] of this.bridges) {
|
|
507
|
-
bridge.dispose();
|
|
508
|
-
this.bridges.delete(browserId);
|
|
210
|
+
execFileSync("systemctl", ["--user", "status", SERVICE_NAME], { stdio: "inherit" });
|
|
211
|
+
} catch {
|
|
212
|
+
console.log(`[agent] Service is not installed or not running.`);
|
|
509
213
|
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (this.stopped) {
|
|
517
|
-
return;
|
|
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);
|
|
518
220
|
}
|
|
519
|
-
console.log(`[agent]
|
|
520
|
-
this.reconnectTimer = setTimeout(() => {
|
|
521
|
-
this.reconnectTimer = null;
|
|
522
|
-
this.connect();
|
|
523
|
-
}, this.reconnectDelay);
|
|
524
|
-
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
function buildWsUrl(serverUrl) {
|
|
528
|
-
const url = new URL("/ws/agent", serverUrl);
|
|
529
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
530
|
-
return url.toString();
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// src/cli.ts
|
|
534
|
-
var SERVICE_NAME = "webmux-agent";
|
|
535
|
-
var program = new Command();
|
|
536
|
-
program.name("webmux-agent").description("Webmux agent \u2014 connects your machine to the webmux server").version("0.0.0");
|
|
537
|
-
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
|
-
const serverUrl = opts.server.replace(/\/+$/, "");
|
|
539
|
-
const agentName = opts.name ?? os2.hostname();
|
|
540
|
-
console.log(`[agent] Registering with server ${serverUrl}...`);
|
|
541
|
-
console.log(`[agent] Agent name: ${agentName}`);
|
|
542
|
-
const body = {
|
|
543
|
-
token: opts.token,
|
|
544
|
-
name: agentName
|
|
545
|
-
};
|
|
546
|
-
let response;
|
|
547
|
-
try {
|
|
548
|
-
response = await fetch(`${serverUrl}/api/agents/register`, {
|
|
549
|
-
method: "POST",
|
|
550
|
-
headers: { "Content-Type": "application/json" },
|
|
551
|
-
body: JSON.stringify(body)
|
|
552
|
-
});
|
|
553
|
-
} catch (err) {
|
|
554
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
555
|
-
console.error(`[agent] Failed to connect to server: ${message}`);
|
|
556
|
-
process.exit(1);
|
|
557
|
-
}
|
|
558
|
-
if (!response.ok) {
|
|
559
|
-
let errorMessage;
|
|
221
|
+
console.log(`[agent] Switching managed service to ${opts.to}...`);
|
|
560
222
|
try {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
223
|
+
upgradeService({
|
|
224
|
+
agentName: creds.name,
|
|
225
|
+
packageName: AGENT_PACKAGE_NAME,
|
|
226
|
+
version: opts.to
|
|
227
|
+
});
|
|
228
|
+
console.log("[agent] Managed service updated successfully.");
|
|
229
|
+
} catch (err) {
|
|
230
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
231
|
+
console.error(`[agent] Failed to upgrade managed service: ${message}`);
|
|
232
|
+
process.exit(1);
|
|
565
233
|
}
|
|
566
|
-
console.error(`[agent] Registration failed: ${errorMessage}`);
|
|
567
|
-
process.exit(1);
|
|
568
|
-
}
|
|
569
|
-
const result = await response.json();
|
|
570
|
-
saveCredentials({
|
|
571
|
-
serverUrl,
|
|
572
|
-
agentId: result.agentId,
|
|
573
|
-
agentSecret: result.agentSecret,
|
|
574
|
-
name: agentName
|
|
575
234
|
});
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const creds = loadCredentials();
|
|
586
|
-
if (!creds) {
|
|
587
|
-
console.error(
|
|
588
|
-
`[agent] No credentials found at ${credentialsPath()}. Run "npx @webmux/agent register" first.`
|
|
589
|
-
);
|
|
590
|
-
process.exit(1);
|
|
591
|
-
}
|
|
592
|
-
console.log(`[agent] Starting agent "${creds.name}"...`);
|
|
593
|
-
console.log(`[agent] Server: ${creds.serverUrl}`);
|
|
594
|
-
console.log(`[agent] Agent ID: ${creds.agentId}`);
|
|
595
|
-
const tmux = new TmuxClient({
|
|
596
|
-
socketName: "webmux",
|
|
597
|
-
workspaceRoot: os2.homedir()
|
|
598
|
-
});
|
|
599
|
-
const connection = new AgentConnection(
|
|
600
|
-
creds.serverUrl,
|
|
601
|
-
creds.agentId,
|
|
602
|
-
creds.agentSecret,
|
|
603
|
-
tmux
|
|
604
|
-
);
|
|
605
|
-
const shutdown = () => {
|
|
606
|
-
console.log("\n[agent] Shutting down...");
|
|
607
|
-
connection.stop();
|
|
608
|
-
process.exit(0);
|
|
609
|
-
};
|
|
610
|
-
process.on("SIGINT", shutdown);
|
|
611
|
-
process.on("SIGTERM", shutdown);
|
|
612
|
-
connection.start();
|
|
613
|
-
});
|
|
614
|
-
program.command("status").description("Show agent status and credentials info").action(() => {
|
|
615
|
-
const creds = loadCredentials();
|
|
616
|
-
if (!creds) {
|
|
617
|
-
console.log(`[agent] Not registered. No credentials found at ${credentialsPath()}.`);
|
|
618
|
-
process.exit(0);
|
|
619
|
-
}
|
|
620
|
-
console.log(`Agent Name: ${creds.name}`);
|
|
621
|
-
console.log(`Server URL: ${creds.serverUrl}`);
|
|
622
|
-
console.log(`Agent ID: ${creds.agentId}`);
|
|
623
|
-
console.log(`Credentials File: ${credentialsPath()}`);
|
|
624
|
-
try {
|
|
625
|
-
const result = execSync2(`systemctl --user is-active ${SERVICE_NAME} 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
626
|
-
console.log(`Service: ${result}`);
|
|
627
|
-
} catch {
|
|
628
|
-
console.log(`Service: not installed`);
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
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(() => {
|
|
633
|
-
const creds = loadCredentials();
|
|
634
|
-
if (!creds) {
|
|
635
|
-
console.error(`[agent] Not registered. Run "npx @webmux/agent register" first.`);
|
|
636
|
-
process.exit(1);
|
|
637
|
-
}
|
|
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
|
-
try {
|
|
668
|
-
execSync2("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
669
|
-
execSync2(`systemctl --user enable ${SERVICE_NAME}`, { stdio: "inherit" });
|
|
670
|
-
execSync2(`systemctl --user start ${SERVICE_NAME}`, { stdio: "inherit" });
|
|
671
|
-
execSync2(`loginctl enable-linger ${os2.userInfo().username}`, { stdio: "inherit" });
|
|
672
|
-
console.log(``);
|
|
673
|
-
console.log(`[agent] Service installed and started!`);
|
|
674
|
-
console.log(`[agent] It will auto-start on boot.`);
|
|
675
|
-
console.log(``);
|
|
676
|
-
console.log(`Useful commands:`);
|
|
677
|
-
console.log(` systemctl --user status ${SERVICE_NAME}`);
|
|
678
|
-
console.log(` journalctl --user -u ${SERVICE_NAME} -f`);
|
|
679
|
-
console.log(` npx @webmux/agent service uninstall`);
|
|
680
|
-
} catch (err) {
|
|
681
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
682
|
-
console.error(`[agent] Failed to enable service: ${message}`);
|
|
683
|
-
console.error(`[agent] Service file was written to ${servicePath}`);
|
|
684
|
-
console.error(`[agent] You can try manually: systemctl --user enable --now ${SERVICE_NAME}`);
|
|
685
|
-
process.exit(1);
|
|
686
|
-
}
|
|
687
|
-
});
|
|
688
|
-
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
|
-
try {
|
|
691
|
-
execSync2(`systemctl --user stop ${SERVICE_NAME} 2>/dev/null`, { stdio: "inherit" });
|
|
692
|
-
execSync2(`systemctl --user disable ${SERVICE_NAME} 2>/dev/null`, { stdio: "inherit" });
|
|
693
|
-
} catch {
|
|
694
|
-
}
|
|
695
|
-
if (fs2.existsSync(servicePath)) {
|
|
696
|
-
fs2.unlinkSync(servicePath);
|
|
697
|
-
console.log(`[agent] Service file removed: ${servicePath}`);
|
|
698
|
-
}
|
|
699
|
-
try {
|
|
700
|
-
execSync2("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
701
|
-
} catch {
|
|
702
|
-
}
|
|
703
|
-
console.log(`[agent] Service uninstalled.`);
|
|
704
|
-
});
|
|
705
|
-
service.command("status").description("Show systemd service status").action(() => {
|
|
706
|
-
try {
|
|
707
|
-
execSync2(`systemctl --user status ${SERVICE_NAME}`, { stdio: "inherit" });
|
|
708
|
-
} catch {
|
|
709
|
-
console.log(`[agent] Service is not installed or not running.`);
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
service.command("upgrade").description("Upgrade agent to latest version and restart service").action(() => {
|
|
713
|
-
console.log("[agent] Upgrading @webmux/agent to latest...");
|
|
714
|
-
try {
|
|
715
|
-
execSync2("npm install -g @webmux/agent@latest", { stdio: "inherit" });
|
|
716
|
-
} catch {
|
|
717
|
-
console.error("[agent] Failed to upgrade. Try manually: npm install -g @webmux/agent@latest");
|
|
718
|
-
process.exit(1);
|
|
719
|
-
}
|
|
720
|
-
console.log("[agent] Restarting service...");
|
|
721
|
-
try {
|
|
722
|
-
execSync2(`systemctl --user restart ${SERVICE_NAME}`, { stdio: "inherit" });
|
|
723
|
-
console.log("[agent] Upgrade complete!");
|
|
724
|
-
} catch {
|
|
725
|
-
console.log("[agent] Package upgraded. Service not installed or restart failed.");
|
|
726
|
-
console.log("[agent] If running manually, restart with: npx @webmux/agent@latest start");
|
|
727
|
-
}
|
|
728
|
-
});
|
|
729
|
-
function findBinary(name) {
|
|
730
|
-
try {
|
|
731
|
-
return execSync2(`which ${name} 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
732
|
-
} catch {
|
|
733
|
-
return null;
|
|
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;
|
|
734
244
|
}
|
|
245
|
+
return import.meta.url === pathToFileURL(entryPath).href;
|
|
735
246
|
}
|
|
736
|
-
|
|
247
|
+
if (isDirectExecution()) {
|
|
248
|
+
void run();
|
|
249
|
+
}
|
|
250
|
+
export {
|
|
251
|
+
createProgram,
|
|
252
|
+
run
|
|
253
|
+
};
|