@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/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 path2 from "path";
7
- import { execSync as execSync2 } from "child_process";
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/tmux.ts
47
- import { execFile } from "child_process";
48
- import { promisify } from "util";
49
- var execFileAsync = promisify(execFile);
50
- var FIELD_SEPARATOR = "";
51
- var SESSION_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,31}$/;
52
- var TMUX_EMPTY_STATE_MARKERS = [
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
- // src/connection.ts
257
- var AGENT_VERSION = "0.1.4";
258
- var HEARTBEAT_INTERVAL_MS = 3e4;
259
- var SESSION_SYNC_INTERVAL_MS = 15e3;
260
- var INITIAL_RECONNECT_DELAY_MS = 1e3;
261
- var MAX_RECONNECT_DELAY_MS = 3e4;
262
- var AgentConnection = class {
263
- serverUrl;
264
- agentId;
265
- agentSecret;
266
- tmux;
267
- ws = null;
268
- heartbeatTimer = null;
269
- sessionSyncTimer = null;
270
- reconnectTimer = null;
271
- reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
272
- bridges = /* @__PURE__ */ new Map();
273
- stopped = false;
274
- constructor(serverUrl, agentId, agentSecret, tmux) {
275
- this.serverUrl = serverUrl;
276
- this.agentId = agentId;
277
- this.agentSecret = agentSecret;
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
- connect() {
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
- msg = JSON.parse(raw.toString());
90
+ const errorBody = await response.json();
91
+ errorMessage = errorBody.error ?? response.statusText;
312
92
  } catch {
313
- console.error("[agent] Failed to parse server message:", raw.toString());
314
- return;
93
+ errorMessage = response.statusText;
315
94
  }
316
- this.handleMessage(msg);
317
- });
318
- ws.on("close", (code, reason) => {
319
- console.log(`[agent] WebSocket closed: code=${code} reason=${reason.toString()}`);
320
- this.onDisconnect();
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
- ws.on("error", (err) => {
323
- console.error("[agent] WebSocket error:", err.message);
105
+ console.log(`[agent] Registration successful!`);
106
+ console.log(`[agent] Agent ID: ${result.agentId}`);
107
+ console.log(`[agent] Credentials saved to ${credentialsPath()}`);
108
+ console.log(``);
109
+ console.log(`Next steps:`);
110
+ console.log(` pnpm dlx @webmux/agent start # run once`);
111
+ console.log(` pnpm dlx @webmux/agent service install # install as managed systemd service`);
112
+ });
113
+ program.command("start").description("Start the agent and connect to the server").action(async () => {
114
+ const creds = loadCredentials();
115
+ if (!creds) {
116
+ console.error(
117
+ `[agent] No credentials found at ${credentialsPath()}. Run "npx @webmux/agent register" first.`
118
+ );
119
+ process.exit(1);
120
+ }
121
+ const { AgentConnection, TmuxClient } = await loadAgentRuntime();
122
+ console.log(`[agent] Starting agent "${creds.name}"...`);
123
+ console.log(`[agent] Server: ${creds.serverUrl}`);
124
+ console.log(`[agent] Agent ID: ${creds.agentId}`);
125
+ const tmux = new TmuxClient({
126
+ socketName: "webmux",
127
+ workspaceRoot: os2.homedir()
324
128
  });
325
- }
326
- handleMessage(msg) {
327
- switch (msg.type) {
328
- case "auth-ok":
329
- console.log("[agent] Authenticated successfully");
330
- if (msg.latestVersion && msg.latestVersion !== AGENT_VERSION) {
331
- console.log(`[agent] Update available: ${AGENT_VERSION} \u2192 ${msg.latestVersion}`);
332
- this.selfUpdate(msg.latestVersion);
333
- return;
334
- }
335
- this.startHeartbeat();
336
- this.startSessionSync();
337
- this.syncSessions();
338
- break;
339
- case "auth-fail":
340
- console.error(`[agent] Authentication failed: ${msg.message}`);
341
- this.stopped = true;
342
- if (this.ws) {
343
- this.ws.close();
344
- this.ws = null;
345
- }
346
- process.exit(1);
347
- break;
348
- case "sessions-list":
349
- this.syncSessions();
350
- break;
351
- case "terminal-attach":
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 bridge = await createTerminalBridge({
392
- tmux: this.tmux,
393
- sessionName,
394
- cols,
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
- handleTerminalDetach(browserId) {
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
- handleTerminalInput(browserId, data) {
423
- const bridge = this.bridges.get(browserId);
424
- if (bridge) {
425
- bridge.write(data);
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
- await this.tmux.createSession(name);
437
- const sessions = await this.syncSessions();
438
- const session = sessions.find((item) => item.name === name);
439
- if (!session) {
440
- throw new Error("Created session was not returned by tmux");
441
- }
442
- this.sendMessage({ type: "command-result", requestId, ok: true, session });
174
+ installService({
175
+ agentName: creds.name,
176
+ packageName: AGENT_PACKAGE_NAME,
177
+ version: AGENT_VERSION,
178
+ autoUpgrade: opts.autoUpgrade
179
+ });
180
+ console.log(``);
181
+ console.log(`[agent] Service installed and started!`);
182
+ console.log(`[agent] Managed version: ${AGENT_VERSION}`);
183
+ console.log(`[agent] It will auto-start on boot.`);
184
+ console.log(``);
185
+ console.log(`Useful commands:`);
186
+ console.log(` systemctl --user status ${SERVICE_NAME}`);
187
+ console.log(` journalctl --user -u ${SERVICE_NAME} -f`);
188
+ console.log(` pnpm dlx @webmux/agent service upgrade --to <version>`);
189
+ console.log(` pnpm dlx @webmux/agent service uninstall`);
443
190
  } catch (err) {
444
191
  const message = err instanceof Error ? err.message : String(err);
445
- console.error(`[agent] Failed to create session "${name}":`, message);
446
- this.sendMessage({ type: "command-result", requestId, ok: false, error: message });
192
+ console.error(`[agent] Failed to install managed service: ${message}`);
193
+ console.error(`[agent] Service file path: ${servicePath()}`);
194
+ process.exit(1);
447
195
  }
448
- }
449
- async handleSessionKill(requestId, name) {
196
+ });
197
+ service.command("uninstall").description("Stop and remove the systemd user service").action(() => {
450
198
  try {
451
- await this.tmux.killSession(name);
452
- await this.syncSessions();
453
- this.sendMessage({ type: "command-result", requestId, ok: true });
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 kill session "${name}":`, message);
457
- this.sendMessage({ type: "command-result", requestId, ok: false, error: message });
204
+ console.error(`[agent] Failed to uninstall service: ${message}`);
205
+ process.exit(1);
458
206
  }
459
- }
460
- selfUpdate(targetVersion) {
461
- console.log(`[agent] Installing @webmux/agent@${targetVersion}...`);
207
+ });
208
+ service.command("status").description("Show systemd service status").action(() => {
462
209
  try {
463
- execSync(`npm install -g @webmux/agent@${targetVersion}`, { stdio: "inherit" });
464
- console.log("[agent] Update installed. Restarting...");
465
- } catch (err) {
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
- onDisconnect() {
512
- this.stopHeartbeat();
513
- this.stopSessionSync();
514
- this.disposeAllBridges();
515
- this.ws = null;
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] Reconnecting in ${this.reconnectDelay}ms...`);
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
- const errorBody = await response.json();
562
- errorMessage = errorBody.error ?? response.statusText;
563
- } catch {
564
- errorMessage = response.statusText;
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
- console.log(`[agent] Registration successful!`);
577
- console.log(`[agent] Agent ID: ${result.agentId}`);
578
- console.log(`[agent] Credentials saved to ${credentialsPath()}`);
579
- console.log(``);
580
- console.log(`Next steps:`);
581
- console.log(` npx @webmux/agent start # run once`);
582
- console.log(` npx @webmux/agent service install # install as systemd service`);
583
- });
584
- program.command("start").description("Start the agent and connect to the server").action(() => {
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
- program.parse();
247
+ if (isDirectExecution()) {
248
+ void run();
249
+ }
250
+ export {
251
+ createProgram,
252
+ run
253
+ };