( subdir: NcodeConfigDirectory, cwd: string, )
| 276 | * @returns Array of directory paths containing .claude/.ncode subdirs, from most specific (cwd) to least specific |
| 277 | */ |
| 278 | export function getProjectDirsUpToHome( |
| 279 | subdir: NcodeConfigDirectory, |
| 280 | cwd: string, |
| 281 | ): string[] { |
| 282 | const home = resolve(homedir()).normalize('NFC') |
| 283 | const gitRoot = resolveStopBoundary(cwd) |
| 284 | let current = resolve(cwd) |
| 285 | const dirs: string[] = [] |
| 286 | |
| 287 | // Traverse from current directory up to git root (or home if not in a git repo) |
| 288 | while (true) { |
| 289 | // Stop if we've reached the home directory (don't check it, as it's loaded separately as userDir) |
| 290 | // Use normalized comparison to handle Windows drive letter casing (C:\ vs c:\) |
| 291 | if ( |
| 292 | normalizePathForComparison(current) === normalizePathForComparison(home) |
| 293 | ) { |
| 294 | break |
| 295 | } |
| 296 | |
| 297 | // Filter to existing dirs. This is a perf filter (avoids spawning |
| 298 | // ripgrep on non-existent dirs downstream) and the worktree fallback |
| 299 | // in loadMarkdownFilesForSubdir relies on it. statSync + explicit error |
| 300 | // handling instead of existsSync — re-throws unexpected errors rather |
| 301 | // than silently swallowing them. Downstream loadMarkdownFiles handles |
| 302 | // the TOCTOU window (dir disappearing before read) gracefully. |
| 303 | for (const configDir of getExistingProjectOrManagedDirs(current, subdir)) { |
| 304 | dirs.push(configDir) |
| 305 | } |
| 306 | // Codex-aligned cross-vendor ancestor walk: per-directory |
| 307 | // `<dir>/.agents/skills` matching `repo_agents_skill_roots`. Skills-only; |
| 308 | // no recursive whole-repo scan. |
| 309 | if (subdir === 'skills') { |
| 310 | pushIfExistingDir(dirs, join(current, '.agents', subdir)) |
| 311 | } |
| 312 | |
| 313 | // Stop after processing the git root directory - this prevents commands from parent |
| 314 | // directories outside the repository from appearing in the project |
| 315 | if ( |
| 316 | gitRoot && |
| 317 | normalizePathForComparison(current) === |
| 318 | normalizePathForComparison(gitRoot) |
| 319 | ) { |
| 320 | break |
| 321 | } |
| 322 | |
| 323 | // Move to parent directory |
| 324 | const parent = dirname(current) |
| 325 | |
| 326 | // Safety check: if parent is the same as current, we've reached the root |
| 327 | if (parent === current) { |
| 328 | break |
| 329 | } |
| 330 | |
| 331 | current = parent |
| 332 | } |
| 333 | |
| 334 | return dirs |
| 335 | } |
no test coverage detected