Skip to content

Commit fa4bc28

Browse files
fix(cli): survive unavailable chat log directory
1 parent 366311e commit fa4bc28

2 files changed

Lines changed: 87 additions & 6 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import path from 'path'
4+
5+
import { CHAT_LOG_FILENAME, resolveLogTarget } from '../logger'
6+
7+
describe('resolveLogTarget', () => {
8+
test('uses the project debug log in development', () => {
9+
let currentChatDirCalls = 0
10+
11+
const target = resolveLogTarget({
12+
projectRoot: '/project',
13+
isDev: true,
14+
getCurrentChatDir: () => {
15+
currentChatDirCalls += 1
16+
return '/chat'
17+
},
18+
})
19+
20+
expect(target).toBe(path.join('/project', 'debug', 'cli.jsonl'))
21+
expect(currentChatDirCalls).toBe(0)
22+
})
23+
24+
test('uses the current chat log in production', () => {
25+
const target = resolveLogTarget({
26+
projectRoot: '/project',
27+
isDev: false,
28+
getCurrentChatDir: () => '/chat/2026-01-01T00-00-00.000Z',
29+
})
30+
31+
expect(target).toBe(
32+
path.join('/chat/2026-01-01T00-00-00.000Z', CHAT_LOG_FILENAME),
33+
)
34+
})
35+
36+
test('skips file logging when the chat directory cannot be created', () => {
37+
const target = resolveLogTarget({
38+
projectRoot: '/project',
39+
isDev: false,
40+
getCurrentChatDir: () => {
41+
throw new Error('EACCES')
42+
},
43+
})
44+
45+
expect(target).toBeUndefined()
46+
})
47+
})

cli/src/utils/logger.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,45 @@ function setLogPath(p: string): void {
100100
)
101101
}
102102

103+
/**
104+
* Resolve the per-run log destination without allowing filesystem failures to
105+
* take down the CLI during startup.
106+
*
107+
* In production, resolving the destination creates the current chat
108+
* directory. That directory may be unavailable (for example, when the
109+
* config directory is read-only), so callers must treat an absent destination
110+
* as "continue without file logging".
111+
*/
112+
export function resolveLogTarget(params: {
113+
projectRoot: string
114+
isDev: boolean
115+
getCurrentChatDir: () => string
116+
}): string | undefined {
117+
try {
118+
return params.isDev
119+
? path.join(params.projectRoot, 'debug', 'cli.jsonl')
120+
: path.join(params.getCurrentChatDir(), CHAT_LOG_FILENAME)
121+
} catch {
122+
return undefined
123+
}
124+
}
125+
126+
function trySetLogPath(projectRoot: string): void {
127+
const logTarget = resolveLogTarget({
128+
projectRoot,
129+
isDev: IS_DEV,
130+
getCurrentChatDir,
131+
})
132+
if (!logTarget) return
133+
134+
try {
135+
setLogPath(logTarget)
136+
} catch {
137+
// File logging is best-effort and must never prevent the CLI from
138+
// starting when the config or chat directory cannot be written.
139+
}
140+
}
141+
103142
export function clearLogFile(): void {
104143
const projectRoot = getProjectRoot()
105144
const debugDir = path.join(projectRoot, 'debug')
@@ -139,12 +178,7 @@ function sendAnalyticsAndLog(
139178
projectRoot = undefined
140179
}
141180
if (projectRoot) {
142-
const logTarget =
143-
IS_DEV
144-
? path.join(projectRoot, 'debug', 'cli.jsonl')
145-
: path.join(getCurrentChatDir(), CHAT_LOG_FILENAME)
146-
147-
setLogPath(logTarget)
181+
trySetLogPath(projectRoot)
148182
}
149183
}
150184

0 commit comments

Comments
 (0)