@@ -83,10 +83,11 @@ function checkRipgrep(): Check {
8383 }
8484}
8585
86- function checkGit ( ) : Check {
87- const inRepo = fs . existsSync ( path . join ( process . cwd ( ) , '.git' ) )
86+ function checkGit ( overrides : { projectRoot ?: string } = { } ) : Check {
87+ const root = overrides . projectRoot ?? process . cwd ( )
88+ const inRepo = fs . existsSync ( path . join ( root , '.git' ) )
8889 if ( inRepo ) {
89- return { name : 'git repository' , status : 'ok' , detail : process . cwd ( ) }
90+ return { name : 'git repository' , status : 'ok' , detail : root }
9091 }
9192 return {
9293 name : 'git repository' ,
@@ -136,6 +137,149 @@ function checkNodeCompat(): Check {
136137 }
137138}
138139
140+ // ── Hooks & skills checks (syntax-level, dependency-light) ──────────────
141+
142+ const SETTINGS_CANDIDATES = ( projectRoot : string , homeDir ?: string ) : string [ ] => {
143+ const home = homeDir ?? os . homedir ( )
144+ const envSuffix =
145+ process . env . NEXT_PUBLIC_CB_ENVIRONMENT &&
146+ process . env . NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod'
147+ ? `-${ process . env . NEXT_PUBLIC_CB_ENVIRONMENT } `
148+ : ''
149+ const levelcodeBase =
150+ process . env . LEVELCODE_DIR || path . join ( home , '.levelcode' )
151+ return [
152+ path . join ( home , '.config' , `levelcode${ envSuffix } ` , 'settings.json' ) ,
153+ path . join ( levelcodeBase , 'settings.json' ) ,
154+ path . join ( projectRoot , '.levelcode' , 'settings.json' ) ,
155+ ]
156+ }
157+
158+ export type HooksCheckOverrides = { projectRoot ?: string ; homeDir ?: string }
159+
160+ function checkHooksConfig ( overrides : HooksCheckOverrides = { } ) : Check {
161+ const projectRoot = overrides . projectRoot ?? process . cwd ( )
162+ // Dedupe by resolved path: when the project lives inside the home dir,
163+ // several candidates can resolve to the same settings file.
164+ const seen = new Set < string > ( )
165+ const candidates = SETTINGS_CANDIDATES ( projectRoot , overrides . homeDir ) . filter ( ( candidate ) => {
166+ const resolved = path . resolve ( candidate )
167+ if ( seen . has ( resolved ) ) return false
168+ seen . add ( resolved )
169+ return true
170+ } )
171+
172+ let configured = 0
173+ const problems : string [ ] = [ ]
174+
175+ for ( const candidate of candidates ) {
176+ let raw : string
177+ try {
178+ raw = fs . readFileSync ( candidate , 'utf-8' )
179+ } catch {
180+ continue
181+ }
182+ try {
183+ const parsed : unknown = JSON . parse ( raw )
184+ const hooks =
185+ parsed && typeof parsed === 'object' && 'hooks' in parsed
186+ ? ( parsed as { hooks ?: unknown } ) . hooks
187+ : undefined
188+ if ( hooks === undefined ) continue
189+ if ( hooks && typeof hooks === 'object' ) {
190+ configured += Object . keys ( hooks as Record < string , unknown > ) . length
191+ } else {
192+ problems . push ( candidate )
193+ }
194+ } catch {
195+ problems . push ( candidate )
196+ }
197+ }
198+
199+ if ( problems . length > 0 ) {
200+ return {
201+ name : 'hooks config' ,
202+ status : 'warn' ,
203+ detail : `${ problems . length } settings file(s) with invalid JSON` ,
204+ hint : `Fix or remove: ${ problems [ 0 ] } ${ problems . length > 1 ? ` (+${ problems . length - 1 } more)` : '' } ` ,
205+ }
206+ }
207+ if ( configured === 0 ) {
208+ return {
209+ name : 'hooks config' ,
210+ status : 'ok' ,
211+ detail : 'none configured (see docs/hooks.md)' ,
212+ }
213+ }
214+ return {
215+ name : 'hooks config' ,
216+ status : 'ok' ,
217+ detail : `${ configured } event type(s) configured` ,
218+ }
219+ }
220+
221+ const SKILLS_DIRS = ( projectRoot : string , homeDir : string ) : string [ ] => [
222+ path . join ( projectRoot , '.agents' , 'skills' ) ,
223+ path . join ( projectRoot , '.claude' , 'skills' ) ,
224+ path . join ( projectRoot , '.levelcode' , 'skills' ) ,
225+ path . join ( homeDir , '.claude' , 'skills' ) ,
226+ path . join ( homeDir , '.agents' , 'skills' ) ,
227+ path . join ( homeDir , '.config' , 'levelcode' , 'skills' ) ,
228+ path . join ( homeDir , '.levelcode' , 'skills' ) ,
229+ ]
230+
231+ function countSkillsIn ( dir : string ) : number {
232+ try {
233+ return fs
234+ . readdirSync ( dir , { withFileTypes : true } )
235+ . filter (
236+ ( e ) => e . isDirectory ( ) && fs . existsSync ( path . join ( dir , e . name , 'SKILL.md' ) ) ,
237+ ) . length
238+ } catch {
239+ return 0
240+ }
241+ }
242+
243+ export type SkillsCheckOverrides = { projectRoot ?: string ; homeDir ?: string }
244+
245+ function checkSkills ( overrides : SkillsCheckOverrides = { } ) : Check {
246+ const projectRoot = overrides . projectRoot ?? process . cwd ( )
247+ const homeDir = overrides . homeDir ?? os . homedir ( )
248+
249+ // Dedupe by resolved path: project and home lists overlap when the
250+ // project lives inside the home directory.
251+ const seen = new Set < string > ( )
252+ const uniqueDirs = SKILLS_DIRS ( projectRoot , homeDir ) . filter ( ( dir ) => {
253+ const resolved = path . resolve ( dir )
254+ if ( seen . has ( resolved ) ) return false
255+ seen . add ( resolved )
256+ return true
257+ } )
258+
259+ let total = 0
260+ const sources : string [ ] = [ ]
261+ for ( const dir of uniqueDirs ) {
262+ const n = countSkillsIn ( dir )
263+ if ( n > 0 ) {
264+ total += n
265+ sources . push ( dir )
266+ }
267+ }
268+
269+ if ( total === 0 ) {
270+ return {
271+ name : 'skills' ,
272+ status : 'ok' ,
273+ detail : 'none installed (.agents/skills/<name>/SKILL.md)' ,
274+ }
275+ }
276+ return {
277+ name : 'skills' ,
278+ status : 'ok' ,
279+ detail : `${ total } skill(s) in ${ sources . length } location(s)` ,
280+ }
281+ }
282+
139283function checkSandboxSupport ( ) : Check {
140284 if ( process . platform === 'darwin' ) {
141285 return { name : 'sandbox (Seatbelt)' , status : 'ok' , detail : 'macOS sandbox-exec available' }
@@ -158,13 +302,20 @@ function checkSandboxSupport(): Check {
158302 return { name : 'sandbox' , status : 'warn' , detail : `unknown platform ${ process . platform } ` }
159303}
160304
161- export function runDoctorChecks ( ) : Check [ ] {
305+ export type DoctorCheckOverrides = {
306+ projectRoot ?: string
307+ homeDir ?: string
308+ }
309+
310+ export function runDoctorChecks ( overrides : DoctorCheckOverrides = { } ) : Check [ ] {
162311 return [
163312 checkNodeCompat ( ) ,
164313 checkProviderKeys ( ) ,
165314 checkConfigDir ( ) ,
315+ checkHooksConfig ( overrides ) ,
316+ checkSkills ( overrides ) ,
166317 checkRipgrep ( ) ,
167- checkGit ( ) ,
318+ checkGit ( overrides ) ,
168319 checkSandboxSupport ( ) ,
169320 ]
170321}
0 commit comments