chore(gateway): track watch tmux cwd

This commit is contained in:
Peter Steinberger
2026-04-29 09:48:57 +01:00
parent 8c8f396985
commit a4e92c0aa4
3 changed files with 68 additions and 4 deletions

View File

@@ -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.

View File

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

View File

@@ -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({