import { homedir } from 'os' import path from 'path'; import { logger } from './logger.js'; import { detectWorktree } from './worktree.js'; /** * Expand leading ~ to the user's home directory. * Handles "~", "~/", and "~/subpath" but not "~user/" (which is rare in cwd). */ function expandTilde(p: string): string { if (p === '~' || p.startsWith('~/')) { return p.replace(/^~/, homedir()) } return p } /** * Extract project name from working directory path * Handles edge cases: null/undefined cwd, drive roots, trailing slashes, unexpanded ~ * * @param cwd - Current working directory (absolute path, or ~-prefixed path) * @returns Project name or "unknown-project" if extraction fails */ export function getProjectName(cwd: string | null | undefined): string { if (!cwd || cwd.trim() === '') { logger.warn('PROJECT_NAME', 'Empty cwd provided, using fallback', { cwd }); return 'unknown-project'; } // Expand leading ~ before path operations const expanded = expandTilde(cwd) // Extract basename (handles trailing slashes automatically) const basename = path.basename(expanded); // Edge case: Drive roots on Windows (C:\, J:\) or Unix root (/) // path.basename('C:\') returns '' (empty string) if (basename === '') { // Extract drive letter on Windows, or use 'root' on Unix const isWindows = process.platform === 'win32'; if (isWindows) { const driveMatch = cwd.match(/^([A-Z]):\\/i); if (driveMatch) { const driveLetter = driveMatch[1].toUpperCase(); const projectName = `drive-${driveLetter}`; logger.info('PROJECT_NAME', 'Drive root detected', { cwd, projectName }); return projectName; } } logger.warn('PROJECT_NAME', 'Root directory detected, using fallback', { cwd }); return 'unknown-project'; } return basename; } /** * Project context with worktree awareness */ export interface ProjectContext { /** The current project name (worktree or main repo) */ primary: string; /** Parent project name if in a worktree, null otherwise */ parent: string | null; /** True if currently in a worktree */ isWorktree: boolean; /** All projects to query: [primary] for main repo, [parent, primary] for worktree */ allProjects: string[]; } /** * Get project context with worktree detection. * * When in a worktree, returns both the worktree project name and parent project name * for unified timeline queries. * * @param cwd - Current working directory (absolute path) * @returns ProjectContext with worktree info */ export function getProjectContext(cwd: string | null | undefined): ProjectContext { const primary = getProjectName(cwd); if (!cwd) { return { primary, parent: null, isWorktree: false, allProjects: [primary] }; } const expandedCwd = expandTilde(cwd); const worktreeInfo = detectWorktree(expandedCwd); if (worktreeInfo.isWorktree && worktreeInfo.parentProjectName) { // In a worktree: include parent first for chronological ordering return { primary, parent: worktreeInfo.parentProjectName, isWorktree: true, allProjects: [worktreeInfo.parentProjectName, primary] }; } return { primary, parent: null, isWorktree: false, allProjects: [primary] }; }