@webmux/agent 0.2.0 → 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.
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/tmux.ts
4
+ import { execFile } from "child_process";
5
+ import { promisify } from "util";
6
+ var execFileAsync = promisify(execFile);
7
+ var FIELD_SEPARATOR = "";
8
+ var SESSION_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,31}$/;
9
+ var TMUX_EMPTY_STATE_MARKERS = [
10
+ "error connecting to",
11
+ "failed to connect to server",
12
+ "no server running",
13
+ "no sessions"
14
+ ];
15
+ var TmuxClient = class {
16
+ socketName;
17
+ workspaceRoot;
18
+ constructor(options) {
19
+ this.socketName = options.socketName;
20
+ this.workspaceRoot = options.workspaceRoot;
21
+ }
22
+ async listSessions() {
23
+ const stdout = await this.run(
24
+ [
25
+ "list-sessions",
26
+ "-F",
27
+ [
28
+ "#{session_name}",
29
+ "#{session_windows}",
30
+ "#{session_attached}",
31
+ "#{session_created}",
32
+ "#{session_activity}",
33
+ "#{session_path}",
34
+ "#{pane_current_command}"
35
+ ].join(FIELD_SEPARATOR)
36
+ ],
37
+ { allowEmptyState: true }
38
+ );
39
+ const sessions = parseSessionList(stdout);
40
+ const enriched = await Promise.all(
41
+ sessions.map(async (session) => ({
42
+ ...session,
43
+ preview: await this.getPreview(session.name)
44
+ }))
45
+ );
46
+ return enriched.sort((left, right) => {
47
+ if (left.lastActivityAt !== right.lastActivityAt) {
48
+ return right.lastActivityAt - left.lastActivityAt;
49
+ }
50
+ return left.name.localeCompare(right.name);
51
+ });
52
+ }
53
+ async createSession(name) {
54
+ assertValidSessionName(name);
55
+ if (await this.hasSession(name)) {
56
+ return;
57
+ }
58
+ await this.run(["new-session", "-d", "-s", name, "-c", this.workspaceRoot]);
59
+ }
60
+ async killSession(name) {
61
+ assertValidSessionName(name);
62
+ await this.run(["kill-session", "-t", name]);
63
+ }
64
+ async readSession(name) {
65
+ const sessions = await this.listSessions();
66
+ return sessions.find((session) => session.name === name) ?? null;
67
+ }
68
+ async hasSession(name) {
69
+ try {
70
+ await this.run(["has-session", "-t", name]);
71
+ return true;
72
+ } catch (error) {
73
+ const message = String(
74
+ error.stderr ?? error.message
75
+ );
76
+ if (isTmuxEmptyStateMessage(message)) {
77
+ return false;
78
+ }
79
+ return false;
80
+ }
81
+ }
82
+ async getPreview(name) {
83
+ try {
84
+ const stdout = await this.run(
85
+ ["capture-pane", "-p", "-J", "-S", "-18", "-E", "-", "-t", `${name}:`],
86
+ { allowEmptyState: true }
87
+ );
88
+ return formatPreview(stdout);
89
+ } catch {
90
+ return ["Session available. Tap to attach."];
91
+ }
92
+ }
93
+ async run(args, options = {}) {
94
+ try {
95
+ const { stdout } = await execFileAsync(
96
+ "tmux",
97
+ ["-L", this.socketName, ...args],
98
+ {
99
+ cwd: this.workspaceRoot,
100
+ env: {
101
+ ...process.env,
102
+ TERM: "xterm-256color"
103
+ }
104
+ }
105
+ );
106
+ return stdout;
107
+ } catch (error) {
108
+ const message = String(
109
+ error.stderr ?? error.message
110
+ );
111
+ if (options.allowEmptyState && TMUX_EMPTY_STATE_MARKERS.some((marker) => message.includes(marker))) {
112
+ return "";
113
+ }
114
+ throw error;
115
+ }
116
+ }
117
+ };
118
+ function assertValidSessionName(name) {
119
+ if (!SESSION_NAME_PATTERN.test(name)) {
120
+ throw new Error(
121
+ "Invalid session name. Use up to 32 letters, numbers, dot, dash, or underscore."
122
+ );
123
+ }
124
+ }
125
+ function parseSessionList(stdout) {
126
+ return stdout.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
127
+ const parts = line.split(FIELD_SEPARATOR);
128
+ const [name, windows, attachedClients, createdAt, lastActivityAt, path] = parts;
129
+ const currentCommand = parts[6] ?? "";
130
+ if (!name || !windows || !attachedClients || !createdAt || !lastActivityAt || !path) {
131
+ return [];
132
+ }
133
+ return [
134
+ {
135
+ name,
136
+ windows: Number(windows),
137
+ attachedClients: Number(attachedClients),
138
+ createdAt: Number(createdAt),
139
+ lastActivityAt: Number(lastActivityAt),
140
+ path,
141
+ currentCommand
142
+ }
143
+ ];
144
+ });
145
+ }
146
+ function formatPreview(stdout) {
147
+ const lines = stdout.replaceAll("\r", "").split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).slice(-3);
148
+ if (lines.length > 0) {
149
+ return lines;
150
+ }
151
+ return ["Fresh session. Nothing has run yet."];
152
+ }
153
+ function isTmuxEmptyStateMessage(message) {
154
+ return TMUX_EMPTY_STATE_MARKERS.some((marker) => message.includes(marker));
155
+ }
156
+
157
+ export {
158
+ TmuxClient,
159
+ assertValidSessionName,
160
+ parseSessionList,
161
+ formatPreview,
162
+ isTmuxEmptyStateMessage
163
+ };
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/service.ts
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { execFileSync } from "child_process";
8
+ var SERVICE_NAME = "webmux-agent";
9
+ function renderServiceUnit(options) {
10
+ return `[Unit]
11
+ Description=Webmux Agent (${options.agentName})
12
+ After=network-online.target
13
+ Wants=network-online.target
14
+
15
+ [Service]
16
+ Type=simple
17
+ ExecStart=${options.nodePath} ${options.cliPath} start
18
+ Restart=always
19
+ RestartSec=10
20
+ Environment=WEBMUX_AGENT_SERVICE=1
21
+ Environment=WEBMUX_AGENT_AUTO_UPGRADE=${options.autoUpgrade ? "1" : "0"}
22
+ Environment=WEBMUX_AGENT_NAME=${options.agentName}
23
+ Environment=HOME=${options.homeDir}
24
+ Environment=PATH=${options.pathEnv}
25
+ WorkingDirectory=${options.homeDir}
26
+
27
+ [Install]
28
+ WantedBy=default.target
29
+ `;
30
+ }
31
+ function installService(options) {
32
+ const homeDir = options.homeDir ?? os.homedir();
33
+ const autoUpgrade = options.autoUpgrade;
34
+ const release = installManagedRelease({
35
+ packageName: options.packageName,
36
+ version: options.version,
37
+ homeDir
38
+ });
39
+ writeServiceUnit({
40
+ agentName: options.agentName,
41
+ autoUpgrade,
42
+ cliPath: release.cliPath,
43
+ homeDir
44
+ });
45
+ runSystemctl(["--user", "daemon-reload"]);
46
+ runSystemctl(["--user", "enable", SERVICE_NAME]);
47
+ runSystemctl(["--user", "restart", SERVICE_NAME]);
48
+ runCommand("loginctl", ["enable-linger", os.userInfo().username]);
49
+ }
50
+ function upgradeService(options) {
51
+ const homeDir = options.homeDir ?? os.homedir();
52
+ const installedConfig = readInstalledServiceConfig(homeDir);
53
+ const autoUpgrade = options.autoUpgrade ?? installedConfig?.autoUpgrade ?? true;
54
+ const release = installManagedRelease({
55
+ packageName: options.packageName,
56
+ version: options.version,
57
+ homeDir
58
+ });
59
+ writeServiceUnit({
60
+ agentName: options.agentName,
61
+ autoUpgrade,
62
+ cliPath: release.cliPath,
63
+ homeDir
64
+ });
65
+ runSystemctl(["--user", "daemon-reload"]);
66
+ runSystemctl(["--user", "restart", SERVICE_NAME]);
67
+ }
68
+ function uninstallService(homeDir = os.homedir()) {
69
+ const unitPath = servicePath(homeDir);
70
+ try {
71
+ runSystemctl(["--user", "stop", SERVICE_NAME]);
72
+ } catch {
73
+ }
74
+ try {
75
+ runSystemctl(["--user", "disable", SERVICE_NAME]);
76
+ } catch {
77
+ }
78
+ if (fs.existsSync(unitPath)) {
79
+ fs.unlinkSync(unitPath);
80
+ }
81
+ try {
82
+ runSystemctl(["--user", "daemon-reload"]);
83
+ } catch {
84
+ }
85
+ }
86
+ function readInstalledServiceConfig(homeDir = os.homedir()) {
87
+ const unitPath = servicePath(homeDir);
88
+ if (!fs.existsSync(unitPath)) {
89
+ return null;
90
+ }
91
+ const unit = fs.readFileSync(unitPath, "utf-8");
92
+ const autoUpgradeMatch = unit.match(/^Environment=WEBMUX_AGENT_AUTO_UPGRADE=(\d)$/m);
93
+ const versionMatch = unit.match(/\/releases\/([^/\s]+)\/node_modules\//);
94
+ return {
95
+ autoUpgrade: autoUpgradeMatch?.[1] !== "0",
96
+ version: versionMatch?.[1] ?? null
97
+ };
98
+ }
99
+ function servicePath(homeDir = os.homedir()) {
100
+ return path.join(homeDir, ".config", "systemd", "user", `${SERVICE_NAME}.service`);
101
+ }
102
+ function writeServiceUnit(options) {
103
+ const serviceDir = path.dirname(servicePath(options.homeDir));
104
+ fs.mkdirSync(serviceDir, { recursive: true });
105
+ fs.writeFileSync(
106
+ servicePath(options.homeDir),
107
+ renderServiceUnit({
108
+ agentName: options.agentName,
109
+ autoUpgrade: options.autoUpgrade,
110
+ cliPath: options.cliPath,
111
+ homeDir: options.homeDir,
112
+ nodePath: findBinary("node") ?? process.execPath,
113
+ pathEnv: process.env.PATH ?? ""
114
+ })
115
+ );
116
+ }
117
+ function installManagedRelease(options) {
118
+ const releaseDir = path.join(options.homeDir, ".webmux", "releases", options.version);
119
+ const cliPath = path.join(
120
+ releaseDir,
121
+ "node_modules",
122
+ ...options.packageName.split("/"),
123
+ "dist",
124
+ "cli.js"
125
+ );
126
+ if (fs.existsSync(cliPath)) {
127
+ return { cliPath, releaseDir };
128
+ }
129
+ fs.mkdirSync(releaseDir, { recursive: true });
130
+ ensureRuntimePackageJson(releaseDir);
131
+ const packageManager = findBinary("pnpm") ? "pnpm" : "npm";
132
+ if (packageManager === "pnpm") {
133
+ runCommand("pnpm", ["add", "--dir", releaseDir, `${options.packageName}@${options.version}`]);
134
+ } else {
135
+ if (!findBinary("npm")) {
136
+ throw new Error("Cannot find pnpm or npm. Install one package manager before installing the service.");
137
+ }
138
+ runCommand("npm", ["install", "--omit=dev", `${options.packageName}@${options.version}`], releaseDir);
139
+ }
140
+ if (!fs.existsSync(cliPath)) {
141
+ throw new Error(`Managed release did not produce a CLI at ${cliPath}`);
142
+ }
143
+ return { cliPath, releaseDir };
144
+ }
145
+ function ensureRuntimePackageJson(releaseDir) {
146
+ const packageJsonPath = path.join(releaseDir, "package.json");
147
+ if (fs.existsSync(packageJsonPath)) {
148
+ return;
149
+ }
150
+ fs.writeFileSync(
151
+ packageJsonPath,
152
+ JSON.stringify({
153
+ name: "webmux-agent-runtime",
154
+ private: true
155
+ }, null, 2) + "\n"
156
+ );
157
+ }
158
+ function runSystemctl(args) {
159
+ runCommand("systemctl", args);
160
+ }
161
+ function runCommand(command, args, cwd) {
162
+ execFileSync(command, args, {
163
+ cwd,
164
+ stdio: "inherit"
165
+ });
166
+ }
167
+ function findBinary(name) {
168
+ try {
169
+ return execFileSync("which", [name], { encoding: "utf-8" }).trim();
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ // src/version.ts
176
+ import fs2 from "fs";
177
+ var packageMetadata = readAgentPackageMetadata();
178
+ var AGENT_PACKAGE_NAME = packageMetadata.name;
179
+ var AGENT_VERSION = packageMetadata.version;
180
+ function readAgentPackageMetadata() {
181
+ const packageJsonPath = new URL("../package.json", import.meta.url);
182
+ const raw = fs2.readFileSync(packageJsonPath, "utf-8");
183
+ const parsed = JSON.parse(raw);
184
+ if (!parsed.name || !parsed.version) {
185
+ throw new Error("Agent package metadata is missing name or version");
186
+ }
187
+ return {
188
+ name: parsed.name,
189
+ version: parsed.version
190
+ };
191
+ }
192
+
193
+ export {
194
+ SERVICE_NAME,
195
+ installService,
196
+ upgradeService,
197
+ uninstallService,
198
+ readInstalledServiceConfig,
199
+ servicePath,
200
+ AGENT_PACKAGE_NAME,
201
+ AGENT_VERSION
202
+ };