namespace ClaudeDo.Data; public static class Paths { /// /// Expands a leading "~" or "%USERPROFILE%" and returns an absolute path. /// Relative paths are resolved against (default: current directory). /// public static string Expand(string path, string? baseDir = null) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Path must not be empty.", nameof(path)); var expanded = System.Environment.ExpandEnvironmentVariables(path); if (expanded.StartsWith("~", StringComparison.Ordinal)) { var home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile); expanded = home + expanded[1..]; } if (!Path.IsPathRooted(expanded)) expanded = Path.GetFullPath(expanded, baseDir ?? System.Environment.CurrentDirectory); return Path.GetFullPath(expanded); } /// /// Strips trailing directory separators off a path bound for a Windows command line. Windows /// argv rules read \" as an escaped quote, so a quoted token ending in '\' never closes /// ("C:\repo\" parses as C:\repo") and either the path itself or every argument /// after it is corrupted. A bare root ("C:\", "/") is all separator and is returned untouched; /// so is null/blank. /// public static string? TrimTrailingSeparator(string? path) { if (string.IsNullOrWhiteSpace(path)) return path; var trimmed = path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); return trimmed.Length == 0 || trimmed.EndsWith(':') ? path : trimmed; } /// ~/.todo-app — parent directory for db, logs, config, sandbox, worktrees. public static string AppDataRoot() => Expand("~/.todo-app"); }