refactor: resolve the tmux socket from the instance config

The socket name was computed inside tmux-manager, which the TUI cannot import
just to learn which `-L` name its degraded-mode listing belongs on (that module
is the server's tmux driver, not a lookup table). The resolver moves next to
`dataPath()`, where the other half of the instance identity already lives, so
both processes agree by construction instead of by a copied default.

Behaviour is unchanged: the override still wins only when it is a name that can
be passed to `tmux -L` safely, and TmuxManager keeps warning about one that
cannot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-22 14:13:57 +02:00
parent 0566ea3453
commit 64c8048dda
3 changed files with 43 additions and 7 deletions
+15
View File
@@ -40,6 +40,21 @@ const INSTANCE_SUFFIX = CODEMAN_INSTANCE ? `-${CODEMAN_INSTANCE}` : '';
/** Default tmux socket for this instance. `CODEMAN_TMUX_SOCKET` still overrides. */ /** Default tmux socket for this instance. `CODEMAN_TMUX_SOCKET` still overrides. */
export const DEFAULT_TMUX_SOCKET = `codeman${INSTANCE_SUFFIX}`; export const DEFAULT_TMUX_SOCKET = `codeman${INSTANCE_SUFFIX}`;
/** Characters tmux accepts in a `-L` socket name. */
export const SAFE_TMUX_SOCKET_PATTERN = /^[a-zA-Z0-9_.-]+$/;
/**
* This instance's tmux socket: the `CODEMAN_TMUX_SOCKET` override when it is a
* safe name, else the instance default. Every process that runs `tmux -L` has
* to resolve it through here (the server via TmuxManager, the TUI for its
* degraded-mode listing), or a beta instance ends up driving prod's sessions.
*/
export function resolveTmuxSocketName(): string {
const raw = process.env.CODEMAN_TMUX_SOCKET;
if (raw !== undefined && SAFE_TMUX_SOCKET_PATTERN.test(raw)) return raw;
return DEFAULT_TMUX_SOCKET;
}
let _ensured = false; let _ensured = false;
/** /**
+8 -6
View File
@@ -31,7 +31,13 @@ import { existsSync, readFileSync, mkdirSync } from 'node:fs';
import { writeFile, rename } from 'node:fs/promises'; import { writeFile, rename } from 'node:fs/promises';
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { homedir } from 'node:os'; import { homedir } from 'node:os';
import { dataPath, DEFAULT_TMUX_SOCKET, CODEMAN_INSTANCE } from './config/instance.js'; import {
dataPath,
DEFAULT_TMUX_SOCKET,
CODEMAN_INSTANCE,
SAFE_TMUX_SOCKET_PATTERN,
resolveTmuxSocketName,
} from './config/instance.js';
import { import {
ProcessStats, ProcessStats,
PersistedRespawnConfig, PersistedRespawnConfig,
@@ -200,9 +206,6 @@ const SAFE_PANE_TARGET_PATTERN = /^(%\d+|\d+)$/;
* `codeman` for prod, `codeman-beta` on the beta branch). */ * `codeman` for prod, `codeman-beta` on the beta branch). */
const DEFAULT_CODEMAN_TMUX_SOCKET = DEFAULT_TMUX_SOCKET; const DEFAULT_CODEMAN_TMUX_SOCKET = DEFAULT_TMUX_SOCKET;
/** Regex to validate tmux socket names passed to `tmux -L`. */
const SAFE_TMUX_SOCKET_PATTERN = /^[a-zA-Z0-9_.-]+$/;
/** /**
* Separator used in `tmux list-panes -F` output between session name and pid. * Separator used in `tmux list-panes -F` output between session name and pid.
* *
@@ -597,9 +600,8 @@ function resolveConfiguredTmuxSocket(): string {
const raw = process.env.CODEMAN_TMUX_SOCKET ?? DEFAULT_CODEMAN_TMUX_SOCKET; const raw = process.env.CODEMAN_TMUX_SOCKET ?? DEFAULT_CODEMAN_TMUX_SOCKET;
if (!SAFE_TMUX_SOCKET_PATTERN.test(raw)) { if (!SAFE_TMUX_SOCKET_PATTERN.test(raw)) {
console.warn(`[TmuxManager] Ignoring invalid CODEMAN_TMUX_SOCKET: ${JSON.stringify(raw)}`); console.warn(`[TmuxManager] Ignoring invalid CODEMAN_TMUX_SOCKET: ${JSON.stringify(raw)}`);
return DEFAULT_CODEMAN_TMUX_SOCKET;
} }
return raw; return resolveTmuxSocketName();
} }
/** Build the `tmux -L <socket>` command prefix. Socket name is shell-escaped. */ /** Build the `tmux -L <socket>` command prefix. Socket name is shell-escaped. */
+20 -1
View File
@@ -18,7 +18,7 @@ vi.mock('node:fs', async (orig) => {
return { ...actual, mkdirSync: vi.fn() }; return { ...actual, mkdirSync: vi.fn() };
}); });
const ENV_KEYS = ['CODEMAN_INSTANCE', 'CODEMAN_DATA_DIR'] as const; const ENV_KEYS = ['CODEMAN_INSTANCE', 'CODEMAN_DATA_DIR', 'CODEMAN_TMUX_SOCKET'] as const;
const ORIG: Record<string, string | undefined> = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]])); const ORIG: Record<string, string | undefined> = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
async function load(env: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>> = {}) { async function load(env: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>> = {}) {
@@ -77,3 +77,22 @@ describe('config/instance', () => {
expect(m.DEFAULT_TMUX_SOCKET).toBe('codeman-beta'); expect(m.DEFAULT_TMUX_SOCKET).toBe('codeman-beta');
}); });
}); });
describe('resolveTmuxSocketName', () => {
it('is the instance socket when no override is set', async () => {
const m = await load({ CODEMAN_INSTANCE: 'beta', CODEMAN_TMUX_SOCKET: undefined });
expect(m.resolveTmuxSocketName()).toBe('codeman-beta');
});
it('honours a safe CODEMAN_TMUX_SOCKET override', async () => {
const m = await load({ CODEMAN_INSTANCE: undefined, CODEMAN_TMUX_SOCKET: 'codeman-test.1' });
expect(m.resolveTmuxSocketName()).toBe('codeman-test.1');
});
it('ignores an override that could not be passed to `tmux -L` safely', async () => {
// A socket name reaches a command line, so anything outside the pattern
// falls back to the instance default rather than being escaped.
const m = await load({ CODEMAN_INSTANCE: undefined, CODEMAN_TMUX_SOCKET: 'bad; rm -rf /' });
expect(m.resolveTmuxSocketName()).toBe('codeman');
});
});