refactor: pass 2 — extract shared helpers and simplify patterns

app.js:
- Add _clearTimer() helper replacing 11 inline clearTimeout patterns
- Add _isStaleSelect() helper for generation check + cleanup
- Replace 11 keyboard shortcut if-blocks with data-driven lookup table
- Extract _cleanupPreviousSession() from selectSession() (~75 lines)
- Extract _resetAllAppState() from handleInit() (~75 lines)

tmux-manager:
- Extract buildEnvExports() eliminating duplication in createSession/respawnPane
- Extract buildPathExport() for CLI path resolution
- Extract _configureOpenCode() for OpenCode setup

routes:
- Add readJsonConfig() to route-helpers, replacing 5 inline JSON-read patterns
- Add validateSessionFilePath() to route-helpers, replacing 2 identical path
  traversal validation blocks in file-routes

session-auto-ops:
- Convert executeWhenIdle() from 8 positional params to options object
- Extract validateThreshold() for shared compact/clear validation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-03-25 23:32:28 +01:00
co-authored by Claude Opus 4.6
parent 3145eac6d9
commit a448983be3
6 changed files with 237 additions and 257 deletions
+41
View File
@@ -6,6 +6,8 @@
*/
import { join, resolve, relative, isAbsolute } from 'node:path';
import { realpathSync } from 'node:fs';
import fs from 'node:fs/promises';
import { homedir } from 'node:os';
import type { z } from 'zod';
import { Session } from '../session.js';
@@ -33,6 +35,45 @@ export function validatePathWithinBase(name: string, baseDir: string): string |
return fullPath;
}
/**
* Reads and parses a JSON config file, returning a default value on ENOENT.
* Logs an error for any I/O failure other than a missing file.
*/
export async function readJsonConfig<T>(filePath: string, logLabel: string, defaultValue: T): Promise<T> {
try {
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content) as T;
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
console.error(`Failed to read ${logLabel}:`, err);
}
return defaultValue;
}
}
/**
* Validates that a file path (possibly containing symlinks) resolves to a location
* within the given session working directory. Returns the resolved and relative paths,
* or null if the path escapes the directory or doesn't exist.
*/
export function validateSessionFilePath(
sessionWorkingDir: string,
filePath: string
): { resolvedPath: string; relativePath: string } | null {
const fullPath = resolve(sessionWorkingDir, filePath);
let resolvedPath: string;
try {
resolvedPath = realpathSync(fullPath);
} catch {
return null;
}
const relativePath = relative(sessionWorkingDir, resolvedPath);
if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
return null;
}
return { resolvedPath, relativePath };
}
// Maximum hook data size (prevents oversized SSE broadcasts)
const MAX_HOOK_DATA_SIZE = 8 * 1024;