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
+2 -10
View File
@@ -14,7 +14,7 @@ import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.
import { CreateCaseSchema, LinkCaseSchema } from '../schemas.js';
import { generateClaudeMd } from '../../templates/claude-md.js';
import { writeHooksConfig } from '../../hooks-config.js';
import { CASES_DIR, validatePathWithinBase, parseBody } from '../route-helpers.js';
import { CASES_DIR, validatePathWithinBase, parseBody, readJsonConfig } from '../route-helpers.js';
import { SseEvent } from '../sse-events.js';
import type { EventPort, ConfigPort } from '../ports/index.js';
@@ -22,15 +22,7 @@ const LINKED_CASES_FILE = join(homedir(), '.codeman', 'linked-cases.json');
/** Read and parse linked-cases.json, returning empty object on missing/invalid file. */
async function readLinkedCases(): Promise<Record<string, string>> {
try {
return JSON.parse(await fs.readFile(LINKED_CASES_FILE, 'utf-8'));
} catch (err) {
// Only warn on real I/O errors, not ENOENT (file missing) or SyntaxError (corrupted JSON)
if ((err as NodeJS.ErrnoException).code && (err as NodeJS.ErrnoException).code !== 'ENOENT') {
console.warn('[Server] Failed to read linked cases:', err);
}
return {};
}
return readJsonConfig<Record<string, string>>(LINKED_CASES_FILE, 'linked cases', {});
}
/** Resolve a case name to its directory path, checking linked cases first, then CASES_DIR. */
+8 -22
View File
@@ -4,12 +4,11 @@
*/
import { FastifyInstance } from 'fastify';
import { join, resolve, relative, isAbsolute } from 'node:path';
import { realpathSync } from 'node:fs';
import { join } from 'node:path';
import fs from 'node:fs/promises';
import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js';
import { fileStreamManager } from '../../file-stream-manager.js';
import { findSessionOrFail } from '../route-helpers.js';
import { findSessionOrFail, validateSessionFilePath } from '../route-helpers.js';
import type { SessionPort } from '../ports/index.js';
export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort): void {
@@ -148,17 +147,11 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort): void
}
// Validate path is within working directory (security: resolve symlinks to prevent traversal)
const fullPath = resolve(session.workingDir, filePath);
let resolvedPath: string;
try {
resolvedPath = realpathSync(fullPath);
} catch {
const validated = validateSessionFilePath(session.workingDir, filePath);
if (!validated) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found');
}
const relativePath = relative(session.workingDir, resolvedPath);
if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Path must be within working directory');
}
const { resolvedPath } = validated;
try {
const stat = await fs.stat(resolvedPath);
@@ -255,19 +248,12 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort): void
}
// Validate path is within working directory (security: resolve symlinks to prevent traversal)
const fullPath = resolve(session.workingDir, filePath);
let resolvedPath: string;
try {
resolvedPath = realpathSync(fullPath);
} catch {
const validated = validateSessionFilePath(session.workingDir, filePath);
if (!validated) {
reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found'));
return;
}
const relativePath = relative(session.workingDir, resolvedPath);
if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Path must be within working directory'));
return;
}
const { resolvedPath } = validated;
try {
// Validate file size before reading (DoS protection - prevent memory exhaustion)
+6 -38
View File
@@ -24,7 +24,7 @@ import {
import { subagentWatcher } from '../../subagent-watcher.js';
import { imageWatcher } from '../../image-watcher.js';
import { getLifecycleLog } from '../../session-lifecycle-log.js';
import { findSessionOrFail, formatUptime, parseBody, SETTINGS_PATH } from '../route-helpers.js';
import { findSessionOrFail, formatUptime, parseBody, readJsonConfig, SETTINGS_PATH } from '../route-helpers.js';
import { SseEvent } from '../sse-events.js';
import type { SessionPort, EventPort, ConfigPort, InfraPort, AuthPort } from '../ports/index.js';
import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
@@ -393,15 +393,7 @@ export function registerSystemRoutes(
// ========== Settings ==========
app.get('/api/settings', async () => {
try {
const content = await fs.readFile(SETTINGS_PATH, 'utf-8');
return JSON.parse(content);
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
console.error('Failed to read settings:', err);
}
}
return {};
return readJsonConfig(SETTINGS_PATH, 'settings', {});
});
app.put('/api/settings', async (req) => {
@@ -472,16 +464,8 @@ export function registerSystemRoutes(
// ========== Model Configuration ==========
app.get('/api/execution/model-config', async () => {
try {
const content = await fs.readFile(SETTINGS_PATH, 'utf-8');
const settings = JSON.parse(content);
return { success: true, data: settings.modelConfig || {} };
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
console.error('Failed to read model config:', err);
}
return { success: true, data: {} };
}
const settings = await readJsonConfig<Record<string, unknown>>(SETTINGS_PATH, 'model config', {});
return { success: true, data: settings.modelConfig || {} };
});
app.put('/api/execution/model-config', async (req) => {
@@ -545,15 +529,7 @@ export function registerSystemRoutes(
// ========== Subagent Window State Persistence ==========
app.get('/api/subagent-window-states', async () => {
try {
const content = await fs.readFile(windowStatesPath, 'utf-8');
return JSON.parse(content);
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
console.error('Failed to read subagent window states:', err);
}
}
return { minimized: {}, open: [] };
return readJsonConfig(windowStatesPath, 'subagent window states', { minimized: {}, open: [] });
});
app.put('/api/subagent-window-states', async (req) => {
@@ -573,15 +549,7 @@ export function registerSystemRoutes(
// ========== Subagent Parent Associations ==========
app.get('/api/subagent-parents', async () => {
try {
const content = await fs.readFile(parentMapPath, 'utf-8');
return JSON.parse(content);
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
console.error('Failed to read subagent parent map:', err);
}
}
return {};
return readJsonConfig(parentMapPath, 'subagent parent map', {});
});
app.put('/api/subagent-parents', async (req) => {