@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.
- package/dist/cli.js +140 -13
- 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,
|
|
171
|
+
const [name, windows, attachedClients, createdAt, lastActivityAt, path3] = parts;
|
|
169
172
|
const currentCommand = parts[6] ?? "";
|
|
170
|
-
if (!name || !windows || !attachedClients || !createdAt || !lastActivityAt || !
|
|
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:
|
|
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: "
|
|
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: "
|
|
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(
|
|
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
|
|
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:
|
|
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();
|