mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 15:39:41 +02:00
chore: project setup and tooling cleanup (#26)
* add project setup * add tools to eslint * remove contributing.md * update claude md * chore: simplify CI to single Node.js version (22) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * use node 20 * adjust formatting * fix linting * format * fix layout * fix: align CI node version to .nvmrc (22), remove redundant gotcha Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: arkon <arkon.85@hotmail.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
arkon
parent
2b2829ebde
commit
480584de63
+215
-151
@@ -30,10 +30,25 @@ import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
||||
import { writeFile, rename } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { ProcessStats, PersistedRespawnConfig, getErrorMessage, DEFAULT_NICE_CONFIG, type PaneInfo, type ClaudeMode, type SessionMode, type OpenCodeConfig } from './types.js';
|
||||
import {
|
||||
ProcessStats,
|
||||
PersistedRespawnConfig,
|
||||
getErrorMessage,
|
||||
DEFAULT_NICE_CONFIG,
|
||||
type PaneInfo,
|
||||
type ClaudeMode,
|
||||
type SessionMode,
|
||||
type OpenCodeConfig,
|
||||
} from './types.js';
|
||||
import { wrapWithNice } from './utils/nice-wrapper.js';
|
||||
import { SAFE_PATH_PATTERN } from './utils/regex-patterns.js';
|
||||
import type { TerminalMultiplexer, MuxSession, MuxSessionWithStats, CreateSessionOptions, RespawnPaneOptions } from './mux-interface.js';
|
||||
import type {
|
||||
TerminalMultiplexer,
|
||||
MuxSession,
|
||||
MuxSessionWithStats,
|
||||
CreateSessionOptions,
|
||||
RespawnPaneOptions,
|
||||
} from './mux-interface.js';
|
||||
|
||||
// Claude CLI PATH resolution — shared utility
|
||||
import { findClaudeDir } from './utils/claude-cli-resolver.js';
|
||||
@@ -103,11 +118,23 @@ function isValidMuxName(name: string): boolean {
|
||||
* Prevents command injection via malformed paths.
|
||||
*/
|
||||
function isValidPath(path: string): boolean {
|
||||
if (path.includes(';') || path.includes('&') || path.includes('|') ||
|
||||
path.includes('$') || path.includes('`') || path.includes('(') ||
|
||||
path.includes(')') || path.includes('{') || path.includes('}') ||
|
||||
path.includes('<') || path.includes('>') || path.includes("'") ||
|
||||
path.includes('"') || path.includes('\n') || path.includes('\r')) {
|
||||
if (
|
||||
path.includes(';') ||
|
||||
path.includes('&') ||
|
||||
path.includes('|') ||
|
||||
path.includes('$') ||
|
||||
path.includes('`') ||
|
||||
path.includes('(') ||
|
||||
path.includes(')') ||
|
||||
path.includes('{') ||
|
||||
path.includes('}') ||
|
||||
path.includes('<') ||
|
||||
path.includes('>') ||
|
||||
path.includes("'") ||
|
||||
path.includes('"') ||
|
||||
path.includes('\n') ||
|
||||
path.includes('\r')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (path.includes('..')) {
|
||||
@@ -177,7 +204,7 @@ function buildSpawnCommand(options: {
|
||||
}): string {
|
||||
if (options.mode === 'claude') {
|
||||
// Validate model to prevent command injection
|
||||
const safeModel = (options.model && /^[a-zA-Z0-9._-]+$/.test(options.model)) ? options.model : undefined;
|
||||
const safeModel = options.model && /^[a-zA-Z0-9._-]+$/.test(options.model) ? options.model : undefined;
|
||||
const modelFlag = safeModel ? ` --model ${safeModel}` : '';
|
||||
return `claude${buildClaudePermissionFlags(options.claudeMode, options.allowedTools)} --session-id "${options.sessionId}"${modelFlag}`;
|
||||
}
|
||||
@@ -204,7 +231,9 @@ function setOpenCodeEnvVars(muxName: string): void {
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
} catch { /* Non-critical — key may not be needed */ }
|
||||
} catch {
|
||||
/* Non-critical — key may not be needed */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,7 +254,9 @@ function setOpenCodeConfigContent(muxName: string, config?: OpenCodeConfig): voi
|
||||
const existing = JSON.parse(config.configContent) as Record<string, unknown>;
|
||||
Object.assign(permConfig, existing);
|
||||
permConfig.permission = { '*': 'allow' };
|
||||
} catch { /* invalid JSON, use default permConfig */ }
|
||||
} catch {
|
||||
/* invalid JSON, use default permConfig */
|
||||
}
|
||||
}
|
||||
jsonContent = JSON.stringify(permConfig);
|
||||
} else if (config.configContent) {
|
||||
@@ -247,7 +278,9 @@ function setOpenCodeConfigContent(muxName: string, config?: OpenCodeConfig): voi
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
} catch { /* Non-critical */ }
|
||||
} catch {
|
||||
/* Non-critical */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +427,14 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
if (mode === 'claude') envExports.splice(2, 0, 'unset CLAUDECODE');
|
||||
const envExportsStr = envExports.join(' && ');
|
||||
|
||||
const baseCmd = buildSpawnCommand({ mode, sessionId, model, claudeMode, allowedTools, openCodeConfig });
|
||||
const baseCmd = buildSpawnCommand({
|
||||
mode,
|
||||
sessionId,
|
||||
model,
|
||||
claudeMode,
|
||||
allowedTools,
|
||||
openCodeConfig,
|
||||
});
|
||||
|
||||
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
||||
const cmd = wrapWithNice(baseCmd, config);
|
||||
@@ -412,15 +452,22 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
// (Production uses systemd which has a clean env, but dev/test may be nested.)
|
||||
const cleanEnv = { ...process.env };
|
||||
delete cleanEnv.TMUX;
|
||||
execSync(
|
||||
`tmux new-session -ds "${muxName}" -c "${workingDir}" -x 120 -y 40`,
|
||||
{ cwd: workingDir, timeout: EXEC_TIMEOUT_MS, stdio: 'ignore', env: cleanEnv }
|
||||
);
|
||||
execSync(`tmux new-session -ds "${muxName}" -c "${workingDir}" -x 120 -y 40`, {
|
||||
cwd: workingDir,
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
stdio: 'ignore',
|
||||
env: cleanEnv,
|
||||
});
|
||||
|
||||
// Set remain-on-exit now that the server is running — must be before respawn-pane
|
||||
try {
|
||||
execSync(`tmux set-option -t "${muxName}" remain-on-exit on`, { timeout: EXEC_TIMEOUT_MS, stdio: 'ignore' });
|
||||
} catch { /* Non-critical */ }
|
||||
execSync(`tmux set-option -t "${muxName}" remain-on-exit on`, {
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
} catch {
|
||||
/* Non-critical */
|
||||
}
|
||||
|
||||
// For OpenCode: set sensitive env vars and config via tmux setenv
|
||||
// (not visible in ps output or tmux history, inherited by panes)
|
||||
@@ -430,13 +477,13 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
}
|
||||
|
||||
// Replace the shell with the actual command (no echo in terminal)
|
||||
execSync(
|
||||
`tmux respawn-pane -k -t "${muxName}" bash -c ${JSON.stringify(fullCmd)}`,
|
||||
{ timeout: EXEC_TIMEOUT_MS, stdio: 'ignore' }
|
||||
);
|
||||
execSync(`tmux respawn-pane -k -t "${muxName}" bash -c ${JSON.stringify(fullCmd)}`, {
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
// Wait for tmux session to be queryable
|
||||
await new Promise(resolve => setTimeout(resolve, TMUX_CREATION_WAIT_MS));
|
||||
await new Promise((resolve) => setTimeout(resolve, TMUX_CREATION_WAIT_MS));
|
||||
|
||||
// Non-critical tmux config — run in parallel to avoid blocking event loop.
|
||||
// These configure UX niceties (no status bar, true color).
|
||||
@@ -445,18 +492,28 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
const configPromises: Promise<void>[] = [
|
||||
// Disable tmux status bar — Codeman's web UI provides session info
|
||||
execAsync(`tmux set-option -t "${muxName}" status off`, { timeout: EXEC_TIMEOUT_MS })
|
||||
.then(() => {}).catch(() => { /* Non-critical — session still works with status bar */ }),
|
||||
.then(() => {})
|
||||
.catch(() => {
|
||||
/* Non-critical — session still works with status bar */
|
||||
}),
|
||||
// Override global remain-on-exit with session-level setting
|
||||
execAsync(`tmux set-option -t "${muxName}" remain-on-exit on`, { timeout: EXEC_TIMEOUT_MS })
|
||||
.then(() => {}).catch(() => { /* Already set globally as fallback */ }),
|
||||
.then(() => {})
|
||||
.catch(() => {
|
||||
/* Already set globally as fallback */
|
||||
}),
|
||||
];
|
||||
|
||||
// Enable 24-bit true color passthrough — server-wide, set once per lifetime
|
||||
if (!this.trueColorConfigured) {
|
||||
configPromises.push(
|
||||
execAsync(`tmux set-option -sa terminal-overrides ",*:Tc"`, { timeout: EXEC_TIMEOUT_MS })
|
||||
.then(() => { this.trueColorConfigured = true; })
|
||||
.catch(() => { /* Non-critical — colors limited to 256 */ })
|
||||
.then(() => {
|
||||
this.trueColorConfigured = true;
|
||||
})
|
||||
.catch(() => {
|
||||
/* Non-critical — colors limited to 256 */
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -467,7 +524,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
// Get the PID of the pane process (retry for tmux server cold-start)
|
||||
let pid = this.getPanePid(muxName);
|
||||
for (let i = 0; !pid && i < GET_PID_MAX_RETRIES; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, GET_PID_RETRY_MS));
|
||||
await new Promise((resolve) => setTimeout(resolve, GET_PID_RETRY_MS));
|
||||
pid = this.getPanePid(muxName);
|
||||
}
|
||||
if (!pid) {
|
||||
@@ -507,10 +564,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
}
|
||||
|
||||
try {
|
||||
const output = execSync(
|
||||
`tmux display-message -t "${muxName}" -p '#{pane_pid}'`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
).trim();
|
||||
const output = execSync(`tmux display-message -t "${muxName}" -p '#{pane_pid}'`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
}).trim();
|
||||
const pid = parseInt(output, 10);
|
||||
return Number.isNaN(pid) ? null : pid;
|
||||
} catch {
|
||||
@@ -533,10 +590,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
if (IS_TEST_MODE) return false;
|
||||
if (!isValidMuxName(muxName)) return false;
|
||||
try {
|
||||
const output = execSync(
|
||||
`tmux display-message -t "${muxName}" -p '#{pane_dead}'`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
).trim();
|
||||
const output = execSync(`tmux display-message -t "${muxName}" -p '#{pane_dead}'`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
}).trim();
|
||||
return output === '1';
|
||||
} catch {
|
||||
return false;
|
||||
@@ -578,7 +635,14 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
if (mode === 'claude') envExports.splice(2, 0, 'unset CLAUDECODE');
|
||||
const envExportsStr = envExports.join(' && ');
|
||||
|
||||
const baseCmd = buildSpawnCommand({ mode, sessionId, model, claudeMode, allowedTools, openCodeConfig });
|
||||
const baseCmd = buildSpawnCommand({
|
||||
mode,
|
||||
sessionId,
|
||||
model,
|
||||
claudeMode,
|
||||
allowedTools,
|
||||
openCodeConfig,
|
||||
});
|
||||
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
||||
const cmd = wrapWithNice(baseCmd, config);
|
||||
const fullCmd = `${pathExport}${envExportsStr} && ${cmd}`;
|
||||
@@ -590,12 +654,11 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
setOpenCodeConfigContent(muxName, openCodeConfig);
|
||||
}
|
||||
|
||||
await execAsync(
|
||||
`tmux respawn-pane -k -t "${muxName}" bash -c ${JSON.stringify(fullCmd)}`,
|
||||
{ timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
await execAsync(`tmux respawn-pane -k -t "${muxName}" bash -c ${JSON.stringify(fullCmd)}`, {
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
// Wait for the respawned process to start
|
||||
await new Promise(resolve => setTimeout(resolve, TMUX_CREATION_WAIT_MS));
|
||||
await new Promise((resolve) => setTimeout(resolve, TMUX_CREATION_WAIT_MS));
|
||||
const pid = this.getPanePid(muxName);
|
||||
if (pid) session.pid = pid;
|
||||
return pid;
|
||||
@@ -628,7 +691,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
}).trim();
|
||||
if (output) {
|
||||
for (const childPid of output.split('\n').map(p => parseInt(p, 10)).filter(p => !Number.isNaN(p))) {
|
||||
for (const childPid of output
|
||||
.split('\n')
|
||||
.map((p) => parseInt(p, 10))
|
||||
.filter((p) => !Number.isNaN(p))) {
|
||||
pids.push(childPid);
|
||||
pids.push(...this.getChildPids(childPid));
|
||||
}
|
||||
@@ -655,14 +721,14 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
const checkInterval = 100;
|
||||
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
const aliveCount = pids.filter(pid => this.isProcessAlive(pid)).length;
|
||||
const aliveCount = pids.filter((pid) => this.isProcessAlive(pid)).length;
|
||||
if (aliveCount === 0) {
|
||||
return true;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
||||
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
||||
}
|
||||
|
||||
const stillAlive = pids.filter(pid => this.isProcessAlive(pid));
|
||||
const stillAlive = pids.filter((pid) => this.isProcessAlive(pid));
|
||||
if (stillAlive.length > 0) {
|
||||
console.warn(`[TmuxManager] ${stillAlive.length} processes still alive after kill: ${stillAlive.join(', ')}`);
|
||||
}
|
||||
@@ -717,7 +783,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, TMUX_KILL_WAIT_MS));
|
||||
await new Promise((resolve) => setTimeout(resolve, TMUX_KILL_WAIT_MS));
|
||||
|
||||
childPids = this.getChildPids(currentPid);
|
||||
for (const childPid of childPids) {
|
||||
@@ -735,7 +801,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
if (this.isProcessAlive(currentPid)) {
|
||||
try {
|
||||
process.kill(-currentPid, 'SIGTERM');
|
||||
await new Promise(resolve => setTimeout(resolve, GRACEFUL_SHUTDOWN_WAIT_MS));
|
||||
await new Promise((resolve) => setTimeout(resolve, GRACEFUL_SHUTDOWN_WAIT_MS));
|
||||
if (this.isProcessAlive(currentPid)) {
|
||||
process.kill(-currentPid, 'SIGKILL');
|
||||
}
|
||||
@@ -829,10 +895,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
|
||||
// Discover unknown codeman sessions
|
||||
try {
|
||||
const output = execSync(
|
||||
"tmux list-sessions -F '#{session_name}' 2>/dev/null || true",
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
).trim();
|
||||
const output = execSync("tmux list-sessions -F '#{session_name}' 2>/dev/null || true", {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
}).trim();
|
||||
|
||||
for (const line of output.split('\n')) {
|
||||
const sessionName = line.trim();
|
||||
@@ -890,26 +956,26 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
}
|
||||
|
||||
try {
|
||||
const psOutput = execSync(
|
||||
`ps -o rss=,pcpu= -p ${session.pid} 2>/dev/null || echo "0 0"`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
).trim();
|
||||
const psOutput = execSync(`ps -o rss=,pcpu= -p ${session.pid} 2>/dev/null || echo "0 0"`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
}).trim();
|
||||
|
||||
const [rss, cpu] = psOutput.split(/\s+/).map(x => parseFloat(x) || 0);
|
||||
const [rss, cpu] = psOutput.split(/\s+/).map((x) => parseFloat(x) || 0);
|
||||
|
||||
let childCount = 0;
|
||||
try {
|
||||
const childOutput = execSync(
|
||||
`pgrep -P ${session.pid} | wc -l`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
).trim();
|
||||
const childOutput = execSync(`pgrep -P ${session.pid} | wc -l`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
}).trim();
|
||||
childCount = parseInt(childOutput, 10) || 0;
|
||||
} catch {
|
||||
// No children or command failed
|
||||
}
|
||||
|
||||
return {
|
||||
memoryMB: Math.round(rss / 1024 * 10) / 10,
|
||||
memoryMB: Math.round((rss / 1024) * 10) / 10,
|
||||
cpuPercent: Math.round(cpu * 10) / 10,
|
||||
childCount,
|
||||
updatedAt: Date.now(),
|
||||
@@ -921,7 +987,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
|
||||
async getSessionsWithStats(): Promise<MuxSessionWithStats[]> {
|
||||
if (IS_TEST_MODE) {
|
||||
return Array.from(this.sessions.values()).map(s => ({
|
||||
return Array.from(this.sessions.values()).map((s) => ({
|
||||
...s,
|
||||
stats: { memoryMB: 0, cpuPercent: 0, childCount: 0, updatedAt: Date.now() },
|
||||
}));
|
||||
@@ -932,7 +998,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sessionPids = sessions.map(s => s.pid);
|
||||
const sessionPids = sessions.map((s) => s.pid);
|
||||
const statsMap = new Map<number, ProcessStats>();
|
||||
|
||||
try {
|
||||
@@ -941,7 +1007,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
|
||||
const pgrepOutput = execSync(
|
||||
`for p in ${sessionPids.join(' ')}; do children=$(pgrep -P $p 2>/dev/null | tr '\\n' ','); echo "$p:$children"; done`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
{
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
}
|
||||
).trim();
|
||||
|
||||
for (const line of pgrepOutput.split('\n')) {
|
||||
@@ -950,8 +1019,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
if (!Number.isNaN(sessionPid)) {
|
||||
const children = (childrenStr || '')
|
||||
.split(',')
|
||||
.map(s => parseInt(s.trim(), 10))
|
||||
.filter(n => !Number.isNaN(n) && n > 0);
|
||||
.map((s) => parseInt(s.trim(), 10))
|
||||
.filter((n) => !Number.isNaN(n) && n > 0);
|
||||
descendantMap.set(sessionPid, children);
|
||||
}
|
||||
}
|
||||
@@ -967,10 +1036,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
// Step 3: Single ps call
|
||||
const pidArray = Array.from(allPids);
|
||||
if (pidArray.length > 0) {
|
||||
const psOutput = execSync(
|
||||
`ps -o pid=,rss=,pcpu= -p ${pidArray.join(',')} 2>/dev/null || true`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
).trim();
|
||||
const psOutput = execSync(`ps -o pid=,rss=,pcpu= -p ${pidArray.join(',')} 2>/dev/null || true`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
}).trim();
|
||||
|
||||
const processStats = new Map<number, { rss: number; cpu: number }>();
|
||||
for (const line of psOutput.split('\n')) {
|
||||
@@ -1002,7 +1071,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
}
|
||||
|
||||
statsMap.set(sessionPid, {
|
||||
memoryMB: Math.round(totalRss / 1024 * 10) / 10,
|
||||
memoryMB: Math.round((totalRss / 1024) * 10) / 10,
|
||||
cpuPercent: Math.round(totalCpu * 10) / 10,
|
||||
childCount: children.length,
|
||||
updatedAt: Date.now(),
|
||||
@@ -1011,7 +1080,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
}
|
||||
} catch {
|
||||
// Fall back to individual queries
|
||||
const statsPromises = sessions.map(session => this.getProcessStats(session.sessionId));
|
||||
const statsPromises = sessions.map((session) => this.getProcessStats(session.sessionId));
|
||||
const results = await Promise.allSettled(statsPromises);
|
||||
return sessions.map((session, i) => ({
|
||||
...session,
|
||||
@@ -1019,7 +1088,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
}));
|
||||
}
|
||||
|
||||
return sessions.map(session => ({
|
||||
return sessions.map((session) => ({
|
||||
...session,
|
||||
stats: statsMap.get(session.pid) || undefined,
|
||||
}));
|
||||
@@ -1145,7 +1214,9 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
async sendInput(sessionId: string, input: string): Promise<boolean> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[TmuxManager] sendInput failed: no session found for ${sessionId}. Known: ${Array.from(this.sessions.keys()).join(', ')}`);
|
||||
console.error(
|
||||
`[TmuxManager] sendInput failed: no session found for ${sessionId}. Known: ${Array.from(this.sessions.keys()).join(', ')}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1154,7 +1225,9 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`[TmuxManager] sendInput to ${session.muxName}, input length: ${input.length}, hasCarriageReturn: ${input.includes('\r')}`);
|
||||
console.log(
|
||||
`[TmuxManager] sendInput to ${session.muxName}, input length: ${input.length}, hasCarriageReturn: ${input.includes('\r')}`
|
||||
);
|
||||
|
||||
if (!isValidMuxName(session.muxName)) {
|
||||
console.error('[TmuxManager] Invalid session name in sendInput:', session.muxName);
|
||||
@@ -1170,27 +1243,23 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
// Ink (Claude CLI's terminal framework) needs them split — sending both in a
|
||||
// single tmux invocation (via \;) causes Ink to interpret Enter as a newline
|
||||
// character in the input buffer rather than as form submission.
|
||||
await execAsync(
|
||||
`tmux send-keys -t "${session.muxName}" -l ${shellescape(textPart)}`,
|
||||
{ timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await execAsync(
|
||||
`tmux send-keys -t "${session.muxName}" Enter`,
|
||||
{ timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
await execAsync(`tmux send-keys -t "${session.muxName}" -l ${shellescape(textPart)}`, {
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await execAsync(`tmux send-keys -t "${session.muxName}" Enter`, {
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
} else if (textPart) {
|
||||
// Text only, no Enter
|
||||
await execAsync(
|
||||
`tmux send-keys -t "${session.muxName}" -l ${shellescape(textPart)}`,
|
||||
{ timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
await execAsync(`tmux send-keys -t "${session.muxName}" -l ${shellescape(textPart)}`, {
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
} else if (hasCarriageReturn) {
|
||||
// Enter only
|
||||
await execAsync(
|
||||
`tmux send-keys -t "${session.muxName}" Enter`,
|
||||
{ timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
await execAsync(`tmux send-keys -t "${session.muxName}" Enter`, {
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1215,10 +1284,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`tmux set-option -t "${muxName}" mouse on`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
execSync(`tmux set-option -t "${muxName}" mouse on`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
console.log(`[TmuxManager] Mouse mode ON for ${muxName}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
@@ -1239,10 +1308,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`tmux set-option -t "${muxName}" mouse off`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
execSync(`tmux set-option -t "${muxName}" mouse off`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
console.log(`[TmuxManager] Mouse mode OFF for ${muxName}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
@@ -1283,16 +1352,19 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
).trim();
|
||||
|
||||
return output.split('\n').map(line => {
|
||||
const [paneId, indexStr, pidStr, widthStr, heightStr] = line.split(':');
|
||||
return {
|
||||
paneId,
|
||||
paneIndex: parseInt(indexStr, 10),
|
||||
panePid: parseInt(pidStr, 10),
|
||||
width: parseInt(widthStr, 10),
|
||||
height: parseInt(heightStr, 10),
|
||||
};
|
||||
}).filter(p => !Number.isNaN(p.paneIndex));
|
||||
return output
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const [paneId, indexStr, pidStr, widthStr, heightStr] = line.split(':');
|
||||
return {
|
||||
paneId,
|
||||
paneIndex: parseInt(indexStr, 10),
|
||||
panePid: parseInt(pidStr, 10),
|
||||
width: parseInt(widthStr, 10),
|
||||
height: parseInt(heightStr, 10),
|
||||
};
|
||||
})
|
||||
.filter((p) => !Number.isNaN(p.paneIndex));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -1314,33 +1386,31 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
}
|
||||
|
||||
// Build target: sessionName.paneId (e.g., "codeman-abc12345.%1")
|
||||
const target = paneTarget.startsWith('%')
|
||||
? `${muxName}.${paneTarget}`
|
||||
: `${muxName}.%${paneTarget}`;
|
||||
const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`;
|
||||
|
||||
try {
|
||||
const hasCarriageReturn = input.includes('\r');
|
||||
const textPart = input.replace(/\r/g, '').replace(/\n/g, '').trimEnd();
|
||||
|
||||
if (textPart && hasCarriageReturn) {
|
||||
execSync(
|
||||
`tmux send-keys -t ${shellescape(target)} -l ${shellescape(textPart)}`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
execSync(
|
||||
`tmux send-keys -t ${shellescape(target)} Enter`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
execSync(`tmux send-keys -t ${shellescape(target)} -l ${shellescape(textPart)}`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
execSync(`tmux send-keys -t ${shellescape(target)} Enter`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
} else if (textPart) {
|
||||
execSync(
|
||||
`tmux send-keys -t ${shellescape(target)} -l ${shellescape(textPart)}`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
execSync(`tmux send-keys -t ${shellescape(target)} -l ${shellescape(textPart)}`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
} else if (hasCarriageReturn) {
|
||||
execSync(
|
||||
`tmux send-keys -t ${shellescape(target)} Enter`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
execSync(`tmux send-keys -t ${shellescape(target)} Enter`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1365,15 +1435,13 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
return null;
|
||||
}
|
||||
|
||||
const target = paneTarget.startsWith('%')
|
||||
? `${muxName}.${paneTarget}`
|
||||
: `${muxName}.%${paneTarget}`;
|
||||
const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`;
|
||||
|
||||
try {
|
||||
return execSync(
|
||||
`tmux capture-pane -p -e -t ${shellescape(target)} -S -5000`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
return execSync(`tmux capture-pane -p -e -t ${shellescape(target)} -S -5000`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[TmuxManager] Failed to capture pane buffer:', err);
|
||||
return null;
|
||||
@@ -1399,15 +1467,13 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = paneTarget.startsWith('%')
|
||||
? `${muxName}.${paneTarget}`
|
||||
: `${muxName}.%${paneTarget}`;
|
||||
const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`;
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`tmux pipe-pane -O -t ${shellescape(target)} ${shellescape('cat >> ' + outputFile)}`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
execSync(`tmux pipe-pane -O -t ${shellescape(target)} ${shellescape('cat >> ' + outputFile)}`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[TmuxManager] Failed to start pipe-pane:', err);
|
||||
@@ -1429,15 +1495,13 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = paneTarget.startsWith('%')
|
||||
? `${muxName}.${paneTarget}`
|
||||
: `${muxName}.%${paneTarget}`;
|
||||
const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`;
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`tmux pipe-pane -t ${shellescape(target)}`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
execSync(`tmux pipe-pane -t ${shellescape(target)}`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[TmuxManager] Failed to stop pipe-pane:', err);
|
||||
|
||||
Reference in New Issue
Block a user