From a4e92c0aa44dadf9b99f3d2d4660f885ce2441c2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 09:48:57 +0100 Subject: [PATCH] chore(gateway): track watch tmux cwd --- AGENTS.md | 2 +- scripts/gateway-watch-tmux.mjs | 24 +++++++++++++++ src/infra/gateway-watch-tmux.test.ts | 46 ++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2038431c980..6bd25b9a29e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -174,7 +174,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work. - Before simulator/emulator testing, check real iOS/Android devices. - "restart iOS/Android apps" = rebuild/reinstall/relaunch, not kill/launch. - SwiftUI: Observation (`@Observable`, `@Bindable`) over new `ObservableObject`. -- Mac gateway: use app or `openclaw gateway restart/status --deep`; no ad-hoc tmux gateway. Logs: `./scripts/clawlog.sh`. +- Mac gateway: dev watch = `pnpm gateway:watch` (tmux `openclaw-gateway-watch-main`, auto-attach). Noninteractive: `OPENCLAW_GATEWAY_WATCH_ATTACH=0 pnpm gateway:watch`; attach/stop: `tmux attach -t openclaw-gateway-watch-main` / `tmux kill-session -t openclaw-gateway-watch-main`. Managed installs: `openclaw gateway restart/status --deep`. No launchd/ad-hoc tmux. Logs: `./scripts/clawlog.sh`. - Version bump touches: `package.json`, `apps/android/app/build.gradle.kts`, `apps/ios/version.json` + `pnpm ios:version:sync`, macOS `Info.plist`, `docs/install/updating.md`. Appcast only for Sparkle release. - Mobile LAN pairing: plaintext `ws://` loopback-only. Private-network `ws://` needs `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; Tailscale/public use `wss://` or tunnel. - A2UI hash `src/canvas-host/a2ui/.bundle.hash`: generated; ignore unless running `pnpm canvas:a2ui:bundle`; commit separately. diff --git a/scripts/gateway-watch-tmux.mjs b/scripts/gateway-watch-tmux.mjs index 24b6cc5703e..4ed17f2c310 100644 --- a/scripts/gateway-watch-tmux.mjs +++ b/scripts/gateway-watch-tmux.mjs @@ -8,6 +8,8 @@ const TMUX_ATTACH_DISABLE_VALUES = new Set(["0", "false", "no", "off"]); const TMUX_ATTACH_FORCE_VALUES = new Set(["1", "true", "yes", "on"]); const DEFAULT_PROFILE_NAME = "main"; const RAW_WATCH_SCRIPT = "scripts/watch-node.mjs"; +const TMUX_CWD_ENV_KEY = "OPENCLAW_GATEWAY_WATCH_CWD"; +const TMUX_CWD_OPTION_KEY = "@openclaw.gateway_watch.cwd"; const TMUX_CHILD_ENV_KEYS = [ "NODE_OPTIONS", "OPENCLAW_CONFIG_PATH", @@ -137,6 +139,20 @@ const attachTmux = ({ env, sessionName, spawnSyncImpl }) => { return runTmux(spawnSyncImpl, args, { stdio: "inherit" }); }; +const setTmuxSessionMetadata = ({ cwd, sessionName, spawnSyncImpl, stderr }) => { + const updates = [ + ["set-option", "-q", "-t", sessionName, TMUX_CWD_OPTION_KEY, cwd], + ["set-environment", "-t", sessionName, TMUX_CWD_ENV_KEY, cwd], + ]; + for (const args of updates) { + const result = runTmux(spawnSyncImpl, args); + if (result.error || result.status !== 0) { + log(stderr, `warning: failed to update tmux session metadata: ${getTmuxErrorText(result)}`); + return; + } + } +}; + export const runGatewayWatchTmuxMain = (params = {}) => { const deps = { args: params.args ?? process.argv.slice(2), @@ -219,6 +235,13 @@ export const runGatewayWatchTmuxMain = (params = {}) => { return result.status || 1; } + setTmuxSessionMetadata({ + cwd: deps.cwd, + sessionName, + spawnSyncImpl: deps.spawnSync, + stderr: deps.stderr, + }); + log(deps.stderr, `gateway:watch ${action} in tmux session ${sessionName}`); if ( shouldAttachTmux({ @@ -241,6 +264,7 @@ export const runGatewayWatchTmuxMain = (params = {}) => { return 0; } deps.stdout.write(`Attach: tmux attach -t ${sessionName}\n`); + deps.stdout.write(`Cwd: tmux show-options -v -t ${sessionName} ${TMUX_CWD_OPTION_KEY}\n`); deps.stdout.write("Restart: rerun the same pnpm gateway:watch command\n"); deps.stdout.write(`Stop: tmux kill-session -t ${sessionName}\n`); return 0; diff --git a/src/infra/gateway-watch-tmux.test.ts b/src/infra/gateway-watch-tmux.test.ts index ae9c3dc277d..356b99fee48 100644 --- a/src/infra/gateway-watch-tmux.test.ts +++ b/src/infra/gateway-watch-tmux.test.ts @@ -68,6 +68,8 @@ describe("gateway-watch tmux wrapper", () => { const spawnSync = vi .fn() .mockReturnValueOnce({ status: 1, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }); const code = runGatewayWatchTmuxMain({ @@ -101,10 +103,38 @@ describe("gateway-watch tmux wrapper", () => { ], expect.objectContaining({ encoding: "utf8" }), ); + expect(spawnSync).toHaveBeenNthCalledWith( + 3, + "tmux", + [ + "set-option", + "-q", + "-t", + "openclaw-gateway-watch-main", + "@openclaw.gateway_watch.cwd", + "/repo", + ], + expect.objectContaining({ encoding: "utf8" }), + ); + expect(spawnSync).toHaveBeenNthCalledWith( + 4, + "tmux", + [ + "set-environment", + "-t", + "openclaw-gateway-watch-main", + "OPENCLAW_GATEWAY_WATCH_CWD", + "/repo", + ], + expect.objectContaining({ encoding: "utf8" }), + ); expect(stderr.chunks.join("")).toContain( "gateway:watch started in tmux session openclaw-gateway-watch-main", ); expect(stdout.chunks.join("")).toContain("tmux attach -t openclaw-gateway-watch-main"); + expect(stdout.chunks.join("")).toContain( + "tmux show-options -v -t openclaw-gateway-watch-main @openclaw.gateway_watch.cwd", + ); }); it("auto-attaches in an interactive terminal after creating a session", () => { @@ -114,6 +144,8 @@ describe("gateway-watch tmux wrapper", () => { .fn() .mockReturnValueOnce({ status: 1, stdout: "", stderr: "" }) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }); const code = runGatewayWatchTmuxMain({ @@ -130,7 +162,7 @@ describe("gateway-watch tmux wrapper", () => { expect(code).toBe(0); expect(spawnSync).toHaveBeenNthCalledWith( - 3, + 5, "tmux", ["attach-session", "-t", "openclaw-gateway-watch-main"], expect.objectContaining({ stdio: "inherit" }), @@ -145,6 +177,8 @@ describe("gateway-watch tmux wrapper", () => { .fn() .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }); const code = runGatewayWatchTmuxMain({ @@ -161,7 +195,7 @@ describe("gateway-watch tmux wrapper", () => { expect(code).toBe(0); expect(spawnSync).toHaveBeenNthCalledWith( - 3, + 5, "tmux", ["switch-client", "-t", "openclaw-gateway-watch-main"], expect.objectContaining({ stdio: "inherit" }), @@ -174,6 +208,8 @@ describe("gateway-watch tmux wrapper", () => { const spawnSync = vi .fn() .mockReturnValueOnce({ status: 1, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }); const code = runGatewayWatchTmuxMain({ @@ -189,7 +225,7 @@ describe("gateway-watch tmux wrapper", () => { }); expect(code).toBe(0); - expect(spawnSync).toHaveBeenCalledTimes(2); + expect(spawnSync).toHaveBeenCalledTimes(4); expect(stdout.chunks.join("")).toContain("tmux attach -t openclaw-gateway-watch-main"); }); @@ -199,6 +235,8 @@ describe("gateway-watch tmux wrapper", () => { const spawnSync = vi .fn() .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }); const code = runGatewayWatchTmuxMain({ @@ -239,6 +277,8 @@ describe("gateway-watch tmux wrapper", () => { .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) .mockReturnValueOnce({ status: 1, stdout: "", stderr: "can't find window: 0" }) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" }); const code = runGatewayWatchTmuxMain({