






















@@ -0,0 +1,251 @@
1+#!/usr/bin/env node
2+import { spawnSync } from "node:child_process";
3+import process from "node:process";
4+import { pathToFileURL } from "node:url";
5+6+const TMUX_DISABLE_VALUES = new Set(["0", "false", "no", "off"]);
7+const TMUX_ATTACH_DISABLE_VALUES = new Set(["0", "false", "no", "off"]);
8+const TMUX_ATTACH_FORCE_VALUES = new Set(["1", "true", "yes", "on"]);
9+const DEFAULT_PROFILE_NAME = "main";
10+const RAW_WATCH_SCRIPT = "scripts/watch-node.mjs";
11+const TMUX_CHILD_ENV_KEYS = [
12+"NODE_OPTIONS",
13+"OPENCLAW_CONFIG_PATH",
14+"OPENCLAW_GATEWAY_PORT",
15+"OPENCLAW_HOME",
16+"OPENCLAW_PROFILE",
17+"OPENCLAW_SKIP_CHANNELS",
18+"OPENCLAW_STATE_DIR",
19+];
20+21+const sanitizeSessionPart = (value) => {
22+const normalized = String(value ?? "")
23+.trim()
24+.toLowerCase()
25+.replace(/[^a-z0-9_.-]+/g, "-")
26+.replace(/^-+|-+$/g, "");
27+return normalized || DEFAULT_PROFILE_NAME;
28+};
29+30+const shellQuote = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
31+32+const readArgValue = (args, flag) => {
33+const prefix = `${flag}=`;
34+for (let index = 0; index < args.length; index += 1) {
35+const arg = args[index];
36+if (arg === flag) {
37+const next = args[index + 1];
38+return typeof next === "string" && !next.startsWith("-") ? next : null;
39+}
40+if (typeof arg === "string" && arg.startsWith(prefix)) {
41+return arg.slice(prefix.length);
42+}
43+}
44+return null;
45+};
46+47+export const resolveGatewayWatchTmuxSessionName = ({ args = [], env = process.env } = {}) => {
48+const profile =
49+env.OPENCLAW_PROFILE ||
50+readArgValue(args, "--profile") ||
51+(args.includes("--dev") ? "dev" : null);
52+const port = env.OPENCLAW_GATEWAY_PORT || readArgValue(args, "--port");
53+const parts = [
54+"openclaw",
55+"gateway",
56+"watch",
57+sanitizeSessionPart(profile ?? DEFAULT_PROFILE_NAME),
58+];
59+if (port && port !== "18789") {
60+parts.push(sanitizeSessionPart(port));
61+}
62+return parts.join("-");
63+};
64+65+const resolveShell = (env) => env.SHELL || "/bin/sh";
66+67+export const buildGatewayWatchTmuxCommand = ({
68+ args = [],
69+ cwd = process.cwd(),
70+ env = process.env,
71+ nodePath = process.execPath,
72+ sessionName,
73+} = {}) => {
74+const shell = resolveShell(env);
75+const childEnv = [
76+"env",
77+`OPENCLAW_GATEWAY_WATCH_TMUX_CHILD=1`,
78+`OPENCLAW_GATEWAY_WATCH_SESSION=${sessionName}`,
79+ ...TMUX_CHILD_ENV_KEYS.flatMap((key) =>
80+env[key] == null || env[key] === "" ? [] : [`${key}=${env[key]}`],
81+),
82+];
83+const watchCommand = [
84+"cd",
85+shellQuote(cwd),
86+"&&",
87+"exec",
88+ ...childEnv.map(shellQuote),
89+shellQuote(nodePath),
90+shellQuote(RAW_WATCH_SCRIPT),
91+ ...args.map(shellQuote),
92+].join(" ");
93+return `exec ${shellQuote(shell)} -lc ${shellQuote(watchCommand)}`;
94+};
95+96+const runForegroundWatcher = ({ args, cwd, env, nodePath, spawnSyncImpl, stdio = "inherit" }) => {
97+const result = spawnSyncImpl(nodePath, [RAW_WATCH_SCRIPT, ...args], {
98+ cwd,
99+ env,
100+ stdio,
101+});
102+return result.status ?? (result.signal ? 1 : 0);
103+};
104+105+const runTmux = (spawnSyncImpl, args, options = {}) =>
106+spawnSyncImpl("tmux", args, {
107+encoding: "utf8",
108+stdio: ["ignore", "pipe", "pipe"],
109+ ...options,
110+});
111+112+const log = (stderr, message) => {
113+stderr.write(`[openclaw] ${message}\n`);
114+};
115+116+const getTmuxErrorText = (result) =>
117+result.error?.message || String(result.stderr || "").trim() || "unknown error";
118+119+const isMissingTmuxTarget = (result) =>
120+/can't find (?:session|window|pane)|no current target/i.test(getTmuxErrorText(result));
121+122+const shouldAttachTmux = ({ env, stdinIsTTY, stdoutIsTTY }) => {
123+const raw = String(env.OPENCLAW_GATEWAY_WATCH_ATTACH ?? "").toLowerCase();
124+if (TMUX_ATTACH_FORCE_VALUES.has(raw)) {
125+return true;
126+}
127+if (TMUX_ATTACH_DISABLE_VALUES.has(raw)) {
128+return false;
129+}
130+return !env.CI && stdinIsTTY === true && stdoutIsTTY === true;
131+};
132+133+const attachTmux = ({ env, sessionName, spawnSyncImpl }) => {
134+const args = env.TMUX
135+ ? ["switch-client", "-t", sessionName]
136+ : ["attach-session", "-t", sessionName];
137+return runTmux(spawnSyncImpl, args, { stdio: "inherit" });
138+};
139+140+export const runGatewayWatchTmuxMain = (params = {}) => {
141+const deps = {
142+args: params.args ?? process.argv.slice(2),
143+cwd: params.cwd ?? process.cwd(),
144+env: params.env ? { ...params.env } : { ...process.env },
145+nodePath: params.nodePath ?? process.execPath,
146+spawnSync: params.spawnSync ?? spawnSync,
147+stderr: params.stderr ?? process.stderr,
148+stdinIsTTY: params.stdinIsTTY ?? process.stdin.isTTY,
149+stdout: params.stdout ?? process.stdout,
150+stdoutIsTTY: params.stdoutIsTTY ?? process.stdout.isTTY,
151+};
152+153+if (TMUX_DISABLE_VALUES.has(String(deps.env.OPENCLAW_GATEWAY_WATCH_TMUX ?? "").toLowerCase())) {
154+return runForegroundWatcher({
155+args: deps.args,
156+cwd: deps.cwd,
157+env: deps.env,
158+nodePath: deps.nodePath,
159+spawnSyncImpl: deps.spawnSync,
160+});
161+}
162+163+if (deps.env.OPENCLAW_GATEWAY_WATCH_TMUX_CHILD === "1") {
164+return runForegroundWatcher({
165+args: deps.args,
166+cwd: deps.cwd,
167+env: deps.env,
168+nodePath: deps.nodePath,
169+spawnSyncImpl: deps.spawnSync,
170+});
171+}
172+173+const sessionName =
174+params.sessionName ?? resolveGatewayWatchTmuxSessionName({ args: deps.args, env: deps.env });
175+const command = buildGatewayWatchTmuxCommand({
176+args: deps.args,
177+cwd: deps.cwd,
178+env: deps.env,
179+nodePath: deps.nodePath,
180+ sessionName,
181+});
182+183+const hasSession = runTmux(deps.spawnSync, ["has-session", "-t", sessionName]);
184+if (hasSession.error?.code === "ENOENT") {
185+log(
186+deps.stderr,
187+"tmux is not installed or not on PATH; run `pnpm gateway:watch:raw` for foreground watch mode.",
188+);
189+return 1;
190+}
191+if (hasSession.error) {
192+log(deps.stderr, `failed to query tmux session ${sessionName}: ${hasSession.error.message}`);
193+return 1;
194+}
195+196+const startSession = () =>
197+runTmux(deps.spawnSync, ["new-session", "-d", "-s", sessionName, "-c", deps.cwd, command]);
198+const restartSession = () =>
199+runTmux(deps.spawnSync, ["respawn-pane", "-k", "-t", sessionName, "-c", deps.cwd, command]);
200+const action = hasSession.status === 0 ? "restarted" : "started";
201+let result = hasSession.status === 0 ? restartSession() : startSession();
202+if (hasSession.status === 0 && isMissingTmuxTarget(result)) {
203+runTmux(deps.spawnSync, ["kill-session", "-t", sessionName]);
204+result = startSession();
205+}
206+if (result.error?.code === "ENOENT") {
207+log(
208+deps.stderr,
209+"tmux is not installed or not on PATH; run `pnpm gateway:watch:raw` for foreground watch mode.",
210+);
211+return 1;
212+}
213+if (result.error || result.status !== 0) {
214+const detail = getTmuxErrorText(result);
215+log(
216+deps.stderr,
217+`failed to ${action === "started" ? "start" : "restart"} tmux session ${sessionName}: ${detail}`,
218+);
219+return result.status || 1;
220+}
221+222+log(deps.stderr, `gateway:watch ${action} in tmux session ${sessionName}`);
223+if (
224+shouldAttachTmux({
225+env: deps.env,
226+stdinIsTTY: deps.stdinIsTTY,
227+stdoutIsTTY: deps.stdoutIsTTY,
228+})
229+) {
230+const attachResult = attachTmux({
231+env: deps.env,
232+ sessionName,
233+spawnSyncImpl: deps.spawnSync,
234+});
235+if (attachResult.error || attachResult.status !== 0) {
236+const detail =
237+attachResult.error?.message || String(attachResult.stderr || "").trim() || "unknown error";
238+log(deps.stderr, `failed to attach tmux session ${sessionName}: ${detail}`);
239+return attachResult.status || 1;
240+}
241+return 0;
242+}
243+deps.stdout.write(`Attach: tmux attach -t ${sessionName}\n`);
244+deps.stdout.write("Restart: rerun the same pnpm gateway:watch command\n");
245+deps.stdout.write(`Stop: tmux kill-session -t ${sessionName}\n`);
246+return 0;
247+};
248+249+if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
250+process.exit(runGatewayWatchTmuxMain());
251+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。