From 842d5aa60db49d8a48a1c44e0be78c9e245ad979 Mon Sep 17 00:00:00 2001 From: Bastian Venegas Date: Tue, 10 Mar 2026 19:03:40 -0300 Subject: [PATCH] fix: expand tilde in project path input Replace the Effect-based expandHomePath with a plain synchronous expandTilde using regex replacement. Apply it in resolveThreadWorkspaceCwd (read-path for legacy data), resolveStateDir, and normalizeProjectWorkspaceRoot (write-path, previously using expandHomePath). Co-Authored-By: Claude Opus 4.6 --- apps/server/src/checkpointing/Utils.ts | 5 ++++- apps/server/src/os-jank.test.ts | 31 ++++++++++++++++++++++++++ apps/server/src/os-jank.ts | 15 ++++--------- apps/server/src/wsServer.ts | 4 ++-- 4 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 apps/server/src/os-jank.test.ts diff --git a/apps/server/src/checkpointing/Utils.ts b/apps/server/src/checkpointing/Utils.ts index 3cd92f8510..834d00c0d2 100644 --- a/apps/server/src/checkpointing/Utils.ts +++ b/apps/server/src/checkpointing/Utils.ts @@ -1,5 +1,6 @@ import { Encoding } from "effect"; import { CheckpointRef, ProjectId, type ThreadId } from "@t3tools/contracts"; +import { expandTilde } from "../os-jank.js"; export const CHECKPOINT_REFS_PREFIX = "refs/t3/checkpoints"; @@ -24,5 +25,7 @@ export function resolveThreadWorkspaceCwd(input: { return worktreeCwd; } - return input.projects.find((project) => project.id === input.thread.projectId)?.workspaceRoot; + const raw = input.projects.find((project) => project.id === input.thread.projectId) + ?.workspaceRoot; + return raw ? expandTilde(raw) : undefined; } diff --git a/apps/server/src/os-jank.test.ts b/apps/server/src/os-jank.test.ts new file mode 100644 index 0000000000..b038ed066c --- /dev/null +++ b/apps/server/src/os-jank.test.ts @@ -0,0 +1,31 @@ +import * as OS from "node:os"; +import { describe, expect, it } from "vitest"; +import { expandTilde } from "./os-jank.js"; + +describe("expandTilde", () => { + const home = OS.homedir(); + + it("expands bare ~", () => { + expect(expandTilde("~")).toBe(home); + }); + + it("expands ~/ prefix", () => { + expect(expandTilde("~/projects/foo")).toBe(`${home}/projects/foo`); + }); + + it("expands ~\\ prefix (Windows)", () => { + expect(expandTilde("~\\projects\\foo")).toBe(`${home}\\projects\\foo`); + }); + + it("does not expand ~ in the middle of a path", () => { + expect(expandTilde("/home/~user/foo")).toBe("/home/~user/foo"); + }); + + it("returns absolute paths unchanged", () => { + expect(expandTilde("/usr/local/bin")).toBe("/usr/local/bin"); + }); + + it("returns empty string unchanged", () => { + expect(expandTilde("")).toBe(""); + }); +}); diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts index 586aca6f79..ee6225563e 100644 --- a/apps/server/src/os-jank.ts +++ b/apps/server/src/os-jank.ts @@ -16,21 +16,14 @@ export function fixPath(): void { } } -export const expandHomePath = Effect.fn(function* (input: string) { - const { join } = yield* Path.Path; - if (input === "~") { - return OS.homedir(); - } - if (input.startsWith("~/") || input.startsWith("~\\")) { - return join(OS.homedir(), input.slice(2)); - } - return input; -}); +export function expandTilde(pathStr: string): string { + return pathStr.replace(/^~(?=$|[\\/])/, OS.homedir()); +} export const resolveStateDir = Effect.fn(function* (raw: string | undefined) { const { join, resolve } = yield* Path.Path; if (!raw || raw.trim().length === 0) { return join(OS.homedir(), ".t3", "userdata"); } - return resolve(yield* expandHomePath(raw.trim())); + return resolve(expandTilde(raw.trim())); }); diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 2c10b7cabe..45c386e45f 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -73,7 +73,7 @@ import { } from "./attachmentStore.ts"; import { parseBase64DataUrl } from "./imageMime.ts"; import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts"; -import { expandHomePath } from "./os-jank.ts"; +import { expandTilde } from "./os-jank.ts"; /** * ServerShape - Service API for server lifecycle control. @@ -298,7 +298,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< readonly command: ClientOrchestrationCommand; }) { const normalizeProjectWorkspaceRoot = Effect.fnUntraced(function* (workspaceRoot: string) { - const normalizedWorkspaceRoot = path.resolve(yield* expandHomePath(workspaceRoot.trim())); + const normalizedWorkspaceRoot = path.resolve(expandTilde(workspaceRoot.trim())); const workspaceStat = yield* fileSystem .stat(normalizedWorkspaceRoot) .pipe(Effect.catch(() => Effect.succeed(null)));