88 lines
3.4 KiB
TypeScript
88 lines
3.4 KiB
TypeScript
/**
|
|
* Shared path utilities for CLAUDE.md file generation
|
|
*
|
|
* These utilities handle path normalization and matching, particularly
|
|
* for comparing absolute and relative paths in folder CLAUDE.md generation.
|
|
*
|
|
* @see Issue #794 - Path format mismatch causes folder CLAUDE.md files to show "No recent activity"
|
|
*/
|
|
|
|
/**
|
|
* Normalize path separators to forward slashes, collapse consecutive slashes,
|
|
* and remove trailing slashes.
|
|
*
|
|
* @example
|
|
* normalizePath('app\\api\\router.py') // 'app/api/router.py'
|
|
* normalizePath('app//api///router.py') // 'app/api/router.py'
|
|
* normalizePath('app/api/') // 'app/api'
|
|
*/
|
|
export function normalizePath(p: string): string {
|
|
return p.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/+$/, '');
|
|
}
|
|
|
|
/**
|
|
* Check if a file is a direct child of a folder (not in a subfolder).
|
|
*
|
|
* Handles path format mismatches where folderPath may be absolute but
|
|
* filePath is stored as relative in the database.
|
|
*
|
|
* NOTE: This uses suffix matching which assumes both paths are relative to
|
|
* the same project root. It may produce false positives if used across
|
|
* different project roots, but this is mitigated by project-scoped queries.
|
|
*
|
|
* @param filePath - Path to the file (e.g., "app/api/router.py" or "/Users/x/project/app/api/router.py")
|
|
* @param folderPath - Path to the folder (e.g., "app/api" or "/Users/x/project/app/api")
|
|
* @returns true if file is directly in folder, false if in a subfolder or different folder
|
|
*
|
|
* @example
|
|
* // Same format (both relative)
|
|
* isDirectChild('app/api/router.py', 'app/api') // true
|
|
* isDirectChild('app/api/v1/router.py', 'app/api') // false (in subfolder)
|
|
*
|
|
* @example
|
|
* // Mixed format (absolute folder, relative file) - fixes #794
|
|
* isDirectChild('app/api/router.py', '/Users/dev/project/app/api') // true
|
|
*/
|
|
export function isDirectChild(filePath: string, folderPath: string): boolean {
|
|
const normFile = normalizePath(filePath);
|
|
const normFolder = normalizePath(folderPath);
|
|
|
|
// Strategy 1: Direct prefix match (both paths in same format)
|
|
if (normFile.startsWith(normFolder + '/')) {
|
|
const remainder = normFile.slice(normFolder.length + 1);
|
|
return !remainder.includes('/');
|
|
}
|
|
|
|
// Strategy 2: Handle absolute folderPath with relative filePath
|
|
// e.g., folderPath="/Users/x/project/app/api" and filePath="app/api/router.py"
|
|
const folderSegments = normFolder.split('/');
|
|
const fileSegments = normFile.split('/');
|
|
|
|
// Handle bare filenames (no directory component, e.g. stored as "dashboard.html").
|
|
// These are root-level files and are a direct child only of the root folder.
|
|
// Fixes #1514: bare filenames stored in DB were never matched by any folder query.
|
|
if (fileSegments.length < 2) {
|
|
return normFolder === '' || normFolder === '.';
|
|
}
|
|
|
|
const fileDir = fileSegments.slice(0, -1).join('/'); // Directory part of file
|
|
const fileName = fileSegments[fileSegments.length - 1]; // Actual filename
|
|
|
|
// Check if folder path ends with the file's directory path
|
|
if (normFolder.endsWith('/' + fileDir) || normFolder === fileDir) {
|
|
// File is a direct child (no additional subdirectories)
|
|
return !fileName.includes('/');
|
|
}
|
|
|
|
// Check if file's directory is contained at the end of folder path
|
|
// by progressively checking suffixes
|
|
for (let i = 0; i < folderSegments.length; i++) {
|
|
const folderSuffix = folderSegments.slice(i).join('/');
|
|
if (folderSuffix === fileDir) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|