From bad2dd380ae0a43715a75cf7cd17c97270375c07 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:56:12 -0700 Subject: [PATCH] refactor: home directory comes from a HostProcessHomeDirectory reference Add HostProcessHomeDirectory to @t3tools/shared/hostProcess (default NodeOS.homedir()) and yield it wherever Effect code used to call NodeOS.homedir() directly, so the dependency is visible and swappable in tests. - expandHomePath(value, home) takes the home directory explicitly. - mergeProviderInstanceEnvironment is an Effect.fn that yields the reference; callers yield* it. - Plain helpers reached from Effect code (worktree directory resolution, terminal spawn env, Antigravity user home, ACP stderr sanitizing, Grok plan path host, desktop snapshot config paths) take the home from their Effect caller. - Tests that mocked os.homedir provide the reference instead. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/app/DesktopApp.ts | 5 +- .../src/app/DesktopPreReadyPlatform.ts | 26 ++++---- apps/desktop/src/ipc/methods/window.ts | 4 +- .../snapShot/CaptureShortcutConfig.test.ts | 16 ++--- .../src/snapShot/CaptureShortcutConfig.ts | 9 +-- apps/desktop/src/snapShot/DesktopSnapShot.ts | 14 ++-- .../src/snapShot/HyprlandSnapShot.test.ts | 2 +- apps/desktop/src/snapShot/HyprlandSnapShot.ts | 3 +- apps/server/src/assets/AssetAccess.test.ts | 14 +--- apps/server/src/assets/AssetAccess.ts | 3 +- apps/server/src/cli/app.test.ts | 22 ++----- .../Adapters/ClaudeAdapterV2.ts | 6 +- .../Adapters/CodexAdapterV2.ts | 6 +- .../Adapters/GrokAdapterV2.test.ts | 36 ++++++---- .../orchestration-v2/workflowScriptQuery.ts | 9 +-- apps/server/src/os-jank.ts | 13 ++-- .../server/src/project/AgentSessionScanner.ts | 24 ++++--- apps/server/src/provider/CodexProvider.ts | 5 +- .../src/provider/Drivers/AntigravityDriver.ts | 10 ++- .../Drivers/AntigravitySkills.test.ts | 28 ++++++-- .../src/provider/Drivers/AntigravitySkills.ts | 7 +- .../src/provider/Drivers/AntigravityUsage.ts | 7 +- .../src/provider/Drivers/ClaudeDriver.ts | 5 +- .../server/src/provider/Drivers/ClaudeHome.ts | 8 +-- .../src/provider/Drivers/ClaudeSkills.ts | 8 +-- .../src/provider/Drivers/CodexDriver.ts | 5 +- .../src/provider/Drivers/CodexHomeLayout.ts | 14 ++-- .../provider/Drivers/CodexManagedProvider.ts | 2 +- .../src/provider/Drivers/claudeUsage.ts | 7 +- .../provider/ProviderInstanceRegistry.test.ts | 9 ++- .../src/provider/antigravityAuthSupport.ts | 9 ++- .../server/src/provider/claudeResetCredits.ts | 15 +++-- .../src/provider/providerInstallation.ts | 27 ++++---- apps/server/src/review/ReviewService.ts | 4 +- .../SourceControlRepositoryService.ts | 3 +- apps/server/src/storageCleanup.ts | 2 + apps/server/src/telemetry/Identify.ts | 7 +- apps/server/src/terminal/Manager.ts | 18 +++-- .../src/textGeneration/CodexTextGeneration.ts | 8 ++- apps/server/src/usage/UsageService.ts | 46 +++++++------ apps/server/src/vcs/GitVcsDriverCore.ts | 3 +- apps/server/src/workspace/WorkspaceEntries.ts | 9 ++- apps/server/src/workspace/WorkspacePaths.ts | 5 +- apps/server/src/worktreesDirectory.ts | 9 ++- .../src/server/adapter.ts | 2 +- .../src/server/driver.ts | 5 +- .../src/server/AcpSessionRuntime.ts | 5 +- .../provider-acp/src/server/stderr.test.ts | 3 + packages/provider-acp/src/server/stderr.ts | 6 +- .../src/server/instanceEnvironment.test.ts | 66 ++++++++++--------- .../src/server/instanceEnvironment.ts | 11 ++-- .../src/server/pathExpansion.test.ts | 19 +++--- .../provider-core/src/server/pathExpansion.ts | 16 ++--- .../src/server/CursorUsageAccounts.ts | 9 ++- .../provider-cursor/src/server/adapter.ts | 2 +- packages/provider-cursor/src/server/driver.ts | 2 +- packages/provider-cursor/src/server/skills.ts | 7 +- .../src/server/textGeneration.ts | 4 +- .../provider-cursor/src/server/usageLimits.ts | 6 +- packages/provider-grok/src/server/adapter.ts | 16 +++-- packages/provider-grok/src/server/driver.ts | 2 +- packages/provider-grok/src/server/usage.ts | 6 +- .../provider-grok/src/server/usageLimits.ts | 7 +- .../src/server/xaiAcpExtension.test.ts | 10 +-- .../src/server/xaiAcpExtension.ts | 11 ++-- packages/provider-muse/src/server/driver.ts | 6 +- .../provider-opencode/src/server/adapter.ts | 2 +- .../provider-opencode/src/server/driver.ts | 2 +- .../provider-opencode/src/server/usage.ts | 8 +-- .../src/server/usageLimits.ts | 9 ++- packages/provider-pi/src/server/adapter.ts | 2 +- packages/provider-pi/src/server/driver.ts | 2 +- packages/shared/src/hostProcess.ts | 7 ++ .../src/server/ForgejoCli.ts | 5 +- 74 files changed, 442 insertions(+), 308 deletions(-) diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index 944077d0e396..b76f6b4ba197 100644 --- a/apps/desktop/src/app/DesktopApp.ts +++ b/apps/desktop/src/app/DesktopApp.ts @@ -35,6 +35,7 @@ import * as DesktopUpdates from "../updates/DesktopUpdates.ts"; import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts"; import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts"; import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const DEFAULT_DESKTOP_BACKEND_PORT = 3773; const MAX_TCP_PORT = 65_535; @@ -284,7 +285,9 @@ const startup = Effect.gen(function* () { preReadyElectronOptions.linuxPasswordStoreCommandLine !== null; const linuxElectronOptions = environment.platform === "linux" && !hasCommandLinePasswordStore - ? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess() + ? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess( + yield* HostProcessHomeDirectory, + ) : preReadyElectronOptions.linux; if (linuxElectronOptions !== null && !hasCommandLinePasswordStore) { if ( diff --git a/apps/desktop/src/app/DesktopPreReadyPlatform.ts b/apps/desktop/src/app/DesktopPreReadyPlatform.ts index 9b73a5d825bf..2de132887aa2 100644 --- a/apps/desktop/src/app/DesktopPreReadyPlatform.ts +++ b/apps/desktop/src/app/DesktopPreReadyPlatform.ts @@ -1,13 +1,12 @@ // @effect-diagnostics nodeBuiltinImport:off - pre-ready Electron setup reads settings and prepares the Linux desktop entry synchronously before app services are available. import * as NodeFS from "node:fs"; -import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Electron from "electron"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as DesktopEarlyElectronStartup from "./DesktopEarlyElectronStartup.ts"; import { resolveDesktopAppBranding } from "./DesktopEnvironment.ts"; @@ -31,14 +30,15 @@ function readCommandLineSwitchValue( return value.length > 0 ? value : null; } -export const resolveEarlyLinuxElectronOptionsFromProcess = - (): DesktopEarlyElectronStartup.EarlyLinuxElectronOptions => - DesktopEarlyElectronStartup.resolveEarlyLinuxElectronOptions({ - env: process.env, - homeDirectory: NodeOS.homedir(), - joinPath: NodePath.posix.join, - readFileString: (path) => NodeFS.readFileSync(path, "utf8"), - }); +export const resolveEarlyLinuxElectronOptionsFromProcess = ( + homeDirectory: string, +): DesktopEarlyElectronStartup.EarlyLinuxElectronOptions => + DesktopEarlyElectronStartup.resolveEarlyLinuxElectronOptions({ + env: process.env, + homeDirectory, + joinPath: NodePath.posix.join, + readFileString: (path) => NodeFS.readFileSync(path, "utf8"), + }); export class DesktopPreReadyElectronOptions extends Context.Service< DesktopPreReadyElectronOptions, @@ -51,12 +51,14 @@ export class DesktopPreReadyElectronOptions extends Context.Service< /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const platform = yield* HostProcessPlatform; + const homeDirectory = yield* HostProcessHomeDirectory; return yield* Effect.sync((): DesktopPreReadyElectronOptions["Service"] => { const linuxPasswordStoreCommandLine = platform === "linux" ? readCommandLineSwitchValue(Electron.app.commandLine, "password-store") : null; - const linux = platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess() : null; + const linux = + platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess(homeDirectory) : null; if (linux !== null) { // The portal also requires a valid desktop entry. An AppImage update may @@ -64,7 +66,7 @@ export const make = Effect.gen(function* () { try { const applicationsDir = NodePath.posix.join( process.env.XDG_DATA_HOME?.trim() || - NodePath.posix.join(NodeOS.homedir(), ".local", "share"), + NodePath.posix.join(homeDirectory, ".local", "share"), "applications", ); NodeFS.mkdirSync(applicationsDir, { recursive: true }); diff --git a/apps/desktop/src/ipc/methods/window.ts b/apps/desktop/src/ipc/methods/window.ts index 64e0566d3e70..8e4ffebfefe7 100644 --- a/apps/desktop/src/ipc/methods/window.ts +++ b/apps/desktop/src/ipc/methods/window.ts @@ -16,7 +16,6 @@ import { import { WORKSPACE_IMAGE_PREVIEW_EXTENSIONS } from "@t3tools/shared/filePreview"; import { resolveEditorCommand } from "@t3tools/shared/editor"; import * as HostProcess from "@t3tools/shared/hostProcess"; -import * as NodeOS from "node:os"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Effect from "effect/Effect"; @@ -46,6 +45,7 @@ import { resolveWslPickFolderDefaultPath, wslUncPathToLinuxPath, } from "../../wsl/wslPathParsing.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const ContextMenuPosition = Schema.Struct({ x: Schema.Number, @@ -398,7 +398,7 @@ export const pickThemeFiles = DesktopIpc.makeIpcMethod({ // The VS Code extensions directory is the same dotfolder on Windows, // macOS, and Linux; when it is missing the picker opens wherever the // platform would by default. - const extensionsDir = path.join(NodeOS.homedir(), ".vscode", "extensions"); + const extensionsDir = path.join(yield* HostProcessHomeDirectory, ".vscode", "extensions"); const defaultPath = yield* fileSystem .exists(extensionsDir) .pipe(Effect.orElseSucceed(() => false)); diff --git a/apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts b/apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts index 0b9346a9f8cf..248a6874e48f 100644 --- a/apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts +++ b/apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts @@ -29,7 +29,7 @@ beforeEach(async () => { tools.validateNiri.mockReset().mockResolvedValue(undefined); tools.hyprlandBindings.mockReset().mockResolvedValue([]); tools.reloadHyprland.mockReset().mockResolvedValue(undefined); - setup = new CaptureShortcutConfig(tools); + setup = new CaptureShortcutConfig(NodeOS.homedir(), tools); }); afterEach(async () => { await NodeFSP.rm(directory, { recursive: true, force: true }); @@ -219,15 +219,15 @@ it("does not edit Omarchy's shipped defaults", async () => { }); it("honors NIRI_CONFIG and XDG_CONFIG_HOME", () => { expect( - niriCaptureConfigPath( - { NIRI_CONFIG: "/custom/test.kdl", XDG_CONFIG_HOME: "/config" }, - "/home/test", - ), + niriCaptureConfigPath("/home/test", { + NIRI_CONFIG: "/custom/test.kdl", + XDG_CONFIG_HOME: "/config", + }), ).toBe("/custom/test.kdl"); - expect(niriCaptureConfigPath({ XDG_CONFIG_HOME: "/config" }, "/home/test")).toBe( + expect(niriCaptureConfigPath("/home/test", { XDG_CONFIG_HOME: "/config" })).toBe( "/config/niri/config.kdl", ); - expect(niriCaptureConfigPath({}, "/home/test")).toBe("/home/test/.config/niri/config.kdl"); + expect(niriCaptureConfigPath("/home/test", {})).toBe("/home/test/.config/niri/config.kdl"); }); it("preserves a UTF-8 BOM through preview and apply", async () => { await NodeFSP.writeFile(path, "\uFEFFbinds {}\r\n"); @@ -251,7 +251,7 @@ it("the installed Niri parser accepts the generated edit and its relative includ NodePath.join(directory, "keys.kdl"), "binds {\n Ctrl+Alt+Q { quit; }\n}\n", ); - const checked = new CaptureShortcutConfig({ + const checked = new CaptureShortcutConfig(NodeOS.homedir(), { ...tools, validateNiri: async (file) => { await NodeUtil.promisify(NodeChildProcess.execFile)("niri", ["validate", "--config", file]); diff --git a/apps/desktop/src/snapShot/CaptureShortcutConfig.ts b/apps/desktop/src/snapShot/CaptureShortcutConfig.ts index edc4c300c101..8b86b416cdba 100644 --- a/apps/desktop/src/snapShot/CaptureShortcutConfig.ts +++ b/apps/desktop/src/snapShot/CaptureShortcutConfig.ts @@ -2,7 +2,6 @@ import * as NodeFSP from "node:fs/promises"; import * as NodeFS from "node:fs"; import * as NodePath from "node:path"; -import * as NodeOS from "node:os"; import * as NodeCrypto from "node:crypto"; import * as NodeChildProcess from "node:child_process"; import * as NodeUtil from "node:util"; @@ -39,7 +38,7 @@ const decodeHyprlandBindings = Schema.decodeUnknownSync( ), ); -export function niriCaptureConfigPath(env = process.env, home = NodeOS.homedir()) { +export function niriCaptureConfigPath(home: string, env = process.env) { return ( env.NIRI_CONFIG || NodePath.join(env.XDG_CONFIG_HOME || NodePath.join(home, ".config"), "niri", "config.kdl") @@ -99,8 +98,10 @@ export class CaptureShortcutConfig { | { preview: DesktopCaptureConfigPreview; target: Target; files: Snapshot[]; missing: string[] } | undefined; private applying = false; + private readonly home: string; private readonly tools: typeof defaultTools; - constructor(tools = defaultTools) { + constructor(home: string, tools = defaultTools) { + this.home = home; this.tools = tools; } @@ -166,7 +167,7 @@ export class CaptureShortcutConfig { if (include.path.includes("$") || /[*?[\]]/.test(include.path)) throw new Error("This config uses a dynamic include. Use manual setup in Advanced."); const path = include.path.startsWith("~/") - ? NodePath.join(NodeOS.homedir(), include.path.slice(2)) + ? NodePath.join(this.home, include.path.slice(2)) : NodePath.resolve(NodePath.dirname(file.path), include.path); let child: Snapshot; try { diff --git a/apps/desktop/src/snapShot/DesktopSnapShot.ts b/apps/desktop/src/snapShot/DesktopSnapShot.ts index f1c1c2a4908f..5718676f3b7a 100644 --- a/apps/desktop/src/snapShot/DesktopSnapShot.ts +++ b/apps/desktop/src/snapShot/DesktopSnapShot.ts @@ -88,6 +88,7 @@ import { snapShotShortcutRegistrationFailureMessage, snapShotShortcutSystemConflict, } from "./snapShot.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const MAX_CAPTURE_WIDTH = 2_560; const MAX_CAPTURE_HEIGHT = 1_600; @@ -727,6 +728,7 @@ export const make = Effect.gen(function* () { const runPromise = Effect.runPromiseWith(context); const captureDirectory = path.join(environment.stateDir, "snap-shots"); const linuxAppId = environment.linuxDesktopEntryName.replace(/\.desktop$/, ""); + const home = yield* HostProcessHomeDirectory; let shortcutVerified = false; const gnomeSetupPaths = { bundle: environment.isPackaged @@ -757,7 +759,7 @@ export const make = Effect.gen(function* () { dataHome: path.dirname(environment.linuxApplicationsDir), }; const shiftShortcutWorkerPath = path.join(__dirname, "snapShot", "GlobalShiftShortcutWorker.cjs"); - const shortcutConfig = new CaptureShortcutConfig(); + const shortcutConfig = new CaptureShortcutConfig(home); const accessibilityWorkerPath = path.join( __dirname, "snapShot", @@ -1184,7 +1186,7 @@ export const make = Effect.gen(function* () { shortcut, shortcutRegistered: false, shortcutBinding: niriCaptureBinding(linuxAppId), - shortcutConfigPath: niriCaptureConfigPath(), + shortcutConfigPath: niriCaptureConfigPath(home), shortcutActionRegistered: registered, shortcutMessage: registered ? "Set up the shortcut to add it to your Niri config." @@ -1474,8 +1476,8 @@ export const make = Effect.gen(function* () { const configPath = selectedPath ?? (desktop === "niri" - ? niriCaptureConfigPath() - : (await hyprlandCaptureShortcut(linuxAppId)).shortcutConfigPath); + ? niriCaptureConfigPath(home) + : (await hyprlandCaptureShortcut(linuxAppId, home)).shortcutConfigPath); return shortcutConfig.preview({ desktop, path: configPath, appId: linuxAppId }, request); }, catch: (cause) => @@ -1593,7 +1595,7 @@ export const make = Effect.gen(function* () { : undefined; const hyprlandShortcut = state.linuxBackend === "hyprland" - ? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId)) + ? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId, home)) : undefined; return { ...state, @@ -1609,7 +1611,7 @@ export const make = Effect.gen(function* () { : {}), ...hyprlandShortcut, ...(state.linuxBackend === "niri" - ? { shortcutConfigPath: niriCaptureConfigPath() } + ? { shortcutConfigPath: niriCaptureConfigPath(home) } : {}), ...(kdeHelper ? { diff --git a/apps/desktop/src/snapShot/HyprlandSnapShot.test.ts b/apps/desktop/src/snapShot/HyprlandSnapShot.test.ts index 3129d5bab7fd..fe55319ecea4 100644 --- a/apps/desktop/src/snapShot/HyprlandSnapShot.test.ts +++ b/apps/desktop/src/snapShot/HyprlandSnapShot.test.ts @@ -121,7 +121,7 @@ it.each([true, false])("uses the active config syntax and user bindings (Lua: %s NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"), "custom bindings", ); - const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", directory); + const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", "/home/test", directory); expect(result.shortcutConfigPath).toBe( NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"), ); diff --git a/apps/desktop/src/snapShot/HyprlandSnapShot.ts b/apps/desktop/src/snapShot/HyprlandSnapShot.ts index 2fc90a410a37..4dc57e44448d 100644 --- a/apps/desktop/src/snapShot/HyprlandSnapShot.ts +++ b/apps/desktop/src/snapShot/HyprlandSnapShot.ts @@ -29,7 +29,8 @@ function hyprlandCaptureBinding(appId: string, lua: boolean): string { /** Omarchy owns its defaults; instructions always point at a user-owned config. */ export async function hyprlandCaptureShortcut( appId: string, - configHome = process.env.XDG_CONFIG_HOME || NodePath.join(NodeOS.homedir(), ".config"), + home: string, + configHome = process.env.XDG_CONFIG_HOME || NodePath.join(home, ".config"), ) { const directory = NodePath.join(configHome, "hypr"); const exists = async (name: string) => diff --git a/apps/server/src/assets/AssetAccess.test.ts b/apps/server/src/assets/AssetAccess.test.ts index 61197efb6a50..6b6d4b2b95da 100644 --- a/apps/server/src/assets/AssetAccess.test.ts +++ b/apps/server/src/assets/AssetAccess.test.ts @@ -2,7 +2,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; import * as NodeFSP from "node:fs/promises"; -import * as NodeOS from "node:os"; import { AssetAccessError, AssetPreviewTypeValidationError, @@ -37,17 +36,13 @@ import { openMediaFile } from "./MediaFile.ts"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials"; import { githubMediaResponse } from "./GitHubMediaFetch.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) }; }); -vi.mock("node:os", async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual, homedir: vi.fn(actual.homedir) }; -}); - const layerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-asset-access-test-", }); @@ -299,8 +294,7 @@ describe("AssetAccess", () => { yield* fs.makeDirectory(path.dirname(filePath), { recursive: true }); yield* fs.writeFileString(filePath, "recording bytes"); const canonicalFile = yield* fs.realPath(filePath); - const homeSpy = vi.mocked(NodeOS.homedir).mockReturnValue(home); - try { + yield* Effect.gen(function* () { for (const workspaceRoot of [ path.join(home, "project"), path.join(directory, "srv", "project"), @@ -332,9 +326,7 @@ describe("AssetAccess", () => { expect(yield* Effect.promise(() => response.text())).toBe("recording bytes"); } } - } finally { - homeSpy.mockRestore(); - } + }).pipe(Effect.provideService(HostProcessHomeDirectory, home)); }).pipe(Effect.provide(layerTest)), ); diff --git a/apps/server/src/assets/AssetAccess.ts b/apps/server/src/assets/AssetAccess.ts index c9fbc064168c..d2de3e555736 100644 --- a/apps/server/src/assets/AssetAccess.ts +++ b/apps/server/src/assets/AssetAccess.ts @@ -58,6 +58,7 @@ import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts"; import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; import * as NativeAppIconResolver from "./NativeAppIconResolver.ts"; import { openMediaFile, readMediaFileHeader, type OpenMediaFile } from "./MediaFile.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; export const ASSET_ROUTE_PREFIX = "/api/assets"; @@ -469,7 +470,7 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i switch (input.resource._tag) { case "media-file": { - let requestedPath = expandHomePath(input.resource.path); + let requestedPath = expandHomePath(input.resource.path, yield* HostProcessHomeDirectory); if (!path.isAbsolute(requestedPath)) { if (!input.workspaceRoot) { return yield* new AssetWorkspaceContextNotFoundError({ resource: input.resource }); diff --git a/apps/server/src/cli/app.test.ts b/apps/server/src/cli/app.test.ts index 4bde751dcf41..0c35ab9f6941 100644 --- a/apps/server/src/cli/app.test.ts +++ b/apps/server/src/cli/app.test.ts @@ -9,6 +9,7 @@ import { it } from "@effect/vitest"; import type { DesktopAppActivationRequest } from "@t3tools/contracts"; import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl"; import { + HostProcessHomeDirectory, HostProcessPlatform, HostProcessUserId, HostProcessWorkingDirectory, @@ -19,18 +20,11 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import { Command } from "effect/cli"; -import { afterEach, describe, expect, vi } from "vite-plus/test"; +import { describe, expect } from "vite-plus/test"; import { makeCli } from "../binCli.ts"; import { PersistedServerRuntimeState } from "../serverRuntimeState.ts"; -vi.mock("node:os", async (importOriginal) => { - const os = await importOriginal(); - return { ...os, homedir: vi.fn(os.homedir) }; -}); - -afterEach(() => vi.mocked(NodeOS.homedir).mockReset()); - const encodeRuntimeState = Schema.encodeEffect(Schema.fromJsonString(PersistedServerRuntimeState)); const runCli = (args: ReadonlyArray, env: Record = {}) => @@ -296,7 +290,6 @@ describe("t3 app", () => { it.effect("prefers the installed desktop app when a dev desktop is also running", () => withTempDirectory("t3-app-preferred-test-", (root) => Effect.gen(function* () { - vi.mocked(NodeOS.homedir).mockReturnValue(root); const baseDir = NodePath.join(root, ".t3"); const desktop = yield* fakeDesktop({ baseDir }); const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" }); @@ -305,14 +298,13 @@ describe("t3 app", () => { expect(desktop.received).toHaveLength(1); expect(development.received).toHaveLength(0); - }).pipe(Effect.scoped), + }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), ), ); it.effect("finds the dev desktop when the default desktop socket is absent", () => withTempDirectory("t3-app-dev-test-", (root) => Effect.gen(function* () { - vi.mocked(NodeOS.homedir).mockReturnValue(root); const baseDir = NodePath.join(root, ".t3"); const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" }); @@ -321,14 +313,13 @@ describe("t3 app", () => { expect(development.received).toHaveLength(2); expect(yield* pathExists(baseDir)).toBe(false); - }).pipe(Effect.scoped), + }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), ), ); it.effect("never searches a dev state directory for an explicit T3 home", () => withTempDirectory("t3-app-explicit-test-", (root) => Effect.gen(function* () { - vi.mocked(NodeOS.homedir).mockReturnValue(root); const baseDir = NodePath.join(root, ".t3"); const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" }); @@ -338,7 +329,7 @@ describe("t3 app", () => { expect(flagError).toMatchObject({ _tag: "DesktopAppUnreachableError" }); expect(envError).toMatchObject({ _tag: "DesktopAppUnreachableError" }); expect(development.received).toHaveLength(0); - }).pipe(Effect.scoped), + }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), ), ); @@ -347,7 +338,6 @@ describe("t3 app", () => { (responseKind) => withTempDirectory("t3-app-response-test-", (root) => Effect.gen(function* () { - vi.mocked(NodeOS.homedir).mockReturnValue(root); const baseDir = NodePath.join(root, ".t3"); const desktop = yield* fakeDesktop({ baseDir, @@ -387,7 +377,7 @@ describe("t3 app", () => { cause: { message: "The desktop app response is invalid." }, }); } - }).pipe(Effect.scoped), + }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), ), ); }); diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index c7b1fb0d5819..33db150ec689 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -31,7 +31,7 @@ import type { WebSearchOutput, } from "@anthropic-ai/claude-agent-sdk/sdk-tools"; import { parseCliArgs } from "@t3tools/shared/cliArgs"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model"; import { CLAUDE_RESUME_COMPACTION_NEVER_ANSWER, @@ -8181,12 +8181,12 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( const queryRunner = yield* ClaudeAgentSdkQueryRunner; const serverConfig = yield* ServerConfig.ServerConfig; const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; - const baseEnvironment = mergeProviderInstanceEnvironment(environment, hostEnvironment); + const baseEnvironment = yield* mergeProviderInstanceEnvironment(environment, hostEnvironment); const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment); const path = yield* Path.Path; const crypto = yield* Crypto.Crypto; const binaryPath = yield* resolveClaudeSdkExecutablePath( - expandHomePath(config.binaryPath), + expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory), claudeEnvironment, ); return yield* makeClaudeAdapterV2({ diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index dbdbab32a74d..69eb0aa7f38e 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -30,7 +30,7 @@ import { type ProviderSetupError, } from "@t3tools/contracts"; import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import { dynamicToolTitle } from "@t3tools/shared/toolActivity"; import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; @@ -1582,14 +1582,14 @@ export const createCodexAdapterV2 = ( const settings = { ...config, enabled, - binaryPath: expandHomePath(config.binaryPath), + binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory), homePath: homeLayout.effectiveHomePath ?? "", } satisfies CodexSettings; return yield* makeCodexAdapterV2({ instanceId, settings, - environment: mergeProviderInstanceEnvironment(environment, hostEnvironment), + environment: yield* mergeProviderInstanceEnvironment(environment, hostEnvironment), clientFactory, crypto, fileSystem, diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts index 732bc6904f40..bdfc883aeb93 100644 --- a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts @@ -94,9 +94,12 @@ describe("acpSubagentStatusBlocksTurnSettlement", () => { describe("GrokAdapterV2 capabilities", () => { it("preserves Grok's rate-limit stop and distinguishes other prompt failures", () => { - const flavor = makeGrokAcpAdapterFlavor({ - makeRuntime: () => Effect.never, - } as unknown as Parameters[0]); + const flavor = makeGrokAcpAdapterFlavor( + { + makeRuntime: () => Effect.never, + } as unknown as Parameters[0], + "/home/ada", + ); const limit = flavor.promptFailure?.( new EffectAcpErrors.AcpRequestError({ code: xAiRateLimitedErrorCode, @@ -122,9 +125,12 @@ describe("GrokAdapterV2 capabilities", () => { }); it("wires hard Stop teardown but soft non-Stop interrupts in the constructor flavor", () => { - const flavor = makeGrokAcpAdapterFlavor({ - makeRuntime: () => Effect.never, - } as unknown as Parameters[0]); + const flavor = makeGrokAcpAdapterFlavor( + { + makeRuntime: () => Effect.never, + } as unknown as Parameters[0], + "/home/ada", + ); assert.isFalse(flavor.interruptPromptOnCancel); // User Stop (requestRuntimeRestart) keeps the hard process-group kill and @@ -139,9 +145,12 @@ describe("GrokAdapterV2 capabilities", () => { }); it("terminalizes only foreground tools under the actual Grok flavor", () => { - const flavor = makeGrokAcpAdapterFlavor({ - makeRuntime: () => Effect.never, - } as unknown as Parameters[0]); + const flavor = makeGrokAcpAdapterFlavor( + { + makeRuntime: () => Effect.never, + } as unknown as Parameters[0], + "/home/ada", + ); const foreground = { toolCallId: "foreground-1", title: "Terminal", @@ -284,9 +293,12 @@ describe("ACP permission policy", () => { }); describe("Grok permission prompts", () => { - const disposition = makeGrokAcpAdapterFlavor({ - makeRuntime: () => Effect.never, - } as unknown as Parameters[0]).permissionDisposition; + const disposition = makeGrokAcpAdapterFlavor( + { + makeRuntime: () => Effect.never, + } as unknown as Parameters[0], + "/home/ada", + ).permissionDisposition; // grok_auto_blocked_command replays Auto end to end. When an explicit policy // launches Grok asking instead, T3's policy still answers its prompts. diff --git a/apps/server/src/orchestration-v2/workflowScriptQuery.ts b/apps/server/src/orchestration-v2/workflowScriptQuery.ts index 184abc6147ce..a4aa736c669b 100644 --- a/apps/server/src/orchestration-v2/workflowScriptQuery.ts +++ b/apps/server/src/orchestration-v2/workflowScriptQuery.ts @@ -14,16 +14,16 @@ * never trusted beyond these checks. */ import * as NodeFSP from "node:fs/promises"; -import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import { OrchestrationGetWorkflowScriptError } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const SCRIPT_BYTE_CAP = 256 * 1024; -function scriptsRoot(): string { - return NodePath.join(NodeOS.homedir(), ".claude", "projects"); +function scriptsRoot(home: string): string { + return NodePath.join(home, ".claude", "projects"); } export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(function* (input: { @@ -38,8 +38,9 @@ export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")( }); } + const home = yield* HostProcessHomeDirectory; const root = yield* Effect.tryPromise({ - try: () => NodeFSP.realpath(scriptsRoot()), + try: () => NodeFSP.realpath(scriptsRoot(home)), catch: (cause) => new OrchestrationGetWorkflowScriptError({ reason: "root-unavailable", diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts index 18ddbc66c0c8..25c7dfdcb973 100644 --- a/apps/server/src/os-jank.ts +++ b/apps/server/src/os-jank.ts @@ -1,4 +1,8 @@ -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { + HostProcessEnvironment, + HostProcessHomeDirectory, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; import { listLoginShellCandidates, mergePathEntries, @@ -93,11 +97,12 @@ export const fixPath = Effect.fn("fixPath")(function* (): Effect.fn.Return< export const expandHomePath = Effect.fn(function* (input: string) { const { join } = yield* Path.Path; + const home = yield* HostProcessHomeDirectory; if (input === "~") { - return NodeOS.homedir(); + return home; } if (input.startsWith("~/") || input.startsWith("~\\")) { - return join(NodeOS.homedir(), input.slice(2)); + return join(home, input.slice(2)); } return input; }); @@ -105,7 +110,7 @@ export const expandHomePath = Effect.fn(function* (input: string) { export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) { const { join, resolve } = yield* Path.Path; if (!raw || raw.trim().length === 0) { - return join(NodeOS.homedir(), ".t3"); + return join(yield* HostProcessHomeDirectory, ".t3"); } return resolve(yield* expandHomePath(raw.trim())); }); diff --git a/apps/server/src/project/AgentSessionScanner.ts b/apps/server/src/project/AgentSessionScanner.ts index 0fe74152f1c3..5177a948d945 100644 --- a/apps/server/src/project/AgentSessionScanner.ts +++ b/apps/server/src/project/AgentSessionScanner.ts @@ -44,7 +44,11 @@ import { parseGitHubRepositoryNameWithOwnerFromRemoteUrl, parseOriginUrlFromGitConfig, } from "@t3tools/shared/git"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { + HostProcessEnvironment, + HostProcessHomeDirectory, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; import { normalizeProjectPathForComparison } from "@t3tools/shared/path"; import * as ServerConfig from "../config.ts"; @@ -631,7 +635,7 @@ export const make = Effect.gen(function* () { // must case fold. const foldWorktreeCase = (yield* HostProcessPlatform) === "win32"; const hostEnvironment = yield* HostProcessEnvironment; - const homeDir = NodeOS.homedir(); + const homeDir = yield* HostProcessHomeDirectory; // `/private/tmp` is what macOS reports for sessions started in `/tmp`. const excludedProjectRoots = new Set( [homeDir, NodeOS.tmpdir(), "/tmp", "/private/tmp"].map((directory) => @@ -912,13 +916,13 @@ export const make = Effect.gen(function* () { const resolveClaudeConfigDir = (homePath: string, environmentHome?: string): string => { const configured = homePath.trim(); if (configured.length > 0) { - return path.resolve(expandHomePath(configured)); + return path.resolve(expandHomePath(configured, homeDir)); } const fromEnvironment = environmentHome?.trim() ?? ""; if (fromEnvironment.length > 0) { - return path.resolve(expandHomePath(fromEnvironment)); + return path.resolve(expandHomePath(fromEnvironment, homeDir)); } - return path.join(NodeOS.homedir(), ".claude"); + return path.join(homeDir, ".claude"); }; const discoverClaudeTranscripts = Effect.fn("AgentSessionScanner.discoverClaudeTranscripts")( @@ -1217,7 +1221,7 @@ export const make = Effect.gen(function* () { const gitIdentities = new Map(); for (const candidate of raw) { - const expanded = expandHomePath(candidate.cwd.trim()); + const expanded = expandHomePath(candidate.cwd.trim(), homeDir); if (!path.isAbsolute(expanded)) continue; const resolved = path.resolve(expanded); if (isExcludedProjectPath(resolved)) continue; @@ -1281,7 +1285,7 @@ export const make = Effect.gen(function* () { ); const importedProjectsByRoot = new Map(); for (const project of importedProjects) { - const projectRoot = path.resolve(expandHomePath(project.workspaceRoot)); + const projectRoot = path.resolve(expandHomePath(project.workspaceRoot, homeDir)); importedProjectsByRoot.set(normalizeProjectPathForComparison(projectRoot), project); importedProjectsByRoot.set(yield* directoryIdentity(projectRoot), project); } @@ -1328,7 +1332,7 @@ export const make = Effect.gen(function* () { workspaceRoot: string, completedSources: ReadonlyArray, ) { - const root = path.resolve(expandHomePath(workspaceRoot)); + const root = path.resolve(expandHomePath(workspaceRoot, homeDir)); const realRoot = yield* fileSystem.realPath(root).pipe(Effect.orElseSucceed(() => root)); if (isExcludedProjectPath(root) || isExcludedProjectPath(realRoot)) return Stream.empty; const rootIdentity = yield* directoryIdentity(root); @@ -1343,7 +1347,7 @@ export const make = Effect.gen(function* () { readonly transcript: RawCandidate["transcripts"][number] & { readonly mtimeMs: number }; }> = []; for (const candidate of candidates) { - const expanded = expandHomePath(candidate.cwd.trim()); + const expanded = expandHomePath(candidate.cwd.trim(), homeDir); if (!path.isAbsolute(expanded)) continue; const resolved = path.resolve(expanded); if ((yield* directoryIdentity(resolved)) !== rootIdentity) continue; @@ -1439,7 +1443,7 @@ export const make = Effect.gen(function* () { if (snapshotCwd === null) { return Option.some({ _tag: "Skipped" }); } - const expandedCwd = expandHomePath(snapshotCwd.trim()); + const expandedCwd = expandHomePath(snapshotCwd.trim(), homeDir); if ( !path.isAbsolute(expandedCwd) || (yield* directoryIdentity(path.resolve(expandedCwd))) !== rootIdentity diff --git a/apps/server/src/provider/CodexProvider.ts b/apps/server/src/provider/CodexProvider.ts index 913a1983a107..a8895e21ae32 100644 --- a/apps/server/src/provider/CodexProvider.ts +++ b/apps/server/src/provider/CodexProvider.ts @@ -47,6 +47,7 @@ import { type CodexRateLimitSnapshot, type CodexResetCreditsSummary, } from "./codexUsageLimits.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import packageJson from "../../package.json" with { type: "json" }; const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError); const RATE_LIMITS_PROBE_TIMEOUT_MS = 3_000; @@ -374,7 +375,9 @@ export const withCodexAppServerClient = Effect.fn("withCodexAppServerClient")(fu // so `CODEX_HOME=~/.codex_work` would reach codex verbatim and trip // "CODEX_HOME points to '~/.codex_work', but that path does not exist". // Expand here for parity with `CodexTextGeneration`. - const resolvedHomePath = input.homePath ? expandHomePath(input.homePath) : undefined; + const resolvedHomePath = input.homePath + ? expandHomePath(input.homePath, yield* HostProcessHomeDirectory) + : undefined; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const environment = { ...input.environment, diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index c0812af2a6df..cdf938dbb4d0 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -1,7 +1,7 @@ import { withAgentDeviceEnvironment } from "@t3tools/provider-core/server/mcpSession"; import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; import { AntigravitySettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime"; import { NodeRuntimeUnavailableError, @@ -113,8 +113,12 @@ export const AntigravityDriver: ProviderDriver< gcpLocation: settings.gcpLocation, }; const authConfigIssue = antigravityAuthConfigIssue(auth); - const processEnvironment = mergeProviderInstanceEnvironment(environment); - const userHome = resolveAntigravityUserHome(yield* HostProcessPlatform, processEnvironment); + const processEnvironment = yield* mergeProviderInstanceEnvironment(environment); + const userHome = resolveAntigravityUserHome( + yield* HostProcessPlatform, + processEnvironment, + yield* HostProcessHomeDirectory, + ); const directories = yield* resolveAntigravityInstanceDirectories( host.paths.stateDir, instanceId, diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts index 180ef9968fac..f5bc8ec3f4d3 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts @@ -344,17 +344,35 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { it("resolves the home the agent expands ~ against", () => { assert.equal( - resolveAntigravityUserHome("linux", { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }), + resolveAntigravityUserHome( + "linux", + { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }, + "/home/fallback", + ), "/home/user", ); assert.equal( - resolveAntigravityUserHome("win32", { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }), + resolveAntigravityUserHome( + "win32", + { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }, + "/home/fallback", + ), "C:\\Users\\user", ); assert.equal( - resolveAntigravityUserHome("win32", { HOMEDRIVE: "D:", HOMEPATH: "\\Users\\alice" }), + resolveAntigravityUserHome( + "win32", + { HOMEDRIVE: "D:", HOMEPATH: "\\Users\\alice" }, + "/home/fallback", + ), "D:\\Users\\alice", ); - assert.equal(resolveAntigravityUserHome("darwin", { HOME: "/Users/a b " }), "/Users/a b "); - assert.equal(resolveAntigravityUserHome("darwin", { HOME: "" }).length > 0, true); + assert.equal( + resolveAntigravityUserHome("darwin", { HOME: "/Users/a b " }, "/home/fallback"), + "/Users/a b ", + ); + assert.equal( + resolveAntigravityUserHome("darwin", { HOME: "" }, "/home/fallback"), + "/home/fallback", + ); }); diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.ts b/apps/server/src/provider/Drivers/AntigravitySkills.ts index b11396467582..1bc4e370013f 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.ts @@ -1,5 +1,3 @@ -import * as NodeOS from "node:os"; - import type { ServerProviderSkill } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -18,15 +16,16 @@ import { parse as parseYamlDocument } from "yaml"; export function resolveAntigravityUserHome( platform: NodeJS.Platform, environment: NodeJS.ProcessEnv, + home: string, ): string { if (platform === "win32") { if (environment.USERPROFILE) return environment.USERPROFILE; if (environment.HOMEDRIVE && environment.HOMEPATH) { return `${environment.HOMEDRIVE}${environment.HOMEPATH}`; } - return NodeOS.homedir(); + return home; } - return environment.HOME || NodeOS.homedir(); + return environment.HOME || home; } /** diff --git a/apps/server/src/provider/Drivers/AntigravityUsage.ts b/apps/server/src/provider/Drivers/AntigravityUsage.ts index 7b629c857c9a..d99539b555c1 100644 --- a/apps/server/src/provider/Drivers/AntigravityUsage.ts +++ b/apps/server/src/provider/Drivers/AntigravityUsage.ts @@ -8,7 +8,6 @@ * * @module provider/Drivers/AntigravityUsage */ -import * as NodeOS from "node:os"; import { type AntigravitySettings, UsageReadError } from "@t3tools/contracts"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; @@ -18,7 +17,7 @@ import type { ProviderUsageReader, ProviderUsageScan, } from "@t3tools/provider-core/server/usage"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; @@ -50,7 +49,7 @@ const make = Effect.gen(function* () { /** `ANTIGRAVITY_DATA_DIR` (comma-separated) or the defaults, canonicalized. */ const dataRoots = Effect.gen(function* () { - const home = NodeOS.homedir(); + const home = yield* HostProcessHomeDirectory; const configured = hostEnvironment["ANTIGRAVITY_DATA_DIR"] ?.split(",") .map((value) => value.trim()) @@ -63,7 +62,7 @@ const make = Effect.gen(function* () { ]; const canonical = new Set(); for (const root of configured?.length ? configured : defaults) { - const resolved = path.resolve(expandHomePath(root)); + const resolved = path.resolve(expandHomePath(root, home)); canonical.add( yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)), ); diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index e9f37580215b..9a43be65aac4 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -71,6 +71,7 @@ import { makeClaudeContinuationGroupKey, resolveClaudeHomePath, } from "./ClaudeHome.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); const DRIVER_KIND = ProviderDriverKind.make("claudeAgent"); @@ -130,7 +131,7 @@ export const ClaudeDriver: ProviderDriver { const path = yield* Path.Path; + const home = yield* HostProcessHomeDirectory; const homePath = config.homePath.trim(); if (homePath.length > 0) { - return path.resolve(expandHomePath(homePath)); + return path.resolve(expandHomePath(homePath, home)); } // Inherited env vars are not shell-expanded, so a literal `~` stays literal. const inherited = environment?.CLAUDE_CONFIG_DIR?.trim() ?? ""; if (inherited.length > 0) { return path.resolve(inherited); } - return path.resolve(path.join(NodeOS.homedir(), ".claude")); + return path.resolve(path.join(home, ".claude")); }); export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function* ( diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.ts b/apps/server/src/provider/Drivers/ClaudeSkills.ts index 3ef7d925e031..3082f53b0280 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.ts @@ -13,14 +13,13 @@ * * @module provider/Drivers/ClaudeSkills */ -import * as NodeOS from "node:os"; import type { ClaudeSettings, ServerProviderSkill } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; import { parse as parseYamlDocument } from "yaml"; @@ -296,9 +295,10 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct cwd?: string, ): Effect.fn.Return { const path = yield* Path.Path; + const home = yield* HostProcessHomeDirectory; const homePath = config.homePath.trim(); if (homePath.length > 0) { - return path.resolve(expandHomePath(homePath)); + return path.resolve(expandHomePath(homePath, home)); } // No tilde expansion here: the spawned CLI receives this env var verbatim // (env vars are never shell-expanded), so a literal `~` must stay literal @@ -309,7 +309,7 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct if (environmentConfigDir.length > 0) { return cwd ? path.resolve(cwd, environmentConfigDir) : path.resolve(environmentConfigDir); } - return path.join(NodeOS.homedir(), ".claude"); + return path.join(home, ".claude"); }); /** diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index 32ffed8905b4..e1fadbcd175e 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -77,6 +77,7 @@ import { codexUsageReader } from "./codexUsage.ts"; import * as CodexInstallation from "../CodexInstallation.ts"; import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; import * as ServerEnvironment from "../../environment/ServerEnvironment.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const decodeCodexSettings = Schema.decodeSync(CodexSettings); const DRIVER_KIND = ProviderDriverKind.make("codex"); @@ -154,7 +155,7 @@ export const CodexDriver: ProviderDriver 0 - ? expandHomePath(value) - : path.join(NodeOS.homedir(), ".codex"); + value && value.trim().length > 0 ? expandHomePath(value, home) : path.join(home, ".codex"); return path.resolve(expanded); } @@ -45,7 +42,8 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi config: CodexSettings, ): Effect.fn.Return { const path = yield* Path.Path; - const sharedHomePath = resolveHomePath(path, config.homePath); + const home = yield* HostProcessHomeDirectory; + const sharedHomePath = resolveHomePath(path, home, config.homePath); const shadowHomePath = config.shadowHomePath.trim(); if (shadowHomePath.length === 0) { return { @@ -56,7 +54,7 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi }; } - const effectiveHomePath = path.resolve(expandHomePath(shadowHomePath)); + const effectiveHomePath = path.resolve(expandHomePath(shadowHomePath, home)); return { mode: "authOverlay", sharedHomePath, diff --git a/apps/server/src/provider/Drivers/CodexManagedProvider.ts b/apps/server/src/provider/Drivers/CodexManagedProvider.ts index a7181352b288..92c461390aea 100644 --- a/apps/server/src/provider/Drivers/CodexManagedProvider.ts +++ b/apps/server/src/provider/Drivers/CodexManagedProvider.ts @@ -36,7 +36,7 @@ export const makeManagedCodexProvider = Effect.fn("makeManagedCodexProvider")(fu instanceId, enabled, config, - environment: mergeProviderInstanceEnvironment(input.environment), + environment: yield* mergeProviderInstanceEnvironment(input.environment), }); const continuationIdentity = codexContinuationIdentity(runtime.homeLayout); const stamp = withInstanceIdentity({ diff --git a/apps/server/src/provider/Drivers/claudeUsage.ts b/apps/server/src/provider/Drivers/claudeUsage.ts index 155ebc6a33ec..38df3146e097 100644 --- a/apps/server/src/provider/Drivers/claudeUsage.ts +++ b/apps/server/src/provider/Drivers/claudeUsage.ts @@ -4,7 +4,6 @@ * * @module provider/Drivers/claudeUsage */ -import * as NodeOS from "node:os"; import type { ClaudeSettings } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; @@ -18,6 +17,7 @@ import { type TranscriptUsageFormat, type UsageRecord, } from "@t3tools/provider-core/server/usage"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; /** * Parses one line of a Claude Code transcript. @@ -111,10 +111,11 @@ export const claudeUsageReader: ProviderUsageReader = // An undecodable config has no trustworthy home to read. if (config === undefined) return []; const path = yield* Path.Path; + const homeDirectory = yield* HostProcessHomeDirectory; const configured = config.homePath.trim(); const home = configured - ? expandHomePath(configured) - : environment.CLAUDE_CONFIG_DIR?.trim() || path.join(NodeOS.homedir(), ".claude"); + ? expandHomePath(configured, homeDirectory) + : environment.CLAUDE_CONFIG_DIR?.trim() || path.join(homeDirectory, ".claude"); return [{ dir: path.resolve(home, "projects") }]; }), }; diff --git a/apps/server/src/provider/ProviderInstanceRegistry.test.ts b/apps/server/src/provider/ProviderInstanceRegistry.test.ts index 36db18edb3b9..7c6b191a8b84 100644 --- a/apps/server/src/provider/ProviderInstanceRegistry.test.ts +++ b/apps/server/src/provider/ProviderInstanceRegistry.test.ts @@ -39,7 +39,11 @@ import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderL import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; import type { GrokSettings } from "@t3tools/provider-grok/settings"; import type { CursorSettings } from "@t3tools/provider-cursor/settings"; -import { HostProcessPlatform, isHostWindows } from "@t3tools/shared/hostProcess"; +import { + HostProcessHomeDirectory, + HostProcessPlatform, + isHostWindows, +} from "@t3tools/shared/hostProcess"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -51,7 +55,6 @@ import { HttpClient, HttpClientResponse } from "effect/http"; import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; import * as AntigravityInstallation from "./AntigravityInstallation.ts"; import * as ServerConfig from "../config.ts"; -import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; import * as ServerSettings from "../serverSettings.ts"; import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts"; import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts"; @@ -153,7 +156,7 @@ const makeTildeProviderFixtures = Effect.fn( )(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const homePath = expandHomePath("~"); + const homePath = yield* HostProcessHomeDirectory; const fixtureDir = yield* fileSystem.makeTempDirectoryScoped({ directory: homePath, prefix: ".t3-provider-path-test-", diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index c4816709a2f6..4b0e769cde1d 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -2,7 +2,7 @@ import * as NodeFSP from "node:fs/promises"; import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; @@ -317,7 +317,12 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")( const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const platform = input.platform ?? (yield* HostProcessPlatform); const userHome = - input.userHome ?? resolveAntigravityUserHome(platform, input.baseEnv ?? process.env); + input.userHome ?? + resolveAntigravityUserHome( + platform, + input.baseEnv ?? process.env, + yield* HostProcessHomeDirectory, + ); const runtimeExecutablePath = input.runtimeExecutablePath ?? (yield* resolveNodeExecutable("Antigravity sign-in", input.baseEnv).pipe( diff --git a/apps/server/src/provider/claudeResetCredits.ts b/apps/server/src/provider/claudeResetCredits.ts index 5f8379e1c037..c37a0dfb2d8d 100644 --- a/apps/server/src/provider/claudeResetCredits.ts +++ b/apps/server/src/provider/claudeResetCredits.ts @@ -7,12 +7,11 @@ * * @module provider/claudeResetCredits */ -import * as NodeOS from "node:os"; import type { ProviderConsumeResetCreditOutcome, ServerProviderResetCredits, } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -190,10 +189,14 @@ export const readClaudeResetCredits = Effect.fn("readClaudeResetCredits")( ); /** The CLI keeps the account record beside its settings, or in the home directory by default. */ -export const claudeAccountConfigPath = (configDir: string | undefined) => - Effect.map(Path.Path, (path) => - configDir ? path.join(configDir, ".claude.json") : path.join(NodeOS.homedir(), ".claude.json"), - ); +export const claudeAccountConfigPath = Effect.fn("claudeAccountConfigPath")(function* ( + configDir: string | undefined, +) { + const path = yield* Path.Path; + return configDir + ? path.join(configDir, ".claude.json") + : path.join(yield* HostProcessHomeDirectory, ".claude.json"); +}); const CLAIM_OUTCOMES = { reset: "reset", diff --git a/apps/server/src/provider/providerInstallation.ts b/apps/server/src/provider/providerInstallation.ts index d888725b3ba9..ab7257553339 100644 --- a/apps/server/src/provider/providerInstallation.ts +++ b/apps/server/src/provider/providerInstallation.ts @@ -134,18 +134,21 @@ export const makeProviderInstallation = Effect.fn("makeProviderInstallation")(fu true, ); const entries = yield* readEntries(input.instanceId, "remove-install"); - const protectedPaths = yield* Effect.forEach(Object.values(entries), (entry) => { - if (!hasBinaryPath(entry.config) || !entry.config.binaryPath.trim()) { - return Effect.succeed([]); - } - const binaryPath = entry.config.binaryPath.trim(); - return resolveCommandPath(binaryPath, { - env: mergeProviderInstanceEnvironment(entry.environment), - }).pipe( - Effect.map((resolved) => [binaryPath, resolved]), - Effect.orElseSucceed(() => [binaryPath]), - ); - }); + const protectedPaths = yield* Effect.forEach( + Object.values(entries), + Effect.fnUntraced(function* (entry) { + if (!hasBinaryPath(entry.config) || !entry.config.binaryPath.trim()) { + return []; + } + const binaryPath = entry.config.binaryPath.trim(); + return yield* resolveCommandPath(binaryPath, { + env: yield* mergeProviderInstanceEnvironment(entry.environment), + }).pipe( + Effect.map((resolved) => [binaryPath, resolved]), + Effect.orElseSucceed(() => [binaryPath]), + ); + }), + ); yield* installation .remove(protectedPaths.flat()) .pipe(Effect.mapError(failure(input.instanceId))); diff --git a/apps/server/src/review/ReviewService.ts b/apps/server/src/review/ReviewService.ts index 7aee5847001f..ef7a0e31590f 100644 --- a/apps/server/src/review/ReviewService.ts +++ b/apps/server/src/review/ReviewService.ts @@ -20,6 +20,7 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as ServerSettings from "../serverSettings.ts"; import { isFilesystemRoot, managedWorktreesDirectories } from "../worktreesDirectory.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; export class ReviewService extends Context.Service< ReviewService, @@ -73,13 +74,14 @@ export const make = Effect.gen(function* () { const worktreesDirectories = yield* settings.getSettings.pipe( Effect.orElseSucceed(() => ({ worktreesDirectory: "", previousWorktreesDirectories: [] })), ); + const home = yield* HostProcessHomeDirectory; const [candidate, workspaceRoot, worktreesRoots] = yield* Effect.all([ canonicalizePath(cwd), canonicalizePath(config.cwd), // A managed root that cannot be resolved, or resolves to a filesystem // root through a symlink, is skipped rather than failing every review. Effect.forEach( - managedWorktreesDirectories(worktreesDirectories, config.worktreesDir, path), + managedWorktreesDirectories(worktreesDirectories, config.worktreesDir, path, home), (directory) => canonicalizePath(directory).pipe(Effect.orElseSucceed(() => null)), ).pipe( Effect.map((roots) => diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index 1bbca1e2b3d5..e595b4028cee 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -29,6 +29,7 @@ import { import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); const isSourceControlProviderError = Schema.is(SourceControlProviderError); const isBitbucketRepositoryLocatorError = Schema.is(BitbucketApi.BitbucketRepositoryLocatorError); @@ -213,7 +214,7 @@ export const make = Effect.gen(function* () { }); } - return path.resolve(expandHomePath(trimmed)); + return path.resolve(expandHomePath(trimmed, yield* HostProcessHomeDirectory)); }, ); diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index d823e84e45dd..c0ad95a63c61 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -38,6 +38,7 @@ import * as TerminalManager from "./terminal/Manager.ts"; import { isFilesystemRoot, managedWorktreesDirectories } from "./worktreesDirectory.ts"; import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; import { withWorkspaceLease } from "./workspace/workspaceLease.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const decodeCleanupThread = Schema.decodeUnknownEffect( Schema.fromJsonString(OrchestrationV2AppThreadJson), @@ -213,6 +214,7 @@ export const make = Effect.gen(function* () { serverSettings, config.worktreesDir, path, + yield* HostProcessHomeDirectory, )) { // An unmounted drive only skips its own worktrees. const root = yield* fs.exists(directory).pipe( diff --git a/apps/server/src/telemetry/Identify.ts b/apps/server/src/telemetry/Identify.ts index 4009625d5752..a0e6c1c7ecfd 100644 --- a/apps/server/src/telemetry/Identify.ts +++ b/apps/server/src/telemetry/Identify.ts @@ -1,4 +1,3 @@ -import * as NodeOS from "node:os"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Hex from "effect/encoding/Hex"; @@ -10,6 +9,7 @@ import * as Schema from "effect/Schema"; import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite"; import * as ServerConfig from "../config.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; /** * Codex writes `tokens` only for ChatGPT logins and omits the key for API-key, @@ -311,6 +311,7 @@ export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierFo Effect.orElseSucceed(() => null), ); -export const getTelemetryIdentifier = Effect.suspend(() => - getTelemetryIdentifierForHome(NodeOS.homedir()), +export const getTelemetryIdentifier = Effect.flatMap( + HostProcessHomeDirectory, + getTelemetryIdentifierForHome, ); diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index 8aa64408993e..2a16b9d44f75 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -42,7 +42,11 @@ import { } from "@t3tools/contracts"; import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker"; import * as KeyedLock from "@t3tools/shared/KeyedLock"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { + HostProcessArchitecture, + HostProcessHomeDirectory, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; import { mergePathEntries } from "@t3tools/shared/shell"; import { acpRegistryManagedBinaryDirectories } from "@t3tools/provider-acp-registry/server"; @@ -1317,6 +1321,7 @@ function createTerminalSpawnEnv( baseEnv: NodeJS.ProcessEnv, runtimeEnv: Record | null | undefined, platform: NodeJS.Platform, + home: string, ): NodeJS.ProcessEnv { const spawnEnv: NodeJS.ProcessEnv = {}; for (const [key, value] of Object.entries(baseEnv)) { @@ -1331,7 +1336,7 @@ function createTerminalSpawnEnv( ? Object.keys(spawnEnv).find((candidate) => candidate.toLowerCase() === key.toLowerCase()) : undefined; spawnEnv[existingKey ?? key] = - key === "CODEX_HOME" || key === "CLAUDE_CONFIG_DIR" ? expandHomePath(value) : value; + key === "CODEX_HOME" || key === "CLAUDE_CONFIG_DIR" ? expandHomePath(value, home) : value; } } // An explicit empty override opts out for terminals started without a client. @@ -1408,7 +1413,7 @@ export const resolveProviderInstanceTerminalEnvironment = Effect.fn( return yield* new TerminalProviderInstanceNotFoundError({ providerInstanceId }); } - let resolved = mergeProviderInstanceEnvironment(instance.environment, input.env ?? {}); + let resolved = yield* mergeProviderInstanceEnvironment(instance.environment, input.env ?? {}); if (instance.driver === "codex") { const config = decodeCodexSettings(instance.config ?? {}); if (Option.isSome(config)) { @@ -2244,7 +2249,12 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func Effect.andThen( Effect.gen(function* () { const shellCandidates = resolveShellCandidates(shellResolver, platform, baseEnv); - const terminalEnv = createTerminalSpawnEnv(baseEnv, session.runtimeEnv, platform); + const terminalEnv = createTerminalSpawnEnv( + baseEnv, + session.runtimeEnv, + platform, + yield* HostProcessHomeDirectory, + ); // Append (never prepend) managed ACP agent install directories so // `kimi login` and friends resolve by name without shadowing any // system or user tool of the same name. diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index 0477c7fa163b..c11fed75814d 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -26,6 +26,7 @@ import { } from "@t3tools/provider-core/server/textGenerationUtils"; import { codexModelFamily, getModelSelectionStringOptionValue } from "@t3tools/shared/model"; import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const CODEX_TIMEOUT_MS = 180_000; const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); @@ -214,7 +215,12 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func env: { ...effectiveEnvironment, ...(effectiveConfig.homePath - ? { CODEX_HOME: expandHomePath(effectiveConfig.homePath) } + ? { + CODEX_HOME: expandHomePath( + effectiveConfig.homePath, + yield* HostProcessHomeDirectory, + ), + } : {}), }, cwd, diff --git a/apps/server/src/usage/UsageService.ts b/apps/server/src/usage/UsageService.ts index ba65ba2887bb..2ceeb8750001 100644 --- a/apps/server/src/usage/UsageService.ts +++ b/apps/server/src/usage/UsageService.ts @@ -339,10 +339,10 @@ export const make = Effect.gen(function* () { * not decode. Disabled accounts still have history. An unconfigured default * slot runs with default config, just as it does in the provider registry. */ - const usageInstances = ( + const usageInstances = Effect.fn("UsageService.usageInstances")(function* ( driver: ProviderDriver, settings: ServerSettingsValue, - ): Array> => { + ) { const entries: Array< readonly [ProviderInstanceId, Pick, boolean] > = Object.entries(settings.providerInstances) @@ -352,13 +352,22 @@ export const make = Effect.gen(function* () { entries.push([ProviderInstanceId.make(driver.driverKind), {}, false]); } const decodeConfig = Schema.decodeUnknownOption(driver.configSchema); - return entries.map(([instanceId, instance, configured]) => ({ - instanceId, - config: Option.getOrUndefined(decodeConfig(instance.config ?? {})), - environment: mergeProviderInstanceEnvironment(instance.environment, hostEnvironment), - configured, - })); - }; + return yield* Effect.forEach( + entries, + Effect.fnUntraced(function* ([instanceId, instance, configured]) { + const instanceConfig: ProviderUsageInstance = { + instanceId, + config: Option.getOrUndefined(decodeConfig(instance.config ?? {})), + environment: yield* mergeProviderInstanceEnvironment( + instance.environment, + hostEnvironment, + ), + configured, + }; + return instanceConfig; + }), + ); + }); /** Resolves every transcript directory the usage readers point at. */ const resolveTranscriptDirs = Effect.fn("UsageService.resolveTranscriptDirs")(function* ( @@ -369,8 +378,9 @@ export const make = Effect.gen(function* () { const seen = new Set(); for (const { driver, reader } of transcriptReaders) { const { provider, format } = reader; - const directories = yield* Effect.forEach(usageInstances(driver, settings), (instance) => - reader.directories(instance), + const directories = yield* Effect.forEach( + yield* usageInstances(driver, settings), + (instance) => reader.directories(instance), ); for (const { dir: directory, fileName } of directories.flat()) { const sourceKey = provider + "\0" + directory; @@ -635,14 +645,12 @@ export const make = Effect.gen(function* () { const scans = Effect.forEach( scanReaders, ({ driver, reader }) => - reader - .scan({ - instances: usageInstances(driver, settings), - settings, - windowStartMs, - retentionCutoffMs, - awaitRefresh, - }) + usageInstances(driver, settings) + .pipe( + Effect.flatMap((instances) => + reader.scan({ instances, settings, windowStartMs, retentionCutoffMs, awaitRefresh }), + ), + ) .pipe( Effect.flatMap((sources) => Effect.forEach(sources, ({ volumeId, ...source }) => diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 002311fda2cf..b273e9eff52f 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -29,7 +29,7 @@ import { type VcsRef, } from "@t3tools/contracts"; import { dedupeRemoteBranchesWithLocalMatches, normalizeGitRemoteUrl } from "@t3tools/shared/git"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { compactTraceAttributes } from "@t3tools/shared/observability"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; import { parseT3ProjectFile } from "@t3tools/shared/t3ProjectFile"; @@ -3403,6 +3403,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* options?.worktreesDirectory ?? "", worktreesDir, path, + yield* HostProcessHomeDirectory, ); if (parentDir === null) { return yield* new GitCommandError({ diff --git a/apps/server/src/workspace/WorkspaceEntries.ts b/apps/server/src/workspace/WorkspaceEntries.ts index beac8d18a094..153360443338 100644 --- a/apps/server/src/workspace/WorkspaceEntries.ts +++ b/apps/server/src/workspace/WorkspaceEntries.ts @@ -19,7 +19,7 @@ import type { ProjectSearchEntriesInput, ProjectSearchEntriesResult, } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path"; import { normalizeSearchQuery } from "@t3tools/shared/searchRanking"; @@ -120,7 +120,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu } if (!isExplicitRelativePath(input.partialPath)) { - return path.resolve(expandHomePath(input.partialPath)); + return path.resolve(expandHomePath(input.partialPath, yield* HostProcessHomeDirectory)); } if (!input.cwd) { @@ -128,7 +128,10 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu partialPath: input.partialPath, }); } - return path.resolve(expandHomePath(input.cwd), input.partialPath); + return path.resolve( + expandHomePath(input.cwd, yield* HostProcessHomeDirectory), + input.partialPath, + ); }); /** @public Service construction is part of the canonical Effect module API. */ diff --git a/apps/server/src/workspace/WorkspacePaths.ts b/apps/server/src/workspace/WorkspacePaths.ts index 000a422bcdc8..748689330072 100644 --- a/apps/server/src/workspace/WorkspacePaths.ts +++ b/apps/server/src/workspace/WorkspacePaths.ts @@ -15,6 +15,7 @@ import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; export class WorkspaceRootNotExistsError extends Schema.TaggedError()( "WorkspaceRootNotExistsError", @@ -152,7 +153,9 @@ export const make = Effect.gen(function* () { const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn( "WorkspacePaths.normalizeWorkspaceRoot", )(function* (workspaceRoot, options) { - const normalizedWorkspaceRoot = path.resolve(expandHomePath(workspaceRoot.trim())); + const normalizedWorkspaceRoot = path.resolve( + expandHomePath(workspaceRoot.trim(), yield* HostProcessHomeDirectory), + ); let workspaceStat = yield* statWorkspaceRoot( workspaceRoot, normalizedWorkspaceRoot, diff --git a/apps/server/src/worktreesDirectory.ts b/apps/server/src/worktreesDirectory.ts index ee7154577458..65542b2c03bc 100644 --- a/apps/server/src/worktreesDirectory.ts +++ b/apps/server/src/worktreesDirectory.ts @@ -4,7 +4,8 @@ import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; /** * Directory new worktrees are created under: the `worktreesDirectory` - * setting, or `defaultDir` (`/worktrees`) when it is empty. Null when + * setting (`~` expands to `home`), or `defaultDir` (`/worktrees`) + * when it is empty. Null when * the setting is not an absolute path on this machine, such as `D:\worktrees` * configured for a Windows server and synced to a Linux one, or when it is a * filesystem root, which would make every path on that drive look managed. @@ -13,9 +14,10 @@ export function resolveWorktreesDirectory( setting: string, defaultDir: string, path: Path.Path, + home: string, ): string | null { if (setting === "") return defaultDir; - const expanded = expandHomePath(setting); + const expanded = expandHomePath(setting, home); if (!path.isAbsolute(expanded)) return null; const resolved = path.resolve(expanded); return isFilesystemRoot(resolved, path) ? null : resolved; @@ -34,10 +36,11 @@ export function managedWorktreesDirectories( }, defaultDir: string, path: Path.Path, + home: string, ): ReadonlyArray { const directories = new Set([defaultDir]); for (const setting of [settings.worktreesDirectory, ...settings.previousWorktreesDirectories]) { - const directory = resolveWorktreesDirectory(setting, defaultDir, path); + const directory = resolveWorktreesDirectory(setting, defaultDir, path, home); if (directory !== null) directories.add(directory); } return [...directories]; diff --git a/packages/provider-acp-registry/src/server/adapter.ts b/packages/provider-acp-registry/src/server/adapter.ts index 872eb0999802..4827ca4e4c56 100644 --- a/packages/provider-acp-registry/src/server/adapter.ts +++ b/packages/provider-acp-registry/src/server/adapter.ts @@ -278,7 +278,7 @@ export const AcpRegistryAdapterV2Driver: ProviderAdapterDriver< return yield* makeAcpRegistryAdapterV2({ instanceId: input.instanceId, settings: { ...input.config, enabled: input.enabled }, - environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment), + environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment), selfInvocation, nativeLogging: (threadId) => makeNativeLogger({ diff --git a/packages/provider-acp-registry/src/server/driver.ts b/packages/provider-acp-registry/src/server/driver.ts index bcfd83af7a39..4468aa70af39 100644 --- a/packages/provider-acp-registry/src/server/driver.ts +++ b/packages/provider-acp-registry/src/server/driver.ts @@ -497,7 +497,10 @@ export const AcpRegistryDriver: ProviderDriver(); const stderrTailRef = yield* Ref.make(""); + const homeDirectory = yield* HostProcessHomeDirectory; const stderrDrained = yield* Deferred.make(); const runtimeClosed = yield* Deferred.make(); const promptSerializationSemaphore = yield* Semaphore.make(1); @@ -1440,7 +1441,7 @@ export const make = ( Effect.ignore, Effect.andThen(Ref.get(stderrTailRef)), Effect.map((tail) => { - const stderr = sanitizeAcpStderrExcerpt(tail); + const stderr = sanitizeAcpStderrExcerpt(tail, homeDirectory); return stderr.length === 0 ? error : new EffectAcpErrors.AcpProcessExitedError({ diff --git a/packages/provider-acp/src/server/stderr.test.ts b/packages/provider-acp/src/server/stderr.test.ts index f5f704f04996..5e5c42b63cd7 100644 --- a/packages/provider-acp/src/server/stderr.test.ts +++ b/packages/provider-acp/src/server/stderr.test.ts @@ -20,6 +20,7 @@ describe("AcpStderr", () => { "Visit http://localhost:5733/pair#token=ABCDEF for pairing", "key=sk-abcdefghijklmnopqrstuv", ].join("\n"), + "/home/server", { HOME: "/Users/ada" }, ); @@ -41,6 +42,8 @@ describe("AcpStderr", () => { "Authorization: Basic dXNlcjpwYXNz", "x-api-key: ant-api-key-value", ].join("\n"), + "/Users/ada", + {}, ); expect(excerpt).toContain("[redacted]"); diff --git a/packages/provider-acp/src/server/stderr.ts b/packages/provider-acp/src/server/stderr.ts index 52f68bb4c19a..01ed491d8732 100644 --- a/packages/provider-acp/src/server/stderr.ts +++ b/packages/provider-acp/src/server/stderr.ts @@ -1,6 +1,3 @@ -// @effect-diagnostics nodeBuiltinImport:off -- The excerpt sanitizer masks the home directory, which only the Node os module can resolve. -import * as NodeOS from "node:os"; - /** Last few KiB of ACP child stderr kept for startup / exit diagnostics. */ export const ACP_STDERR_TAIL_MAX_CHARS = 4_096; @@ -19,10 +16,11 @@ export function appendAcpStderrTail(current: string, chunk: string): string { /** Bounded, redacted excerpt safe to put on user-facing adapter errors. */ export function sanitizeAcpStderrExcerpt( text: string, + homeDirectory: string, environment: NodeJS.ProcessEnv = process.env, ): string { let result = text.replaceAll("\0", ""); - const homes = [environment.HOME, environment.USERPROFILE, NodeOS.homedir()].filter( + const homes = [environment.HOME, environment.USERPROFILE, homeDirectory].filter( (value): value is string => typeof value === "string" && value.length > 1, ); for (const home of new Set(homes)) { diff --git a/packages/provider-core/src/server/instanceEnvironment.test.ts b/packages/provider-core/src/server/instanceEnvironment.test.ts index 73d2ae3c313c..e64cd31650f5 100644 --- a/packages/provider-core/src/server/instanceEnvironment.test.ts +++ b/packages/provider-core/src/server/instanceEnvironment.test.ts @@ -1,9 +1,8 @@ -import * as NodeOS from "node:os"; - import * as NodeServices from "@effect/platform-node/NodeServices"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Path from "effect/Path"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import { mergeProviderInstanceEnvironment } from "./instanceEnvironment.ts"; @@ -18,7 +17,7 @@ describe("mergeProviderInstanceEnvironment", () => { CODEX_HOME: "~/.inherited-codex", CLAUDE_CONFIG_DIR: "~/.inherited-claude", }; - const environment = mergeProviderInstanceEnvironment( + const environment = yield* mergeProviderInstanceEnvironment( [ { name: "CODEX_HOME", value, sensitive: false }, { name: "CLAUDE_CONFIG_DIR", value, sensitive: false }, @@ -28,41 +27,48 @@ describe("mergeProviderInstanceEnvironment", () => { ); expect(environment).toEqual({ - CODEX_HOME: path.join(NodeOS.homedir(), tail), - CLAUDE_CONFIG_DIR: path.join(NodeOS.homedir(), tail), + CODEX_HOME: path.join("/home/ada", tail), + CLAUDE_CONFIG_DIR: path.join("/home/ada", tail), CUSTOM_VALUE: value, }); expect(baseEnv).toEqual({ CODEX_HOME: "~/.inherited-codex", CLAUDE_CONFIG_DIR: "~/.inherited-claude", }); - }).pipe(Effect.provide(NodeServices.layer)), + }).pipe( + Effect.provideService(HostProcessHomeDirectory, "/home/ada"), + Effect.provide(NodeServices.layer), + ), ); - it("leaves inherited provider homes unchanged", () => { - const baseEnv = { CODEX_HOME: "~/.codex", CLAUDE_CONFIG_DIR: "~\\.claude" }; + it.effect("leaves inherited provider homes unchanged", () => + Effect.gen(function* () { + const baseEnv = { CODEX_HOME: "~/.codex", CLAUDE_CONFIG_DIR: "~\\.claude" }; - expect( - mergeProviderInstanceEnvironment( - [{ name: "CUSTOM_VALUE", value: "~/.custom", sensitive: false }], - baseEnv, - ), - ).toEqual({ ...baseEnv, CUSTOM_VALUE: "~/.custom" }); - }); + expect( + yield* mergeProviderInstanceEnvironment( + [{ name: "CUSTOM_VALUE", value: "~/.custom", sensitive: false }], + baseEnv, + ), + ).toEqual({ ...baseEnv, CUSTOM_VALUE: "~/.custom" }); + }), + ); - it("overrides inherited environment values and preserves empty strings", () => { - expect( - mergeProviderInstanceEnvironment( - [ - { name: "OPENROUTER_API_KEY", value: "sk-or-test", sensitive: true }, - { name: "ANTHROPIC_API_KEY", value: "", sensitive: false }, - ], - { ANTHROPIC_API_KEY: "inherited", PATH: "/bin" }, - ), - ).toMatchObject({ - OPENROUTER_API_KEY: "sk-or-test", - ANTHROPIC_API_KEY: "", - PATH: "/bin", - }); - }); + it.effect("overrides inherited environment values and preserves empty strings", () => + Effect.gen(function* () { + expect( + yield* mergeProviderInstanceEnvironment( + [ + { name: "OPENROUTER_API_KEY", value: "sk-or-test", sensitive: true }, + { name: "ANTHROPIC_API_KEY", value: "", sensitive: false }, + ], + { ANTHROPIC_API_KEY: "inherited", PATH: "/bin" }, + ), + ).toMatchObject({ + OPENROUTER_API_KEY: "sk-or-test", + ANTHROPIC_API_KEY: "", + PATH: "/bin", + }); + }), + ); }); diff --git a/packages/provider-core/src/server/instanceEnvironment.ts b/packages/provider-core/src/server/instanceEnvironment.ts index ff8c9815731a..0ddb61af4b5c 100644 --- a/packages/provider-core/src/server/instanceEnvironment.ts +++ b/packages/provider-core/src/server/instanceEnvironment.ts @@ -1,22 +1,25 @@ import type { ProviderInstanceEnvironment } from "@t3tools/contracts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as Effect from "effect/Effect"; import { expandHomePath } from "./pathExpansion.ts"; -export function mergeProviderInstanceEnvironment( +export const mergeProviderInstanceEnvironment = Effect.fn(function* ( environment: ProviderInstanceEnvironment | undefined, baseEnv: NodeJS.ProcessEnv = process.env, -): NodeJS.ProcessEnv { +) { if (!environment || environment.length === 0) { return baseEnv; } + const home = yield* HostProcessHomeDirectory; const next: NodeJS.ProcessEnv = { ...baseEnv }; for (const variable of environment) { // Child processes do not apply shell expansion to environment values. next[variable.name] = variable.name === "CODEX_HOME" || variable.name === "CLAUDE_CONFIG_DIR" - ? expandHomePath(variable.value) + ? expandHomePath(variable.value, home) : variable.value; } return next; -} +}); diff --git a/packages/provider-core/src/server/pathExpansion.test.ts b/packages/provider-core/src/server/pathExpansion.test.ts index cc7c85786da6..46a9faf87e8a 100644 --- a/packages/provider-core/src/server/pathExpansion.test.ts +++ b/packages/provider-core/src/server/pathExpansion.test.ts @@ -1,34 +1,35 @@ // @effect-diagnostics nodeBuiltinImport:off -import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import { describe, expect, it } from "vite-plus/test"; import { expandHomePath } from "./pathExpansion.ts"; +const home = "/home/ada"; + describe("expandHomePath", () => { it("returns an empty string unchanged", () => { - expect(expandHomePath("")).toBe(""); + expect(expandHomePath("", home)).toBe(""); }); it("returns paths without a leading tilde unchanged", () => { - expect(expandHomePath("/absolute/path")).toBe("/absolute/path"); - expect(expandHomePath("relative/path")).toBe("relative/path"); - expect(expandHomePath("some~weird~path")).toBe("some~weird~path"); + expect(expandHomePath("/absolute/path", home)).toBe("/absolute/path"); + expect(expandHomePath("relative/path", home)).toBe("relative/path"); + expect(expandHomePath("some~weird~path", home)).toBe("some~weird~path"); }); it("expands a lone tilde to the home directory", () => { - expect(expandHomePath("~")).toBe(NodeOS.homedir()); + expect(expandHomePath("~", home)).toBe(home); }); it("expands ~/ to a subpath of the home directory", () => { - expect(expandHomePath("~/.codex-work")).toBe(NodePath.join(NodeOS.homedir(), ".codex-work")); + expect(expandHomePath("~/.codex-work", home)).toBe(NodePath.join(home, ".codex-work")); }); it("expands a Windows-style ~\\ prefix", () => { - expect(expandHomePath("~\\.codex")).toBe(NodePath.join(NodeOS.homedir(), ".codex")); + expect(expandHomePath("~\\.codex", home)).toBe(NodePath.join(home, ".codex")); }); it("does not expand ~user paths", () => { - expect(expandHomePath("~alice/foo")).toBe("~alice/foo"); + expect(expandHomePath("~alice/foo", home)).toBe("~alice/foo"); }); }); diff --git a/packages/provider-core/src/server/pathExpansion.ts b/packages/provider-core/src/server/pathExpansion.ts index bacdaece0b1c..d486b2988d47 100644 --- a/packages/provider-core/src/server/pathExpansion.ts +++ b/packages/provider-core/src/server/pathExpansion.ts @@ -1,12 +1,12 @@ // @effect-diagnostics nodeBuiltinImport:off -import * as NodeOS from "node:os"; import * as NodePath from "node:path"; /** - * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the - * current user's home directory. Spawned processes don't get shell - * expansion, so env vars like `CODEX_HOME=~/.codex-work` would be passed - * verbatim and treated as relative paths by the receiver. + * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to `home`, + * the current user's home directory (`HostProcessHomeDirectory`). Spawned + * processes don't get shell expansion, so env vars like + * `CODEX_HOME=~/.codex-work` would be passed verbatim and treated as relative + * paths by the receiver. * * Matches the behavior of the other `expandHomePath` helpers in the * workspace layers and CLI bootstrap: `~` alone and both `~/` and `~\` @@ -14,11 +14,11 @@ import * as NodePath from "node:path"; * start with `~` or is empty. Does not handle `~user` (other-user) * expansion. */ -export function expandHomePath(value: string): string { +export function expandHomePath(value: string, home: string): string { if (!value) return value; - if (value === "~") return NodeOS.homedir(); + if (value === "~") return home; if (value.startsWith("~/") || value.startsWith("~\\")) { - return NodePath.join(NodeOS.homedir(), value.slice(2)); + return NodePath.join(home, value.slice(2)); } return value; } diff --git a/packages/provider-cursor/src/server/CursorUsageAccounts.ts b/packages/provider-cursor/src/server/CursorUsageAccounts.ts index 852d746aed00..56f64104a449 100644 --- a/packages/provider-cursor/src/server/CursorUsageAccounts.ts +++ b/packages/provider-cursor/src/server/CursorUsageAccounts.ts @@ -10,12 +10,15 @@ * * @module provider-cursor/server/CursorUsageAccounts */ -import * as NodeOS from "node:os"; import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; import type { ProviderUsageScan } from "@t3tools/provider-core/server/usage"; import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { + HostProcessEnvironment, + HostProcessHomeDirectory, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; import * as Deferred from "effect/Deferred"; @@ -283,7 +286,7 @@ const make = Effect.gen(function* () { const scan: CursorUsageAccounts["Service"]["scan"] = Effect.fn("CursorUsageAccounts.scan")( function* ({ keychainUsageEnabled, windowStartMs, retentionCutoffMs, awaitRefresh }) { - const home = NodeOS.homedir(); + const home = yield* HostProcessHomeDirectory; const userHome = (platform === "win32" ? hostEnvironment["USERPROFILE"] : hostEnvironment["HOME"]) || home; const configHome = hostEnvironment["XDG_CONFIG_HOME"]?.trim(); diff --git a/packages/provider-cursor/src/server/adapter.ts b/packages/provider-cursor/src/server/adapter.ts index ecca0e80ac7a..fa557d6d8bc1 100644 --- a/packages/provider-cursor/src/server/adapter.ts +++ b/packages/provider-cursor/src/server/adapter.ts @@ -2627,7 +2627,7 @@ export const CursorAdapterV2Driver: ProviderAdapterDriver< ...input.config, enabled: input.enabled, }, - environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment), + environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment), }); }, (effect, input) => diff --git a/packages/provider-cursor/src/server/driver.ts b/packages/provider-cursor/src/server/driver.ts index a9fa4b31acb4..2c748bb70d5e 100644 --- a/packages/provider-cursor/src/server/driver.ts +++ b/packages/provider-cursor/src/server/driver.ts @@ -99,7 +99,7 @@ export const CursorDriver: ProviderDriver< const httpClient = yield* HttpClient.HttpClient; const keychain = yield* CursorKeychain.CursorKeychain; const sdkRunner = yield* CursorAgentSdk.CursorAgentSdkRunner; - const processEnv = mergeProviderInstanceEnvironment(environment); + const processEnv = yield* mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, diff --git a/packages/provider-cursor/src/server/skills.ts b/packages/provider-cursor/src/server/skills.ts index c8cd561dfea0..4c7b0bbea985 100644 --- a/packages/provider-cursor/src/server/skills.ts +++ b/packages/provider-cursor/src/server/skills.ts @@ -8,7 +8,6 @@ * * @module provider/Drivers/CursorSkills */ -import * as NodeOS from "node:os"; import type { ServerProviderSkill } from "@t3tools/contracts"; import * as ByteSize from "effect/ByteSize"; @@ -18,6 +17,7 @@ import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import { parse as parseYamlDocument } from "yaml"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/; const SKILL_MENTION_PATTERN = @@ -221,7 +221,10 @@ const inspectCursorSkills = Effect.fn("inspectCursorSkills")(function* ( environment: NodeJS.ProcessEnv = process.env, ) { const path = yield* Path.Path; - const userHome = environment.HOME?.trim() || environment.USERPROFILE?.trim() || NodeOS.homedir(); + const userHome = + environment.HOME?.trim() || + environment.USERPROFILE?.trim() || + (yield* HostProcessHomeDirectory); const rootsBelow = (base: string, scope: "user" | "project") => [ { directory: path.join(base, ".cursor", "skills"), scope }, { directory: path.join(base, ".agents", "skills"), scope }, diff --git a/packages/provider-cursor/src/server/textGeneration.ts b/packages/provider-cursor/src/server/textGeneration.ts index 59fa3dfc452d..6a6b8ae989cf 100644 --- a/packages/provider-cursor/src/server/textGeneration.ts +++ b/packages/provider-cursor/src/server/textGeneration.ts @@ -1,4 +1,3 @@ -import * as NodeOS from "node:os"; import * as FileSystem from "effect/FileSystem"; import type { AgentOptions, RunResult } from "@cursor/sdk"; @@ -13,6 +12,7 @@ import * as TextGenerationOperations from "@t3tools/provider-core/server/textGen import { cursorSdkModelSelection } from "./sdkModel.ts"; import type { CursorAuth } from "./auth.ts"; import * as CursorSdk from "./CursorSdk.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const CURSOR_TIMEOUT_MS = 180_000; @@ -90,7 +90,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu const apiKey = yield* resolveCursorApiKey(operation); // The SDK loads sandbox.json independently of settingSources and lets it // expand the writable paths. Its public API cannot override that policy. - if (yield* fs.exists(`${NodeOS.homedir()}/.cursor/sandbox.json`)) { + if (yield* fs.exists(`${yield* HostProcessHomeDirectory}/.cursor/sandbox.json`)) { return yield* new TextGenerationError({ operation, detail: diff --git a/packages/provider-cursor/src/server/usageLimits.ts b/packages/provider-cursor/src/server/usageLimits.ts index 57f74bc54488..dd29a443cd3a 100644 --- a/packages/provider-cursor/src/server/usageLimits.ts +++ b/packages/provider-cursor/src/server/usageLimits.ts @@ -1,7 +1,6 @@ -import * as NodeOS from "node:os"; import type { ServerProviderUsageWindow } from "@t3tools/contracts"; import type { CursorSettings } from "../settings.ts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { CURSOR_USAGE_WINDOWS } from "@t3tools/shared/usageLimits"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; @@ -109,7 +108,8 @@ export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function token = (yield* keychain.accessToken)?.trim(); } else if (!token) { const home = - (platform === "win32" ? environment.USERPROFILE : environment.HOME) || NodeOS.homedir(); + (platform === "win32" ? environment.USERPROFILE : environment.HOME) || + (yield* HostProcessHomeDirectory); const directory = platform === "win32" ? path.join(environment.APPDATA || path.join(home, "AppData", "Roaming"), "Cursor") diff --git a/packages/provider-grok/src/server/adapter.ts b/packages/provider-grok/src/server/adapter.ts index 8100246ace18..cca040e81698 100644 --- a/packages/provider-grok/src/server/adapter.ts +++ b/packages/provider-grok/src/server/adapter.ts @@ -4,7 +4,11 @@ import { isXAiTaskCompletedWakeNotification, xAiRateLimitedErrorCode, } from "./xaiAcpExtension.ts"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { + HostProcessEnvironment, + HostProcessHomeDirectory, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime"; import { defaultInstanceIdForDriver, @@ -227,7 +231,10 @@ export function grokLaunchRuntimeMode( } /** The flavor runs Grok's launcher through the adapter's spawner. */ -export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdapterV2Flavor { +export function makeGrokAcpAdapterFlavor( + options: GrokAdapterV2Options, + homeDirectory: string, +): AcpAdapterV2Flavor { return { driver: GROK_PROVIDER, runtimeHarness: "Grok", @@ -311,6 +318,7 @@ export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdap extractGrokPlanMarkdownFromToolCallData(toolCall.data, { platform: options.hostPlatform, environment: options.environment, + homeDirectory, }), extractBackgroundTaskId: extractXAiMonitorTaskId, extractBackgroundToolMutation: extractXAiAcpBackgroundToolMutation, @@ -331,7 +339,7 @@ export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* ( ) { return yield* makeAcpAdapterV2({ instanceId: options.instanceId, - flavor: makeGrokAcpAdapterFlavor(options), + flavor: makeGrokAcpAdapterFlavor(options, yield* HostProcessHomeDirectory), selfInvocation: options.selfInvocation, ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }), ...(options.continuationRequests === undefined @@ -366,7 +374,7 @@ export const GrokAdapterV2Driver: ProviderAdapterDriver const path = yield* Path.Path; const host = yield* ProviderHost.ProviderHost; const { cwd } = host.paths; - const processEnv = mergeProviderInstanceEnvironment(environment); + const processEnv = yield* mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, diff --git a/packages/provider-grok/src/server/usage.ts b/packages/provider-grok/src/server/usage.ts index 9f2be3c04299..8b5c0db45ea6 100644 --- a/packages/provider-grok/src/server/usage.ts +++ b/packages/provider-grok/src/server/usage.ts @@ -4,7 +4,6 @@ * * @module provider-grok/server/usage */ -import * as NodeOS from "node:os"; import type { UsageTokenTotals } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; @@ -20,6 +19,7 @@ import { } from "@t3tools/provider-core/server/usage"; import type { GrokSettings } from "../settings.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; /** * Grok reports cost in integer ticks where `1 USD = 10^10` ticks. See Grok @@ -229,8 +229,10 @@ export const grokUsageReader: ProviderUsageReader = { format: grokUsageFormat, directories: Effect.fn("grokUsageReader.directories")(function* ({ environment }) { const path = yield* Path.Path; + const homeDirectory = yield* HostProcessHomeDirectory; const home = expandHomePath( - environment.GROK_HOME?.trim() || path.join(NodeOS.homedir(), ".grok"), + environment.GROK_HOME?.trim() || path.join(homeDirectory, ".grok"), + homeDirectory, ); // Sessions also ship multi-megabyte `chat_history` and `events` logs that // never carry usage; only `updates.jsonl` does. diff --git a/packages/provider-grok/src/server/usageLimits.ts b/packages/provider-grok/src/server/usageLimits.ts index 83369c062c16..b7db7f6fd79a 100644 --- a/packages/provider-grok/src/server/usageLimits.ts +++ b/packages/provider-grok/src/server/usageLimits.ts @@ -1,4 +1,3 @@ -import * as NodeOS from "node:os"; import type { ServerProviderUsageWindow } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; @@ -14,6 +13,7 @@ import { makeUnavailableUsageLimits, makeUsageLimits, } from "@t3tools/provider-core/server/usageLimits"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const GrokCredentials = Schema.Record( Schema.String, @@ -100,7 +100,10 @@ const readGrokCredential = Effect.fn("readGrokCredential")(function* ( const path = yield* Path.Path; const home = environment.GROK_HOME?.trim() || - path.join(environment.HOME || environment.USERPROFILE || NodeOS.homedir(), ".grok"); + path.join( + environment.HOME || environment.USERPROFILE || (yield* HostProcessHomeDirectory), + ".grok", + ); for (const configPath of [ path.join(home, "config.toml"), path.join(home, "managed_config.toml"), diff --git a/packages/provider-grok/src/server/xaiAcpExtension.test.ts b/packages/provider-grok/src/server/xaiAcpExtension.test.ts index 5dce41edead9..18803e0f39e4 100644 --- a/packages/provider-grok/src/server/xaiAcpExtension.test.ts +++ b/packages/provider-grok/src/server/xaiAcpExtension.test.ts @@ -1,5 +1,3 @@ -// @effect-diagnostics nodeBuiltinImport:off -- plan-path tests resolve the OS home dir. -import * as NodeOS from "node:os"; import { it } from "@effect/vitest"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -1902,13 +1900,14 @@ describe("Grok exit_plan_mode capture (#8358)", () => { expect(extractXAiExitPlanMarkdown(wrapped)).toBe(XAI_EMPTY_PLAN_MARKDOWN); }); it("identifies Grok plan.md paths and extracts markdown from tool call data", () => { - const linuxHost = { platform: "linux" as const, environment: {} }; - const windowsHost = { platform: "win32" as const, environment: {} }; + const home = "/home/ada"; + const linuxHost = { platform: "linux" as const, environment: {}, homeDirectory: home }; + const windowsHost = { platform: "win32" as const, environment: {}, homeDirectory: home }; const grokHomeHost = { platform: "linux" as const, environment: { GROK_HOME: "/opt/grok-data" }, + homeDirectory: home, }; - const home = NodeOS.homedir().replace(/\\/g, "/"); const sessionPlan = `${home}/.grok/sessions/abc/plan.md`; const nestedSessionPlan = `${home}/.grok/sessions/%2Fhome%2Fproj/019fd20e-c563-70a0-b801-a6bc51815a9b/plan.md`; expect(isGrokPlanMarkdownPath(sessionPlan, linuxHost)).toBe(true); @@ -1935,6 +1934,7 @@ describe("Grok exit_plan_mode capture (#8358)", () => { isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", { platform: "win32", environment: { GROK_HOME: "/opt/grok-data" }, + homeDirectory: home, }), ).toBe(true); expect(isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", grokHomeHost)).toBe( diff --git a/packages/provider-grok/src/server/xaiAcpExtension.ts b/packages/provider-grok/src/server/xaiAcpExtension.ts index 4c424a664d90..3fe2c2886dea 100644 --- a/packages/provider-grok/src/server/xaiAcpExtension.ts +++ b/packages/provider-grok/src/server/xaiAcpExtension.ts @@ -1,6 +1,3 @@ -// @effect-diagnostics nodeBuiltinImport:off -- Grok's plan file lives under the OS home dir. -import * as NodeOS from "node:os"; - import type { ProviderUserInputAnswers, UserInputQuestion } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Deferred from "effect/Deferred"; @@ -1088,11 +1085,13 @@ function addGrokSessionPrefix( export interface GrokPlanPathHost { readonly platform: NodeJS.Platform; readonly environment: NodeJS.ProcessEnv; + readonly homeDirectory: string; } -function grokPlanSessionPrefixes(environment: NodeJS.ProcessEnv): ReadonlySet { +function grokPlanSessionPrefixes(host: GrokPlanPathHost): ReadonlySet { + const { environment } = host; const prefixes = new Set(); - addGrokSessionPrefix(prefixes, NodeOS.homedir(), true); + addGrokSessionPrefix(prefixes, host.homeDirectory, true); addGrokSessionPrefix(prefixes, "~", true); addGrokSessionPrefix(prefixes, environment.HOME ?? "", true); addGrokSessionPrefix(prefixes, environment.USERPROFILE ?? "", true); @@ -1134,7 +1133,7 @@ export function isGrokPlanMarkdownPath( ) { return false; } - for (const prefix of grokPlanSessionPrefixes(host.environment)) { + for (const prefix of grokPlanSessionPrefixes(host)) { const needle = win32 ? prefix.toLowerCase() : prefix; if (!haystack.startsWith(needle)) { continue; diff --git a/packages/provider-muse/src/server/driver.ts b/packages/provider-muse/src/server/driver.ts index 1d2922e54270..3b9df42100a6 100644 --- a/packages/provider-muse/src/server/driver.ts +++ b/packages/provider-muse/src/server/driver.ts @@ -1,6 +1,6 @@ import { ProviderDriverKind } from "@t3tools/contracts"; import { MuseSettings } from "../settings.ts"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -71,14 +71,14 @@ export const MuseDriver: ProviderDriver = { const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; const hostEnvironment = yield* HostProcessEnvironment; // Drop an inherited META_API_KEY so Muse uses its login; an instance value still wins. - const processEnvironment = mergeProviderInstanceEnvironment( + const processEnvironment = yield* mergeProviderInstanceEnvironment( environment, makeMuseEnvironment(hostEnvironment), ); const effectiveConfig = { ...config, enabled, - binaryPath: expandHomePath(config.binaryPath), + binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory), } satisfies MuseSettings; const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, diff --git a/packages/provider-opencode/src/server/adapter.ts b/packages/provider-opencode/src/server/adapter.ts index 96d535070294..c7020b8ff45a 100644 --- a/packages/provider-opencode/src/server/adapter.ts +++ b/packages/provider-opencode/src/server/adapter.ts @@ -3744,7 +3744,7 @@ export const OpenCodeAdapterV2Driver: ProviderAdapterDriver< return yield* makeOpenCodeAdapterV2({ instanceId: input.instanceId, settings: { ...input.config, enabled: input.enabled }, - environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment), + environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment), ...(providerEventLoggers.native === undefined ? {} : { nativeEventLogger: providerEventLoggers.native }), diff --git a/packages/provider-opencode/src/server/driver.ts b/packages/provider-opencode/src/server/driver.ts index 0573db664969..f3340a62e9e0 100644 --- a/packages/provider-opencode/src/server/driver.ts +++ b/packages/provider-opencode/src/server/driver.ts @@ -206,7 +206,7 @@ export const OpenCodeDriver: ProviderDriver< const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions; const crypto = yield* Crypto.Crypto; const host = yield* ProviderHost.ProviderHost; - const processEnv = mergeProviderInstanceEnvironment(environment); + const processEnv = yield* mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, diff --git a/packages/provider-opencode/src/server/usage.ts b/packages/provider-opencode/src/server/usage.ts index f0005ad61608..65897f85d98e 100644 --- a/packages/provider-opencode/src/server/usage.ts +++ b/packages/provider-opencode/src/server/usage.ts @@ -4,7 +4,6 @@ * * @module provider-opencode/server/usage */ -import * as NodeOS from "node:os"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; import { @@ -12,7 +11,7 @@ import { type ProviderUsageReader, type UsageRecord, } from "@t3tools/provider-core/server/usage"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; @@ -259,6 +258,7 @@ const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* ( const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const environment = yield* HostProcessEnvironment; + const homeDirectory = yield* HostProcessHomeDirectory; const roots = environment["OPENCODE_DATA_DIR"] ?.split(",") .map((value) => value.trim()) @@ -268,13 +268,13 @@ const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* ( path.join( dataHome && path.isAbsolute(dataHome) ? dataHome - : path.join(NodeOS.homedir(), ".local", "share"), + : path.join(homeDirectory, ".local", "share"), "opencode", ), ]; const canonical = new Set(); for (const root of roots?.length ? roots : defaults) { - const resolved = path.resolve(expandHomePath(root)); + const resolved = path.resolve(expandHomePath(root, homeDirectory)); canonical.add(yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved))); } return [...canonical]; diff --git a/packages/provider-opencode/src/server/usageLimits.ts b/packages/provider-opencode/src/server/usageLimits.ts index 3c21536b8798..05c3b79d411a 100644 --- a/packages/provider-opencode/src/server/usageLimits.ts +++ b/packages/provider-opencode/src/server/usageLimits.ts @@ -1,5 +1,3 @@ -import * as NodeOS from "node:os"; - import type { ServerProviderUsageWindow } from "@t3tools/contracts"; import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; @@ -18,6 +16,7 @@ import { makeUnavailableUsageLimits, makeUsageLimits, } from "@t3tools/provider-core/server/usageLimits"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const AuthFile = Schema.Struct({ "opencode-go": Schema.optionalKey(Schema.Unknown) }); const ApiAuth = Schema.Struct({ type: Schema.Literal("api"), key: Schema.String }); @@ -47,7 +46,11 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")( const env = input.environment; const dataHome = env.XDG_DATA_HOME || - path.join(env.HOME || env.USERPROFILE || NodeOS.homedir(), ".local", "share"); + path.join( + env.HOME || env.USERPROFILE || (yield* HostProcessHomeDirectory), + ".local", + "share", + ); const authPath = path.join(dataHome, "opencode", "auth.json"); const contents = env.OPENCODE_AUTH_CONTENT || diff --git a/packages/provider-pi/src/server/adapter.ts b/packages/provider-pi/src/server/adapter.ts index eecba9c93aad..c6bdc4338ab3 100644 --- a/packages/provider-pi/src/server/adapter.ts +++ b/packages/provider-pi/src/server/adapter.ts @@ -3276,7 +3276,7 @@ export const PiAdapterV2Driver: ProviderAdapterDriver = { const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions; const host = yield* ProviderHost.ProviderHost; const { cwd } = host.paths; - const processEnv = mergeProviderInstanceEnvironment(environment); + const processEnv = yield* mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, diff --git a/packages/shared/src/hostProcess.ts b/packages/shared/src/hostProcess.ts index 9bd41a825044..d729624b7630 100644 --- a/packages/shared/src/hostProcess.ts +++ b/packages/shared/src/hostProcess.ts @@ -25,6 +25,13 @@ export const HostProcessHostname = Context.Reference( }, ); +export const HostProcessHomeDirectory = Context.Reference( + "@t3tools/shared/hostProcess/HostProcessHomeDirectory", + { + defaultValue: () => NodeOS.homedir(), + }, +); + export const HostProcessEnvironment = Context.Reference( "@t3tools/shared/hostProcess/HostProcessEnvironment", { diff --git a/packages/source-control-forgejo/src/server/ForgejoCli.ts b/packages/source-control-forgejo/src/server/ForgejoCli.ts index 73b0098aa403..46afc4522b45 100644 --- a/packages/source-control-forgejo/src/server/ForgejoCli.ts +++ b/packages/source-control-forgejo/src/server/ForgejoCli.ts @@ -6,13 +6,12 @@ import * as Result from "effect/Result"; import * as Clock from "effect/Clock"; import * as FileSystem from "effect/FileSystem"; import * as Semaphore from "effect/Semaphore"; -import * as NodeOS from "node:os"; // @effect-diagnostics-next-line nodeBuiltinImport:off - fj storage paths use explicit Windows and POSIX layouts, independently of this process's platform. import * as NodePath from "node:path"; import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http"; import { ChildProcessSpawner } from "effect/process"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText"; import type { SourceControlProviderContext } from "@t3tools/source-control-core/server/SourceControlProvider"; @@ -250,7 +249,7 @@ export const make = Effect.gen(function* () { const readKeys = Effect.fn("ForgejoCli.readKeys")(function* (cwd: string) { for (const path of forgejoKeysPaths({ platform: yield* HostProcessPlatform, - home: NodeOS.homedir(), + home: yield* HostProcessHomeDirectory, ...(globalThis.process.env.XDG_DATA_HOME ? { dataHome: globalThis.process.env.XDG_DATA_HOME } : {}),