diff --git a/cli/src/utils/__tests__/logger.test.ts b/cli/src/utils/__tests__/logger.test.ts new file mode 100644 index 0000000000..3cc83c4fb5 --- /dev/null +++ b/cli/src/utils/__tests__/logger.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from 'bun:test' + +import path from 'path' + +import { CHAT_LOG_FILENAME, resolveLogTarget } from '../logger' + +describe('resolveLogTarget', () => { + test('uses the project debug log in development', () => { + let currentChatDirCalls = 0 + + const target = resolveLogTarget({ + projectRoot: '/project', + isDev: true, + getCurrentChatDir: () => { + currentChatDirCalls += 1 + return '/chat' + }, + }) + + expect(target).toBe(path.join('/project', 'debug', 'cli.jsonl')) + expect(currentChatDirCalls).toBe(0) + }) + + test('uses the current chat log in production', () => { + const target = resolveLogTarget({ + projectRoot: '/project', + isDev: false, + getCurrentChatDir: () => '/chat/2026-01-01T00-00-00.000Z', + }) + + expect(target).toBe( + path.join('/chat/2026-01-01T00-00-00.000Z', CHAT_LOG_FILENAME), + ) + }) + + test('skips file logging when the chat directory cannot be created', () => { + const target = resolveLogTarget({ + projectRoot: '/project', + isDev: false, + getCurrentChatDir: () => { + throw new Error('EACCES') + }, + }) + + expect(target).toBeUndefined() + }) +}) diff --git a/cli/src/utils/logger.ts b/cli/src/utils/logger.ts index 1a4b5270e5..d44f3b4acf 100644 --- a/cli/src/utils/logger.ts +++ b/cli/src/utils/logger.ts @@ -100,6 +100,45 @@ function setLogPath(p: string): void { ) } +/** + * Resolve the per-run log destination without allowing filesystem failures to + * take down the CLI during startup. + * + * In production, resolving the destination creates the current chat + * directory. That directory may be unavailable (for example, when the + * config directory is read-only), so callers must treat an absent destination + * as "continue without file logging". + */ +export function resolveLogTarget(params: { + projectRoot: string + isDev: boolean + getCurrentChatDir: () => string +}): string | undefined { + try { + return params.isDev + ? path.join(params.projectRoot, 'debug', 'cli.jsonl') + : path.join(params.getCurrentChatDir(), CHAT_LOG_FILENAME) + } catch { + return undefined + } +} + +function trySetLogPath(projectRoot: string): void { + const logTarget = resolveLogTarget({ + projectRoot, + isDev: IS_DEV, + getCurrentChatDir, + }) + if (!logTarget) return + + try { + setLogPath(logTarget) + } catch { + // File logging is best-effort and must never prevent the CLI from + // starting when the config or chat directory cannot be written. + } +} + export function clearLogFile(): void { const projectRoot = getProjectRoot() const debugDir = path.join(projectRoot, 'debug') @@ -139,12 +178,7 @@ function sendAnalyticsAndLog( projectRoot = undefined } if (projectRoot) { - const logTarget = - IS_DEV - ? path.join(projectRoot, 'debug', 'cli.jsonl') - : path.join(getCurrentChatDir(), CHAT_LOG_FILENAME) - - setLogPath(logTarget) + trySetLogPath(projectRoot) } }