Browse Source

refactor(multiplexer): extract shared infrastructure from tmux/zellij/herdr adapters

Extract quoteShellArg, buildOpencodeAttachCommand, and findBinary into
src/multiplexer/shared.ts. All three adapters now import from shared
instead of maintaining inline copies.

- shared.ts: 65 lines of shared infrastructure
- tmux: removed inline quoteShellArg, buildOpencodeAttachCommand, findBinary (-60 lines)
- zellij: removed inline quoteShellArg, buildOpencodeAttachCommand, findBinary (-36 lines)
- herdr: removed inline buildOpencodeAttachCommand, findBinary (-51 lines)
- findBinary simplified: removed -V verification step, derives logPrefix internally

Closes #676
Michael Henke 1 month ago
parent
commit
f43a3c97e1

+ 2 - 51
src/multiplexer/herdr/index.ts

@@ -15,6 +15,7 @@
 import type { MultiplexerLayout } from '../../config/schema';
 import { crossSpawn } from '../../utils/compat';
 import { log } from '../../utils/logger';
+import { buildOpencodeAttachCommand, findBinary } from '../shared';
 import type { Multiplexer, PaneResult } from '../types';
 
 type HerdrPaneDirection = 'right' | 'down';
@@ -47,7 +48,7 @@ export class HerdrMultiplexer implements Multiplexer {
       return this.binaryPath !== null;
     }
 
-    this.binaryPath = await this.findBinary();
+    this.binaryPath = await findBinary('herdr');
     this.hasChecked = true;
     return this.binaryPath !== null;
   }
@@ -215,36 +216,6 @@ export class HerdrMultiplexer implements Multiplexer {
     await this.isAvailable();
     return this.binaryPath;
   }
-
-  private async findBinary(): Promise<string | null> {
-    const cmd = process.platform === 'win32' ? 'where' : 'which';
-
-    try {
-      const proc = crossSpawn([cmd, 'herdr'], {
-        stdout: 'pipe',
-        stderr: 'pipe',
-      });
-
-      const exitCode = await proc.exited;
-      if (exitCode !== 0) {
-        log("[herdr] findBinary: 'which herdr' failed", { exitCode });
-        return null;
-      }
-
-      const stdout = await proc.stdout();
-      const path = stdout.trim().split('\n')[0];
-      if (!path) {
-        log('[herdr] findBinary: no path in output');
-        return null;
-      }
-
-      log('[herdr] findBinary: found', { path });
-      return path;
-    } catch (err) {
-      log('[herdr] findBinary: exception', { error: String(err) });
-      return null;
-    }
-  }
 }
 
 /**
@@ -284,23 +255,3 @@ function getPaneDirection(layout: MultiplexerLayout): HerdrPaneDirection {
       return 'right';
   }
 }
-
-function buildOpencodeAttachCommand(
-  sessionId: string,
-  serverUrl: string,
-  directory: string,
-): string {
-  return [
-    'opencode',
-    'attach',
-    quoteShellArg(serverUrl),
-    '--session',
-    quoteShellArg(sessionId),
-    '--dir',
-    quoteShellArg(directory),
-  ].join(' ');
-}
-
-function quoteShellArg(value: string): string {
-  return `'${value.replace(/'/g, `'\\''`)}'`;
-}

+ 63 - 0
src/multiplexer/shared.ts

@@ -0,0 +1,63 @@
+/**
+ * Shared multiplexer infrastructure
+ *
+ * Functions used across tmux, zellij, and herdr backend adapters.
+ * Extracted to eliminate copy-paste duplication and prevent drift.
+ */
+
+import { crossSpawn } from '../utils/compat';
+import { log } from '../utils/logger';
+
+export function quoteShellArg(value: string): string {
+  return `'${value.replace(/'/g, `'\\''`)}'`;
+}
+
+export function buildOpencodeAttachCommand(
+  sessionId: string,
+  serverUrl: string,
+  directory: string,
+): string {
+  return [
+    'opencode',
+    'attach',
+    quoteShellArg(serverUrl),
+    '--session',
+    quoteShellArg(sessionId),
+    '--dir',
+    quoteShellArg(directory),
+  ].join(' ');
+}
+
+export async function findBinary(binaryName: string): Promise<string | null> {
+  const isWindows = process.platform === 'win32';
+  const cmd = isWindows ? 'where' : 'which';
+  const logPrefix = `[${binaryName}]`;
+
+  try {
+    const proc = crossSpawn([cmd, binaryName], {
+      stdout: 'pipe',
+      stderr: 'pipe',
+    });
+
+    const exitCode = await proc.exited;
+    if (exitCode !== 0) {
+      log(`${logPrefix} findBinary: 'which ${binaryName}' failed`, {
+        exitCode,
+      });
+      return null;
+    }
+
+    const stdout = await proc.stdout();
+    const path = stdout.trim().split('\n')[0];
+    if (!path) {
+      log(`${logPrefix} findBinary: no path in output`);
+      return null;
+    }
+
+    log(`${logPrefix} findBinary: found`, { path });
+    return path;
+  } catch (err) {
+    log(`${logPrefix} findBinary: exception`, { error: String(err) });
+    return null;
+  }
+}

+ 7 - 60
src/multiplexer/tmux/index.ts

@@ -5,6 +5,7 @@
 import type { MultiplexerLayout } from '../../config/schema';
 import { crossSpawn } from '../../utils/compat';
 import { log } from '../../utils/logger';
+import { buildOpencodeAttachCommand, findBinary } from '../shared';
 import type { Multiplexer, PaneResult } from '../types';
 
 const TMUX_LAYOUT_DEBOUNCE_MS = 150;
@@ -30,7 +31,7 @@ export class TmuxMultiplexer implements Multiplexer {
       return this.binaryPath !== null;
     }
 
-    this.binaryPath = await this.findBinary();
+    this.binaryPath = await findBinary('tmux');
     this.hasChecked = true;
     return this.binaryPath !== null;
   }
@@ -53,19 +54,11 @@ export class TmuxMultiplexer implements Multiplexer {
 
     try {
       // Build the attach command
-      const quotedDirectory = quoteShellArg(directory);
-      const quotedUrl = quoteShellArg(serverUrl);
-      const quotedSessionId = quoteShellArg(sessionId);
-
-      const opencodeCmd = [
-        'opencode',
-        'attach',
-        quotedUrl,
-        '--session',
-        quotedSessionId,
-        '--dir',
-        quotedDirectory,
-      ].join(' ');
+      const opencodeCmd = buildOpencodeAttachCommand(
+        sessionId,
+        serverUrl,
+        directory,
+      );
 
       // tmux split-window -h -d -P -F '#{pane_id}' <cmd>
       const args = [
@@ -275,50 +268,4 @@ export class TmuxMultiplexer implements Multiplexer {
   private targetArgs(): string[] {
     return this.targetPane ? ['-t', this.targetPane] : [];
   }
-
-  private async findBinary(): Promise<string | null> {
-    const isWindows = process.platform === 'win32';
-    const cmd = isWindows ? 'where' : 'which';
-
-    try {
-      const proc = crossSpawn([cmd, 'tmux'], {
-        stdout: 'pipe',
-        stderr: 'pipe',
-      });
-
-      const exitCode = await proc.exited;
-      if (exitCode !== 0) {
-        log("[tmux] findBinary: 'which tmux' failed", { exitCode });
-        return null;
-      }
-
-      const stdout = await proc.stdout();
-      const path = stdout.trim().split('\n')[0];
-      if (!path) {
-        log('[tmux] findBinary: no path in output');
-        return null;
-      }
-
-      // Verify it works
-      const verifyProc = crossSpawn([path, '-V'], {
-        stdout: 'pipe',
-        stderr: 'pipe',
-      });
-      const verifyExit = await verifyProc.exited;
-      if (verifyExit !== 0) {
-        log('[tmux] findBinary: tmux -V failed', { path, verifyExit });
-        return null;
-      }
-
-      log('[tmux] findBinary: found', { path });
-      return path;
-    } catch (err) {
-      log('[tmux] findBinary: exception', { error: String(err) });
-      return null;
-    }
-  }
-}
-
-function quoteShellArg(value: string): string {
-  return `'${value.replace(/'/g, `'\\''`)}'`;
 }

