@@ -18,6 +18,28 @@ import { CASES_DIR, validatePathWithinBase } from '../route-helpers.js';
1818import { SseEvent } from '../sse-events.js' ;
1919import type { EventPort , ConfigPort } from '../ports/index.js' ;
2020
21+ const LINKED_CASES_FILE = join ( homedir ( ) , '.codeman' , 'linked-cases.json' ) ;
22+
23+ /** Read and parse linked-cases.json, returning empty object on missing/invalid file. */
24+ async function readLinkedCases ( ) : Promise < Record < string , string > > {
25+ try {
26+ return JSON . parse ( await fs . readFile ( LINKED_CASES_FILE , 'utf-8' ) ) ;
27+ } catch ( err ) {
28+ if ( ( err as NodeJS . ErrnoException ) . code !== 'ENOENT' ) {
29+ console . warn ( '[Server] Failed to read linked cases:' , err ) ;
30+ }
31+ return { } ;
32+ }
33+ }
34+
35+ /** Resolve a case name to its directory path, checking linked cases if not in CASES_DIR. */
36+ async function resolveCasePath ( name : string ) : Promise < string > {
37+ const casePath = join ( CASES_DIR , name ) ;
38+ if ( existsSync ( casePath ) ) return casePath ;
39+ const linkedCases = await readLinkedCases ( ) ;
40+ return linkedCases [ name ] ?? casePath ;
41+ }
42+
2143export function registerCaseRoutes ( app : FastifyInstance , ctx : EventPort & ConfigPort ) : void {
2244 // ═══════════════════════════════════════════════════════════════
2345 // Case CRUD (list, create, link, detail, fix-plan)
@@ -45,22 +67,15 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
4567 }
4668
4769 // Get linked cases
48- const linkedCasesFile = join ( homedir ( ) , '.codeman' , 'linked-cases.json' ) ;
49- try {
50- const linkedCases : Record < string , string > = JSON . parse ( await fs . readFile ( linkedCasesFile , 'utf-8' ) ) ;
51- for ( const [ name , path ] of Object . entries ( linkedCases ) ) {
52- // Only add if not already in cases (avoid duplicates) and path exists
53- if ( ! cases . some ( ( c ) => c . name === name ) && existsSync ( path ) ) {
54- cases . push ( {
55- name,
56- path,
57- hasClaudeMd : existsSync ( join ( path , 'CLAUDE.md' ) ) ,
58- } ) ;
59- }
60- }
61- } catch ( err ) {
62- if ( ( err as NodeJS . ErrnoException ) . code !== 'ENOENT' ) {
63- console . warn ( '[Server] Failed to read linked cases:' , err ) ;
70+ const linkedCases = await readLinkedCases ( ) ;
71+ const existingNames = new Set ( cases . map ( ( c ) => c . name ) ) ;
72+ for ( const [ name , path ] of Object . entries ( linkedCases ) ) {
73+ if ( ! existingNames . has ( name ) && existsSync ( path ) ) {
74+ cases . push ( {
75+ name,
76+ path,
77+ hasClaudeMd : existsSync ( join ( path , 'CLAUDE.md' ) ) ,
78+ } ) ;
6479 }
6580 }
6681
@@ -126,15 +141,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
126141 }
127142
128143 // Load existing linked cases
129- const linkedCasesFile = join ( homedir ( ) , '.codeman' , 'linked-cases.json' ) ;
130- let linkedCases : Record < string , string > = { } ;
131- try {
132- linkedCases = JSON . parse ( await fs . readFile ( linkedCasesFile , 'utf-8' ) ) ;
133- } catch ( err ) {
134- if ( ( err as NodeJS . ErrnoException ) . code !== 'ENOENT' ) {
135- console . warn ( '[Server] Failed to read linked cases:' , err ) ;
136- }
137- }
144+ const linkedCases = await readLinkedCases ( ) ;
138145
139146 // Check if name is already linked
140147 if ( linkedCases [ name ] ) {
@@ -151,7 +158,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
151158 if ( ! existsSync ( codemanDir ) ) {
152159 mkdirSync ( codemanDir , { recursive : true } ) ;
153160 }
154- await fs . writeFile ( linkedCasesFile , JSON . stringify ( linkedCases , null , 2 ) ) ;
161+ await fs . writeFile ( LINKED_CASES_FILE , JSON . stringify ( linkedCases , null , 2 ) ) ;
155162 ctx . broadcast ( SseEvent . CaseLinked , { name, path : expandedPath } ) ;
156163 return { success : true , data : { case : { name, path : expandedPath } } } ;
157164 } catch ( err ) {
@@ -166,34 +173,18 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
166173 return createErrorResponse ( ApiErrorCode . INVALID_INPUT , 'Invalid case name' ) ;
167174 }
168175
169- // First check linked cases
170- const linkedCasesFile = join ( homedir ( ) , '.codeman' , 'linked-cases.json' ) ;
171- try {
172- const linkedCases : Record < string , string > = JSON . parse ( await fs . readFile ( linkedCasesFile , 'utf-8' ) ) ;
173- if ( linkedCases [ name ] ) {
174- const linkedPath = linkedCases [ name ] ;
175- return {
176- name,
177- path : linkedPath ,
178- hasClaudeMd : existsSync ( join ( linkedPath , 'CLAUDE.md' ) ) ,
179- linked : true ,
180- } ;
181- }
182- } catch {
183- // ENOENT or parse errors - fall through to CASES_DIR check
184- }
185-
186- // Then check CASES_DIR
187- const casePath = join ( CASES_DIR , name ) ;
176+ const casePath = await resolveCasePath ( name ) ;
188177
189178 if ( ! existsSync ( casePath ) ) {
190179 return createErrorResponse ( ApiErrorCode . NOT_FOUND , 'Case not found' ) ;
191180 }
192181
182+ const linked = casePath !== join ( CASES_DIR , name ) ;
193183 return {
194184 name,
195185 path : casePath ,
196186 hasClaudeMd : existsSync ( join ( casePath , 'CLAUDE.md' ) ) ,
187+ ...( linked && { linked : true } ) ,
197188 } ;
198189 } ) ;
199190
@@ -206,21 +197,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
206197 }
207198
208199 // Get case path (check linked cases first, then CASES_DIR)
209- let casePath : string | null = null ;
210-
211- const linkedCasesFile = join ( homedir ( ) , '.codeman' , 'linked-cases.json' ) ;
212- try {
213- const linkedCases : Record < string , string > = JSON . parse ( await fs . readFile ( linkedCasesFile , 'utf-8' ) ) ;
214- if ( linkedCases [ name ] ) {
215- casePath = linkedCases [ name ] ;
216- }
217- } catch {
218- // ENOENT or parse errors - fall through to CASES_DIR
219- }
220-
221- if ( ! casePath ) {
222- casePath = join ( CASES_DIR , name ) ;
223- }
200+ const casePath = await resolveCasePath ( name ) ;
224201
225202 const fixPlanPath = join ( casePath , '@fix_plan.md' ) ;
226203
@@ -321,23 +298,11 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
321298
322299 app . get ( '/api/cases/:caseName/ralph-wizard/files' , async ( req ) => {
323300 const { caseName } = req . params as { caseName : string } ;
324- let casePath = validatePathWithinBase ( caseName , CASES_DIR ) ;
325- if ( ! casePath ) {
301+ if ( ! validatePathWithinBase ( caseName , CASES_DIR ) ) {
326302 return createErrorResponse ( ApiErrorCode . INVALID_INPUT , 'Invalid case name' ) ;
327303 }
328304
329- // Check linked cases if path doesn't exist
330- if ( ! existsSync ( casePath ) ) {
331- const linkedCasesFile = join ( homedir ( ) , '.codeman' , 'linked-cases.json' ) ;
332- try {
333- const linkedCases : Record < string , string > = JSON . parse ( await fs . readFile ( linkedCasesFile , 'utf-8' ) ) ;
334- if ( linkedCases [ caseName ] ) {
335- casePath = linkedCases [ caseName ] ;
336- }
337- } catch {
338- // No linked cases file
339- }
340- }
305+ const casePath = await resolveCasePath ( caseName ) ;
341306
342307 const wizardDir = join ( casePath , 'ralph-wizard' ) ;
343308
@@ -376,8 +341,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
376341 // Cache disabled to ensure fresh prompts when starting new plan generations
377342 app . get ( '/api/cases/:caseName/ralph-wizard/file/:filePath' , async ( req , reply ) => {
378343 const { caseName, filePath } = req . params as { caseName : string ; filePath : string } ;
379- let casePath = validatePathWithinBase ( caseName , CASES_DIR ) ;
380- if ( ! casePath ) {
344+ if ( ! validatePathWithinBase ( caseName , CASES_DIR ) ) {
381345 return createErrorResponse ( ApiErrorCode . INVALID_INPUT , 'Invalid case name' ) ;
382346 }
383347
@@ -386,18 +350,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
386350 reply . header ( 'Pragma' , 'no-cache' ) ;
387351 reply . header ( 'Expires' , '0' ) ;
388352
389- // Check linked cases if path doesn't exist
390- if ( ! existsSync ( casePath ) ) {
391- const linkedCasesFile = join ( homedir ( ) , '.codeman' , 'linked-cases.json' ) ;
392- try {
393- const linkedCases : Record < string , string > = JSON . parse ( await fs . readFile ( linkedCasesFile , 'utf-8' ) ) ;
394- if ( linkedCases [ caseName ] ) {
395- casePath = linkedCases [ caseName ] ;
396- }
397- } catch {
398- // No linked cases file
399- }
400- }
353+ const casePath = await resolveCasePath ( caseName ) ;
401354
402355 const wizardDir = join ( casePath , 'ralph-wizard' ) ;
403356
0 commit comments