A list working_dir stored as "C:\Dev\Tests\StaplerTracking\" broke every consumer that puts it on a Windows command line: argv rules read \" as an escaped quote, so the token never closes. "Open in terminal" passed wt.exe a starting directory of C:\Dev\Tests\StaplerTracking" and it failed with 0x8007010b; the same data had already corrupted the ConPTY list handler's arg list in August. Paths.TrimTrailingSeparator is now the single helper (replacing the copies in InteractiveLaunchSpecService and ClaudeHelpLauncher) and ListRepository applies it on Add/Update, which covers every writer: UI create, repo import, hub UpdateList, and MCP CreateList/UpdateList. OpenInTerminal also switches to ArgumentList so its quoting is correct regardless of what's stored.
48 lines
1.9 KiB
C#
48 lines
1.9 KiB
C#
namespace ClaudeDo.Data;
|
|
|
|
public static class Paths
|
|
{
|
|
/// <summary>
|
|
/// Expands a leading "~" or "%USERPROFILE%" and returns an absolute path.
|
|
/// Relative paths are resolved against <paramref name="baseDir"/> (default: current directory).
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Strips trailing directory separators off a path bound for a Windows command line. Windows
|
|
/// argv rules read <c>\"</c> as an escaped quote, so a quoted token ending in '\' never closes
|
|
/// (<c>"C:\repo\"</c> parses as <c>C:\repo"</c>) 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>~/.todo-app — parent directory for db, logs, config, sandbox, worktrees.</summary>
|
|
public static string AppDataRoot() =>
|
|
Expand("~/.todo-app");
|
|
}
|