@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
|
@@ -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
|
+
};
|