mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 23:49:41 +02:00
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:
@@ -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. */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user