@webmux/agent 0.1.0 → 0.1.2

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.
Files changed (2) hide show
  1. package/dist/cli.js +140 -13
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import fs2 from "fs";
4
5
  import os2 from "os";
6
+ import path2 from "path";
7
+ import { execSync } from "child_process";
5
8
  import { Command } from "commander";
6
9
 
7
10
  // src/credentials.ts
@@ -165,9 +168,9 @@ function assertValidSessionName(name) {
165
168
  function parseSessionList(stdout) {
166
169
  return stdout.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
167
170
  const parts = line.split(FIELD_SEPARATOR);
168
- const [name, windows, attachedClients, createdAt, lastActivityAt, path2] = parts;
171
+ const [name, windows, attachedClients, createdAt, lastActivityAt, path3] = parts;
169
172
  const currentCommand = parts[6] ?? "";
170
- if (!name || !windows || !attachedClients || !createdAt || !lastActivityAt || !path2) {
173
+ if (!name || !windows || !attachedClients || !createdAt || !lastActivityAt || !path3) {
171
174
  return [];
172
175
  }
173
176
  return [
@@ -177,7 +180,7 @@ function parseSessionList(stdout) {
177
180
  attachedClients: Number(attachedClients),
178
181
  createdAt: Number(createdAt),
179
182
  lastActivityAt: Number(lastActivityAt),
180
- path: path2,
183
+ path: path3,
181
184
  currentCommand
182
185
  }
183
186
  ];
@@ -251,6 +254,7 @@ async function createTerminalBridge(options) {
251
254
 
252
255
  // src/connection.ts
253
256
  var HEARTBEAT_INTERVAL_MS = 3e4;
257
+ var SESSION_SYNC_INTERVAL_MS = 15e3;
254
258
  var INITIAL_RECONNECT_DELAY_MS = 1e3;
255
259
  var MAX_RECONNECT_DELAY_MS = 3e4;
256
260
  var AgentConnection = class {
@@ -260,6 +264,7 @@ var AgentConnection = class {
260
264
  tmux;
261
265
  ws = null;
262
266
  heartbeatTimer = null;
267
+ sessionSyncTimer = null;
263
268
  reconnectTimer = null;
264
269
  reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
265
270
  bridges = /* @__PURE__ */ new Map();
@@ -281,6 +286,7 @@ var AgentConnection = class {
281
286
  this.reconnectTimer = null;
282
287
  }
283
288
  this.stopHeartbeat();
289
+ this.stopSessionSync();
284
290
  this.disposeAllBridges();
285
291
  if (this.ws) {
286
292
  this.ws.close(1e3, "agent shutting down");
@@ -320,6 +326,7 @@ var AgentConnection = class {
320
326
  case "auth-ok":
321
327
  console.log("[agent] Authenticated successfully");
322
328
  this.startHeartbeat();
329
+ this.startSessionSync();
323
330
  this.syncSessions();
324
331
  break;
325
332
  case "auth-fail":
@@ -347,10 +354,10 @@ var AgentConnection = class {
347
354
  this.handleTerminalResize(msg.browserId, msg.cols, msg.rows);
348
355
  break;
349
356
  case "session-create":
350
- this.handleSessionCreate(msg.name);
357
+ this.handleSessionCreate(msg.requestId, msg.name);
351
358
  break;
352
359
  case "session-kill":
353
- this.handleSessionKill(msg.name);
360
+ this.handleSessionKill(msg.requestId, msg.name);
354
361
  break;
355
362
  default:
356
363
  console.warn("[agent] Unknown message type:", msg.type);
@@ -360,9 +367,11 @@ var AgentConnection = class {
360
367
  try {
361
368
  const sessions = await this.tmux.listSessions();
362
369
  this.sendMessage({ type: "sessions-sync", sessions });
370
+ return sessions;
363
371
  } catch (err) {
364
372
  console.error("[agent] Failed to list sessions:", err);
365
373
  this.sendMessage({ type: "error", message: "Failed to list sessions" });
374
+ return [];
366
375
  }
367
376
  }
368
377
  async handleTerminalAttach(browserId, sessionName, cols, rows) {
@@ -383,10 +392,12 @@ var AgentConnection = class {
383
392
  onExit: (exitCode) => {
384
393
  this.bridges.delete(browserId);
385
394
  this.sendMessage({ type: "terminal-exit", browserId, exitCode });
395
+ void this.syncSessions();
386
396
  }
387
397
  });
388
398
  this.bridges.set(browserId, bridge);
389
399
  this.sendMessage({ type: "terminal-ready", browserId, sessionName });
400
+ await this.syncSessions();
390
401
  } catch (err) {
391
402
  const message = err instanceof Error ? err.message : String(err);
392
403
  console.error(`[agent] Failed to attach terminal for browser ${browserId}:`, message);
@@ -398,6 +409,7 @@ var AgentConnection = class {
398
409
  if (bridge) {
399
410
  bridge.dispose();
400
411
  this.bridges.delete(browserId);
412
+ void this.syncSessions();
401
413
  }
402
414
  }
403
415
  handleTerminalInput(browserId, data) {
@@ -412,24 +424,30 @@ var AgentConnection = class {
412
424
  bridge.resize(cols, rows);
413
425
  }
414
426
  }
415
- async handleSessionCreate(name) {
427
+ async handleSessionCreate(requestId, name) {
416
428
  try {
417
429
  await this.tmux.createSession(name);
418
- await this.syncSessions();
430
+ const sessions = await this.syncSessions();
431
+ const session = sessions.find((item) => item.name === name);
432
+ if (!session) {
433
+ throw new Error("Created session was not returned by tmux");
434
+ }
435
+ this.sendMessage({ type: "command-result", requestId, ok: true, session });
419
436
  } catch (err) {
420
437
  const message = err instanceof Error ? err.message : String(err);
421
438
  console.error(`[agent] Failed to create session "${name}":`, message);
422
- this.sendMessage({ type: "error", message: `Failed to create session: ${message}` });
439
+ this.sendMessage({ type: "command-result", requestId, ok: false, error: message });
423
440
  }
424
441
  }
425
- async handleSessionKill(name) {
442
+ async handleSessionKill(requestId, name) {
426
443
  try {
427
444
  await this.tmux.killSession(name);
428
445
  await this.syncSessions();
446
+ this.sendMessage({ type: "command-result", requestId, ok: true });
429
447
  } catch (err) {
430
448
  const message = err instanceof Error ? err.message : String(err);
431
449
  console.error(`[agent] Failed to kill session "${name}":`, message);
432
- this.sendMessage({ type: "error", message: `Failed to kill session: ${message}` });
450
+ this.sendMessage({ type: "command-result", requestId, ok: false, error: message });
433
451
  }
434
452
  }
435
453
  sendMessage(msg) {
@@ -443,12 +461,24 @@ var AgentConnection = class {
443
461
  this.sendMessage({ type: "heartbeat" });
444
462
  }, HEARTBEAT_INTERVAL_MS);
445
463
  }
464
+ startSessionSync() {
465
+ this.stopSessionSync();
466
+ this.sessionSyncTimer = setInterval(() => {
467
+ void this.syncSessions();
468
+ }, SESSION_SYNC_INTERVAL_MS);
469
+ }
446
470
  stopHeartbeat() {
447
471
  if (this.heartbeatTimer) {
448
472
  clearInterval(this.heartbeatTimer);
449
473
  this.heartbeatTimer = null;
450
474
  }
451
475
  }
476
+ stopSessionSync() {
477
+ if (this.sessionSyncTimer) {
478
+ clearInterval(this.sessionSyncTimer);
479
+ this.sessionSyncTimer = null;
480
+ }
481
+ }
452
482
  disposeAllBridges() {
453
483
  for (const [browserId, bridge] of this.bridges) {
454
484
  bridge.dispose();
@@ -457,6 +487,7 @@ var AgentConnection = class {
457
487
  }
458
488
  onDisconnect() {
459
489
  this.stopHeartbeat();
490
+ this.stopSessionSync();
460
491
  this.disposeAllBridges();
461
492
  this.ws = null;
462
493
  if (this.stopped) {
@@ -477,6 +508,7 @@ function buildWsUrl(serverUrl) {
477
508
  }
478
509
 
479
510
  // src/cli.ts
511
+ var SERVICE_NAME = "webmux-agent";
480
512
  var program = new Command();
481
513
  program.name("webmux-agent").description("Webmux agent \u2014 connects your machine to the webmux server").version("0.0.0");
482
514
  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) => {
@@ -521,13 +553,16 @@ program.command("register").description("Register this agent with a webmux serve
521
553
  console.log(`[agent] Registration successful!`);
522
554
  console.log(`[agent] Agent ID: ${result.agentId}`);
523
555
  console.log(`[agent] Credentials saved to ${credentialsPath()}`);
524
- console.log(`[agent] Run "webmux-agent start" to connect.`);
556
+ console.log(``);
557
+ console.log(`Next steps:`);
558
+ console.log(` npx @webmux/agent start # run once`);
559
+ console.log(` npx @webmux/agent service install # install as systemd service`);
525
560
  });
526
561
  program.command("start").description("Start the agent and connect to the server").action(() => {
527
562
  const creds = loadCredentials();
528
563
  if (!creds) {
529
564
  console.error(
530
- `[agent] No credentials found at ${credentialsPath()}. Run "webmux-agent register" first.`
565
+ `[agent] No credentials found at ${credentialsPath()}. Run "npx @webmux/agent register" first.`
531
566
  );
532
567
  process.exit(1);
533
568
  }
@@ -536,7 +571,7 @@ program.command("start").description("Start the agent and connect to the server"
536
571
  console.log(`[agent] Agent ID: ${creds.agentId}`);
537
572
  const tmux = new TmuxClient({
538
573
  socketName: "webmux",
539
- workspaceRoot: process.cwd()
574
+ workspaceRoot: os2.homedir()
540
575
  });
541
576
  const connection = new AgentConnection(
542
577
  creds.serverUrl,
@@ -563,5 +598,97 @@ program.command("status").description("Show agent status and credentials info").
563
598
  console.log(`Server URL: ${creds.serverUrl}`);
564
599
  console.log(`Agent ID: ${creds.agentId}`);
565
600
  console.log(`Credentials File: ${credentialsPath()}`);
601
+ try {
602
+ const result = execSync(`systemctl --user is-active ${SERVICE_NAME} 2>/dev/null`, { encoding: "utf-8" }).trim();
603
+ console.log(`Service: ${result}`);
604
+ } catch {
605
+ console.log(`Service: not installed`);
606
+ }
566
607
  });
608
+ var service = program.command("service").description("Manage the systemd service");
609
+ service.command("install").description("Install and start the agent as a systemd user service").action(() => {
610
+ const creds = loadCredentials();
611
+ if (!creds) {
612
+ console.error(`[agent] Not registered. Run "npx @webmux/agent register" first.`);
613
+ process.exit(1);
614
+ }
615
+ const npxPath = findBinary("npx");
616
+ if (!npxPath) {
617
+ console.error(`[agent] Cannot find npx. Make sure Node.js is installed.`);
618
+ process.exit(1);
619
+ }
620
+ const serviceDir = path2.join(os2.homedir(), ".config", "systemd", "user");
621
+ const servicePath = path2.join(serviceDir, `${SERVICE_NAME}.service`);
622
+ const unit = `[Unit]
623
+ Description=Webmux Agent (${creds.name})
624
+ After=network-online.target
625
+ Wants=network-online.target
626
+
627
+ [Service]
628
+ Type=simple
629
+ ExecStart=${npxPath} -y @webmux/agent start
630
+ Restart=always
631
+ RestartSec=5
632
+ Environment=HOME=${os2.homedir()}
633
+ Environment=PATH=${process.env.PATH}
634
+ WorkingDirectory=${os2.homedir()}
635
+
636
+ [Install]
637
+ WantedBy=default.target
638
+ `;
639
+ fs2.mkdirSync(serviceDir, { recursive: true });
640
+ fs2.writeFileSync(servicePath, unit);
641
+ console.log(`[agent] Service file created: ${servicePath}`);
642
+ try {
643
+ execSync("systemctl --user daemon-reload", { stdio: "inherit" });
644
+ execSync(`systemctl --user enable ${SERVICE_NAME}`, { stdio: "inherit" });
645
+ execSync(`systemctl --user start ${SERVICE_NAME}`, { stdio: "inherit" });
646
+ execSync(`loginctl enable-linger ${os2.userInfo().username}`, { stdio: "inherit" });
647
+ console.log(``);
648
+ console.log(`[agent] Service installed and started!`);
649
+ console.log(`[agent] It will auto-start on boot.`);
650
+ console.log(``);
651
+ console.log(`Useful commands:`);
652
+ console.log(` systemctl --user status ${SERVICE_NAME}`);
653
+ console.log(` journalctl --user -u ${SERVICE_NAME} -f`);
654
+ console.log(` npx @webmux/agent service uninstall`);
655
+ } catch (err) {
656
+ const message = err instanceof Error ? err.message : String(err);
657
+ console.error(`[agent] Failed to enable service: ${message}`);
658
+ console.error(`[agent] Service file was written to ${servicePath}`);
659
+ console.error(`[agent] You can try manually: systemctl --user enable --now ${SERVICE_NAME}`);
660
+ process.exit(1);
661
+ }
662
+ });
663
+ service.command("uninstall").description("Stop and remove the systemd user service").action(() => {
664
+ const servicePath = path2.join(os2.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
665
+ try {
666
+ execSync(`systemctl --user stop ${SERVICE_NAME} 2>/dev/null`, { stdio: "inherit" });
667
+ execSync(`systemctl --user disable ${SERVICE_NAME} 2>/dev/null`, { stdio: "inherit" });
668
+ } catch {
669
+ }
670
+ if (fs2.existsSync(servicePath)) {
671
+ fs2.unlinkSync(servicePath);
672
+ console.log(`[agent] Service file removed: ${servicePath}`);
673
+ }
674
+ try {
675
+ execSync("systemctl --user daemon-reload", { stdio: "inherit" });
676
+ } catch {
677
+ }
678
+ console.log(`[agent] Service uninstalled.`);
679
+ });
680
+ service.command("status").description("Show systemd service status").action(() => {
681
+ try {
682
+ execSync(`systemctl --user status ${SERVICE_NAME}`, { stdio: "inherit" });
683
+ } catch {
684
+ console.log(`[agent] Service is not installed or not running.`);
685
+ }
686
+ });
687
+ function findBinary(name) {
688
+ try {
689
+ return execSync(`which ${name} 2>/dev/null`, { encoding: "utf-8" }).trim();
690
+ } catch {
691
+ return null;
692
+ }
693
+ }
567
694
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmux/agent",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "webmux-agent": "./dist/cli.js"