diff --git a/src/cli.ts b/src/cli.ts index 7fc4c203..e27fec95 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,6 +13,7 @@ import { getSessionManager } from './session-manager.js'; import { getTaskQueue } from './task-queue.js'; import { getRalphLoop } from './ralph-loop.js'; import { getStore } from './state-store.js'; +import { getErrorMessage } from './types.js'; const program = new Command(); @@ -40,7 +41,7 @@ sessionCmd console.log(` Working directory: ${session.workingDir}`); console.log(` PID: ${session.pid}`); } catch (err) { - console.error(chalk.red(`✗ Failed to start session: ${(err as Error).message}`)); + console.error(chalk.red(`✗ Failed to start session: ${getErrorMessage(err)}`)); process.exit(1); } }); @@ -54,7 +55,7 @@ sessionCmd await manager.stopSession(id); console.log(chalk.green(`✓ Session stopped: ${id}`)); } catch (err) { - console.error(chalk.red(`✗ Failed to stop session: ${(err as Error).message}`)); + console.error(chalk.red(`✗ Failed to stop session: ${getErrorMessage(err)}`)); process.exit(1); } }); @@ -474,7 +475,7 @@ program console.log(chalk.green(`\n✓ Web interface running at http://localhost:${port}`)); console.log(chalk.gray(' Press Ctrl+C to stop\n')); } catch (err) { - console.error(chalk.red(`✗ Failed to start web server: ${(err as Error).message}`)); + console.error(chalk.red(`✗ Failed to start web server: ${getErrorMessage(err)}`)); process.exit(1); } }); @@ -496,7 +497,7 @@ program port, }); } catch (err) { - console.error(chalk.red(`✗ Failed to start TUI: ${(err as Error).message}`)); + console.error(chalk.red(`✗ Failed to start TUI: ${getErrorMessage(err)}`)); process.exit(1); } }); diff --git a/src/ralph-loop.ts b/src/ralph-loop.ts index 695b483a..a996886f 100644 --- a/src/ralph-loop.ts +++ b/src/ralph-loop.ts @@ -19,7 +19,7 @@ import { getTaskQueue, TaskQueue } from './task-queue.js'; import { getStore, StateStore } from './state-store.js'; import { Session } from './session.js'; import { Task } from './task.js'; -import { RalphLoopStatus } from './types.js'; +import { RalphLoopStatus, getErrorMessage } from './types.js'; /** * Events emitted by RalphLoop @@ -304,10 +304,10 @@ export class RalphLoop extends EventEmitter { this.emit('taskAssigned', task.id, session.id); } catch (err) { - task.fail((err as Error).message); + task.fail(getErrorMessage(err)); session.clearTask(); this.taskQueue.updateTask(task); - this.emit('taskFailed', task.id, (err as Error).message); + this.emit('taskFailed', task.id, getErrorMessage(err)); } } diff --git a/src/screen-manager.ts b/src/screen-manager.ts index 98df15ea..c6b443a0 100644 --- a/src/screen-manager.ts +++ b/src/screen-manager.ts @@ -19,7 +19,7 @@ import { spawn, execSync } from 'node:child_process'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { homedir } from 'node:os'; -import { ScreenSession, ProcessStats, ScreenSessionWithStats } from './types.js'; +import { ScreenSession, ProcessStats, ScreenSessionWithStats, getErrorMessage } from './types.js'; /** Path to persisted screen session metadata */ const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json'); @@ -213,7 +213,7 @@ export class ScreenManager extends EventEmitter { return screen; } catch (err) { - throw new Error(`Failed to create screen: ${(err as Error).message}`); + throw new Error(`Failed to create screen: ${getErrorMessage(err)}`); } } diff --git a/src/types.ts b/src/types.ts index 1e6299d6..ddcde499 100644 --- a/src/types.ts +++ b/src/types.ts @@ -615,3 +615,43 @@ export function createInitialState(): AppState { config: { ...DEFAULT_CONFIG }, }; } + +// ========== Error Handling Utilities ========== + +/** + * Type guard to check if a value is an Error instance + * @param value The value to check + * @returns True if the value is an Error instance + */ +export function isError(value: unknown): value is Error { + return value instanceof Error; +} + +/** + * Safely extracts an error message from an unknown caught value. + * Handles the TypeScript 4.4+ unknown error type in catch blocks. + * + * @param error The caught error (type unknown in strict mode) + * @returns A string error message + * + * @example + * ```typescript + * try { + * await riskyOperation(); + * } catch (err) { + * console.error('Failed:', getErrorMessage(err)); + * } + * ``` + */ +export function getErrorMessage(error: unknown): string { + if (isError(error)) { + return error.message; + } + if (typeof error === 'string') { + return error; + } + if (error && typeof error === 'object' && 'message' in error) { + return String((error as { message: unknown }).message); + } + return 'An unknown error occurred'; +} diff --git a/src/web/server.ts b/src/web/server.ts index 30d1791f..58eec2ff 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -23,19 +23,20 @@ import { ScreenManager } from '../screen-manager.js'; import { getStore } from '../state-store.js'; import { generateClaudeMd } from '../templates/claude-md.js'; import { v4 as uuidv4 } from 'uuid'; -import type { - CreateSessionRequest, - RunPromptRequest, - SessionInputRequest, - ResizeRequest, - CreateCaseRequest, - QuickStartRequest, - CreateScheduledRunRequest, - QuickRunRequest, - ApiResponse, - SessionResponse, - QuickStartResponse, - CaseInfo, +import { + getErrorMessage, + type CreateSessionRequest, + type RunPromptRequest, + type SessionInputRequest, + type ResizeRequest, + type CreateCaseRequest, + type QuickStartRequest, + type CreateScheduledRunRequest, + type QuickRunRequest, + type ApiResponse, + type SessionResponse, + type QuickStartResponse, + type CaseInfo, } from '../types.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -376,7 +377,7 @@ export class WebServer extends EventEmitter { this.broadcast('session:updated', { session: session.toDetailedState() }); return { success: true, message: 'Interactive session started' }; } catch (err) { - return { error: (err as Error).message }; + return { error: getErrorMessage(err) }; } }); @@ -399,7 +400,7 @@ export class WebServer extends EventEmitter { this.broadcast('session:updated', { session: session.toDetailedState() }); return { success: true, message: 'Shell session started' }; } catch (err) { - return { error: (err as Error).message }; + return { error: getErrorMessage(err) }; } }); @@ -580,7 +581,7 @@ export class WebServer extends EventEmitter { respawnStatus: controller.getStatus(), }; } catch (err) { - return { error: (err as Error).message }; + return { error: getErrorMessage(err) }; } }); @@ -694,7 +695,7 @@ export class WebServer extends EventEmitter { } catch (err) { // Clean up session on error too await this.cleanupSession(session.id); - return { success: false, sessionId: session.id, error: (err as Error).message }; + return { success: false, sessionId: session.id, error: getErrorMessage(err) }; } }); @@ -809,7 +810,7 @@ export class WebServer extends EventEmitter { return { success: true, case: { name, path: casePath } }; } catch (err) { - return { success: false, error: (err as Error).message }; + return { success: false, error: getErrorMessage(err) }; } }); @@ -868,7 +869,7 @@ export class WebServer extends EventEmitter { this.broadcast('case:linked', { name, path: expandedPath }); return { success: true, case: { name, path: expandedPath } }; } catch (err) { - return { success: false, error: (err as Error).message }; + return { success: false, error: getErrorMessage(err) }; } }); @@ -944,7 +945,7 @@ export class WebServer extends EventEmitter { this.broadcast('case:created', { name: caseName, path: casePath }); } catch (err) { - return { success: false, error: `Failed to create case: ${(err as Error).message}` }; + return { success: false, error: `Failed to create case: ${getErrorMessage(err)}` }; } } @@ -996,7 +997,7 @@ export class WebServer extends EventEmitter { } catch (err) { // Clean up session on error to prevent orphaned resources await this.cleanupSession(session.id); - return { success: false, error: (err as Error).message }; + return { success: false, error: getErrorMessage(err) }; } }); @@ -1026,7 +1027,7 @@ export class WebServer extends EventEmitter { writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); return { success: true }; } catch (err) { - return { error: (err as Error).message }; + return { error: getErrorMessage(err) }; } }); @@ -1415,7 +1416,7 @@ export class WebServer extends EventEmitter { // Small pause between iterations await new Promise(r => setTimeout(r, 2000)); } catch (err) { - addLog(`Error: ${(err as Error).message}`); + addLog(`Error: ${getErrorMessage(err)}`); this.broadcast('scheduled:updated', run); // Clean up the session on error too