Files
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

195 lines
5.5 KiB
JavaScript

/* eslint-disable no-undef */
function shQuote(str) {
return `'${String(str).replace(/'/g, `'\"'\"'`)}'`;
}
function wrapLoginShell(command) {
const oneLine = String(command || "")
.replace(/\r\n/g, "\n")
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.join("; ");
return `bash -lc ${JSON.stringify(oneLine)}`;
}
function wrapShExec(command) {
const oneLine = String(command || "")
.replace(/\r\n/g, "\n")
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.join("; ");
return `exec sh -c ${JSON.stringify(oneLine)}`;
}
function stripAnsi(text) {
return String(text || "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
}
function parseTmuxVersionString(text) {
const match = stripAnsi(text).match(/tmux\s+(\d+)\.(\d+)([a-z0-9]*)/i);
if (!match) {
return { raw: stripAnsi(text).trim(), major: 0, minor: 0, patch: "" };
}
return {
raw: match[0],
major: Number(match[1]) || 0,
minor: Number(match[2]) || 0,
patch: match[3] || "",
};
}
function getListSessionsFormat(version) {
const major = version?.major ?? 0;
const minor = version?.minor ?? 0;
if (major < 2) return null;
const fields = ["#{session_name}", "#{session_windows}", "#{session_attached}"];
if (major >= 3 || (major === 2 && minor >= 1)) {
fields.push("#{session_created}", "#{session_activity}");
}
if (major > 3 || (major === 3 && minor >= 2)) {
fields.push("#{session_group}");
}
return fields.join("\\t");
}
// Single-line script — multiline strings break when passed through bash -lc JSON quoting.
const TMUX_DETECT_SCRIPT = [
"uid=$(id -u 2>/dev/null || echo 0)",
"echo \"__TMUX_VERSION__=$(tmux -V 2>/dev/null || true)\"",
"echo \"__TMUX_BIN__=$(command -v tmux 2>/dev/null || which tmux 2>/dev/null || true)\"",
"for d in \"${TMUX_TMPDIR:-/tmp}/tmux-$uid\" \"/tmp/tmux-$uid\"; do",
"[ -d \"$d\" ] || continue",
"for s in \"$d\"/*; do [ -S \"$s\" ] && echo \"__SOCKET__=$s\"; done",
"done",
].join("; ");
function parseDetectScriptOutput(stdout) {
const info = {
version: { raw: "", major: 0, minor: 0, patch: "" },
binary: "",
sockets: [],
};
for (const line of stripAnsi(stdout).split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
if (trimmed.startsWith("__TMUX_VERSION__=")) {
info.version = parseTmuxVersionString(trimmed.slice("__TMUX_VERSION__=".length));
continue;
}
if (trimmed.startsWith("__TMUX_BIN__=")) {
info.binary = trimmed.slice("__TMUX_BIN__=".length).trim();
continue;
}
if (trimmed.startsWith("__SOCKET__=")) {
info.sockets.push(trimmed.slice("__SOCKET__=".length).trim());
}
}
info.sockets = [...new Set(info.sockets.filter(Boolean))];
return info;
}
function normalizeExecResult(result) {
if (!result) return { success: false, error: "No exec result" };
const stdout = stripAnsi(result.stdout || "");
const stderr = stripAnsi(result.stderr || "");
const combined = [stderr, stdout].filter(Boolean).join("\n").trim();
if (!result.success && combined) {
return {
...result,
success: true,
stdout: combined,
stderr,
code: result.code ?? 1,
};
}
return { ...result, stdout: combined || stdout, stderr };
}
function buildTmuxInvocation(binary, socketPath, args) {
const bin = binary || "tmux";
const socketFlag = socketPath ? `-S ${shQuote(socketPath)} ` : "";
return `${bin} ${socketFlag}${args}`.replace(/\s+/g, " ").trim();
}
// tmux diagnostics that must never be mistaken for session names — a stale
// socket makes `tmux ls 2>&1` print "error connecting to /tmp/tmux-0/default
// (No such file or directory)", which the bare-name fallback below would
// otherwise turn into a phantom session row.
const TMUX_DIAGNOSTIC_LINE = /^(error connecting to|no server running|no current client|can't find|lost server|server exited|failed to connect|protocol version mismatch|open terminal failed|invalid option|usage:|unknown command)/i;
function isTmuxDiagnosticLine(line) {
return TMUX_DIAGNOSTIC_LINE.test(String(line || "").trim());
}
function parseListOutput(stdout) {
const text = stripAnsi(stdout);
const plain = [];
const lines = text.split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
if (isTmuxDiagnosticLine(trimmed)) continue;
const match = trimmed.match(/^([^:]+):\s*(\d+)\s+windows?\b/i);
if (match) {
plain.push({
name: match[1].trim(),
windows: Number(match[2]) || 0,
attached: /\battached\b/i.test(trimmed),
created: 0,
activity: "",
group: "",
});
continue;
}
const parts = trimmed.split("\t");
if (parts.length >= 4) {
plain.push({
name: parts[0].trim(),
windows: Number(parts[1]) || 0,
attached: parts[2] === "1",
created: Number(parts[3]) || 0,
activity: parts[4] || "",
group: parts[5] || "",
});
continue;
}
if (parts.length === 1 && !trimmed.includes(":")) {
plain.push({
name: trimmed,
windows: 0,
attached: false,
created: 0,
activity: "",
group: "",
});
}
}
return plain;
}
module.exports = {
shQuote,
wrapLoginShell,
wrapShExec,
stripAnsi,
parseTmuxVersionString,
getListSessionsFormat,
TMUX_DETECT_SCRIPT,
parseDetectScriptOutput,
normalizeExecResult,
buildTmuxInvocation,
parseListOutput,
isTmuxDiagnosticLine,
TMUX_DIAGNOSTIC_LINE,
};