+ 6 - 36
src/multiplexer/zellij/index.ts

@@ -14,6 +14,11 @@
 
 import type { MultiplexerLayout, ZellijPaneMode } from '../../config/schema';
 import { crossSpawn } from '../../utils/compat';
+import {
+  buildOpencodeAttachCommand,
+  findBinary,
+  quoteShellArg,
+} from '../shared';
 import type { Multiplexer, PaneResult } from '../types';
 
 interface ZellijTabInfo {
@@ -58,7 +63,7 @@ export class ZellijMultiplexer implements Multiplexer {
     if (this.hasChecked) {
       return this.binaryPath !== null;
     }
-    this.binaryPath = await this.findBinary();
+    this.binaryPath = await findBinary('zellij');
     this.hasChecked = true;
     return this.binaryPath !== null;
   }
@@ -584,21 +589,6 @@ export class ZellijMultiplexer implements Multiplexer {
     await this.isAvailable();
     return this.binaryPath;
   }
-
-  private async findBinary(): Promise<string | null> {
-    const cmd = process.platform === 'win32' ? 'where' : 'which';
-    try {
-      const proc = crossSpawn([cmd, 'zellij'], {
-        stdout: 'pipe',
-        stderr: 'pipe',
-      });
-      if ((await proc.exited) !== 0) return null;
-      const stdout = await proc.stdout();
-      return stdout.trim().split('\n')[0] || null;
-    } catch {
-      return null;
-    }
-  }
 }
 
 function normalizePaneId(paneId: string): string {
@@ -620,26 +610,6 @@ function getPaneDirection(
   }
 }
 
-function buildOpencodeAttachCommand(
-  sessionId: string,
-  serverUrl: string,
-  directory: string,
-): string {
-  return [
-    'opencode',
-    'attach',
-    quoteShellArg(serverUrl),
-    '--session',
-    quoteShellArg(sessionId),
-    '--dir',
-    quoteShellArg(directory),
-  ].join(' ');
-}
-
 function buildShellLaunchCommand(command: string): string {
   return ['sh', '-lc', quoteShellArg(command)].join(' ');
 }
-
-function quoteShellArg(value: string): string {
-  return `'${value.replace(/'/g, `'\\''`)}'`;
-}